Metasploit 데이터를 Httpx로?

오랜만에 Metasploit 관련 글을 쓰는 것 같습니다. 다름이 아니라 netpen이라는 plugin을 하나 찾았는데, 이를 이용하면 Metasploit으로 수집한 정보를 가지고 nuclei나 zap/burp 등 다른 도구와 파이프 라인으로 구성해서 사용하기 좋아보였습니다.

Netpen

공식 플러그인은 아니고 wdahlenburg가 만들어둔 플러그인 스크립트로 metasploit에서 수집된 정보를 host:port 형태로 콤보 리스트를 만들어줍니다. 간단한 작업이지만 막상 metasploit으로 수집하고 이를 다시 파싱하려면 약간 귀찮은데요. 이 플러그인은 이러한 점을 딱 해결해줍니다.

  • https://github.com/wdahlenburg/MSF-Plugins/blob/main/netpen.rb

Add plugin

repo에서 코드를 받아서 metasploit의 plugin 디렉토리에 넣어줍니다. 코드는 #netpen code부분에 추가해두었습니다.

wget -P /opt/metasploit-framework/embedded/framework/plugins/ \
https://raw.githubusercontent.com/wdahlenburg/MSF-Plugins/main/netpen.rb

이후 metasploit에서 load 명령으로 netpen 플러그인을 로드할 수 있습니다.

msf6> load netpen
[*] Successfully loaded plugin: Network Pentest Toolset

Test

Recon for testing

테스트를 위해 임의로 몇개 도메인을 nmap으로 포트스캐닝했습니다.

msf6 > db_nmap -PN www.hahwul.com dalfox.hahwul.com testphp.vulnweb.com

이후 services 명령으로 리스트를 살펴보면 80,443 등으로 수집된 것을 볼 수 있습니다.

msf6 > services
Services
========

host             port  proto  name   state     info
----             ----  -----  ----   -----     ----
44.228.249.3     80    tcp    http   open
44.228.249.3     443   tcp    https  filtered
185.199.108.153  80    tcp    http   open
185.199.108.153  443   tcp    https  open
185.199.110.153  80    tcp    http   open
185.199.110.153  443   tcp    https  open

metasploit이 pentesting 도구로써는 좋지만, 웹 취약점을 테스팅하기에는 솔직히 어려운 부분들이 좀 많습니다. 그나마 CVE나 Exploit 기반 테스팅을 쉽게할 수 있다는 장점이 있었지만, projectdiscovery의 nuclei가 나온 이후 시점부턴 솔직히 좀 매력이 떨어지긴 합니다.

어쩄던 metasaploit에서 host:port 형태로 콤보 데이터를 뽑는건 조금 번거롭습니다.

Make combo

자 그럼 netpen을 이용해서 콤보 리스트를 뽑아봅시다. help를 보면 netpen에서 지원하는 커맨드를 확인할 수 있습니다.

msf6 > help

Network Pentest Toolset Commands
================================

    Command         Description
    -------         -----------
    grab_all        List all services in host:port format
    grab_host_port  List all related hosts in host:port format based on searchable parameters
    grab_web        List all web related hosts in host:port format to be passed into httprobe
    list_services   List all open services

기호에 맞게 사용하시면 되고, 테스트로는 grab_all로 리스트를 뽑아봅시다.

msf6 > grab_all -f host-ports.txt

이후 metasploit을 종료한 후 파일을 열어보면 콤보 리스트 형태로 저장된 것을 볼 수 있습니다.

Send httpx

자 익숙한 포맷(host:port)이 나왔으니 httpx로 전달해서 웹 서비스를 식별해봅시다.

cat host_ports.txt | httpx -sc -title

Netpen code

module Msf
  class Plugin::NetPen < Msf::Plugin
    class NetPenDispatcher
      include Msf::Ui::Console::CommandDispatcher

      def name
        'Network Pentest Toolset'
      end

      def commands
        {
          'grab_web' => 'List all web related hosts in host:port format to be passed into httprobe',
          'grab_all' => 'List all services in host:port format',
          'grab_host_port' => 'List all related hosts in host:port format based on searchable parameters',
          'list_services' => 'List all open services'
        }
      end

      def cmd_grab_web(*_args)
        results = []

        # Grab all services matching 'http' type
        http_services = framework.db.services.where(state: 'open').select { |s| !s.name.nil? and s.name.include? 'http' }
        http_services.each do |h|
          host = framework.db.hosts(id: h.host_id)[0]
          ip = host.address
          results << "#{ip}:#{h.port}"
        end

        # Grab all hosts on port 80 and 443
        web_services = framework.db.services.where(state: 'open').select { |s| s.port == 80 || s.port == 443 }
        web_services.each do |w|
          host = framework.db.hosts(id: w.host_id)[0]
          ip = host.address
          results << "#{ip}:#{w.port}"
        end

        results.uniq!

        results.each do |r|
          print "#{r}\n"
        end
      end

      def cmd_grab_all(*args)
        results = []

        opts = Rex::Parser::Arguments.new(
          '-f' => [false, 'Optionally write host:port combos to file'],
          '-h' => [false, 'Display help']
        )

        file = nil
        help = false

        opts.parse(args) do |opt, idx, _val|
          case opt
          when '-h'
            help = true
          when '-f'
            file = args[idx + 1]
          end
        end

        if help
          print_line('Usage: grab_all [-f combos.txt]')
          return
        end

        # Grab all services matching 'http' type
        services = framework.db.services.where(state: 'open')
        services.each do |s|
          host = framework.db.hosts(id: s.host_id)[0]
          ip = host.address
          results << "#{ip}:#{s.port}"
        end

        results.uniq!

        if file.nil?
          results.each do |r|
            print "#{r}\n"
          end
        else
          fp = File.open(file, 'w')
          results.each do |r|
            fp.write("#{r}\n")
          end
          fp.close
        end
      end

      def cmd_grab_host_port(*args)
        opts = Rex::Parser::Arguments.new(
          '-S' => [false, 'Search for a service string'],
          '-p' => [false, 'Inlude specific ports in results (Ex: 80,443-445)']
        )

        query = nil
        ports = nil

        opts.parse(args) do |opt, idx, _val|
          case opt
          when '-S'
            query = args[idx + 1]
          when '-p'
            ports = args[idx + 1]
          end
        end

        if query.nil? && ports.nil?
          print_line('Usage: grab_host_port [-S http] [-p 80,443]')
          return
        end

        results = []

        unless query.nil?
          http_services = framework.db.services.where(state: 'open').select { |s| !s.name.nil? and s.name.include? query }
          http_services.each do |h|
            host = framework.db.hosts(id: h.host_id)[0]
            ip = host.address
            results << "#{ip}:#{h.port}"
          end
        end

        unless ports.nil?
          port_list = Rex::Socket.portspec_crack(ports)
          port_services = framework.db.services.where(state: 'open').where.not(name: nil).select { |s| port_list.include? s.port }
          port_services.each do |p|
            host = framework.db.hosts(id: p.host_id)[0]
            ip = host.address
            results << "#{ip}:#{p.port}"
          end
        end

        results.uniq!

        results.each do |r|
          print "#{r}\n"
        end
      end

      def cmd_list_services(*_args)
        services = framework.db.services.where(state: 'open').where.not(name: nil).pluck(:name).map { |s| s.sub('ssl/', '') }

        service_dictionary = {}

        services.each_with_index do |s, _i|
          service_dictionary[s] = services.count(s)
        end

        service_dictionary = service_dictionary.sort_by { |_k, v| v }.reverse.to_h

        service_dictionary.each do |k, _v|
          print "#{k}\n"
        end
      end
    end

    def name
      'Network Pentest Toolset'
    end

    def desc
      'Toolset to assist with basic network pentest tools by leveraging the MSF DB'
    end

    def initialize(framework, opts)
      super
      add_console_dispatcher(NetPenDispatcher)
    end

    def cleanup
      remove_console_dispatcher('Network Pentest Toolset')
    end
  end
end

References

  • https://github.com/wdahlenburg/MSF-Plugins/blob/main/netpen.rb