#!/usr/bin/env ruby # # A simple utility for watching a given directory for temporary files, # directories, etc. Primarily used to casually discover applications that # are vulnerable to race conditions or symlink foo. # # The irony? There is no mktemp that creates temporary directories, so # here in code that is designed to point out temporary directory/file # issues, I have to reinvent the wheel. # # Temporary files -- yer doing it wrong. # # Jon Hart require 'find' require 'ftools' require 'optparse' require 'ostruct' # generic options parser class Options def self.parse(name, args) options = OpenStruct.new options.verbose = false options.copy = nil options.exclude = [] options.sleep = 1 opts = OptionParser.new do |opts| opts.banner = "#{File.basename(name)} ( http://spoofed.org )" opts.banner += "\nUsage: #{name} [options] /dir/to/watch1 /watch/me/too ..." opts.on("-c", "--copy DIR") do |o| options.copy = o end opts.on("-e", "--exclude PATTERN") do |o| options.exclude << o end opts.on_tail("-h", "--help", "Show help") do puts opts exit end opts.on("-s", "--sleep SECS", "Sleep between checks") do |o| options.sleep = o.to_i end opts.on("-v", "--verbose", "Be verbose") do |o| options.verbose = o end end begin opts.parse!(args) rescue OptionParser::ParseError => e puts "#{e}\n\n#{opts}" exit(1) end options end end # given the path to a file, directory, etc, return a CSV of its info def getinfo(path) require 'etc.so' stat = File.lstat(path) info = [] info << "#{stat.size} bytes" # Thanks File.stat.pretty_print # (http://ruby-doc.org/core/classes/File.lstat.src/M002656.html) info << sprintf("%c%c%c%c%c%c%c%c%c", (stat.mode & 0400 == 0 ? ?- : ?r), (stat.mode & 0200 == 0 ? ?- : ?w), (stat.mode & 0100 == 0 ? (stat.mode & 04000 == 0 ? ?- : ?S) : (stat.mode & 04000 == 0 ? ?x : ?s)), (stat.mode & 0040 == 0 ? ?- : ?r), (stat.mode & 0020 == 0 ? ?- : ?w), (stat.mode & 0010 == 0 ? (stat.mode & 02000 == 0 ? ?- : ?S) : (stat.mode & 02000 == 0 ? ?x : ?s)), (stat.mode & 0004 == 0 ? ?- : ?r), (stat.mode & 0002 == 0 ? ?- : ?w), (stat.mode & 0001 == 0 ? (stat.mode & 01000 == 0 ? ?- : ?T) : (stat.mode & 01000 == 0 ? ?x : ?t))) begin u = Etc.getpwuid(stat.uid) g = Etc.getgrgid(stat.gid) rescue ArgumentError end info << ("uid=#{stat.uid}" + (u ? "(#{u.name})" : nil)) info << ("gid=#{stat.gid}" + (g ? "(#{g.name})" : nil)) info.join(", ") end # recursive ls, only returning files that we have not # squirreled away for safe keeping. def rls(dir) entries = {} Find.find(dir) do |path| next unless @options.exclude.select { |e| path =~ /#{e}/ }.empty? if @options.copy if !path.include?(@options.copy) entries[path] = 1 end else entries[path] = 1 end end entries end @options = Options.parse($0, ARGV) dirs = (ARGV.empty? ? %w(/tmp) : ARGV) threads = [] contents = {} dirs.each do |dir| threads << Thread.new(dir) { |d| # do initial population puts "Populating state of #{d}"if @options.verbose rls(d).each_key do |path| contents[path] = 1 end puts "Watching #{d} for possible temporary files" if @options.verbose while true rls(d).each_key do |path| if !contents[path] stat = File.lstat(path) if @options.copy && (stat.file? || stat.symlink?) if (!File.exist?(@options.copy)) Dir.mkdir(@options.copy) end File.makedirs("#{@options.copy}/#{File.dirname(path)}") begin File.copy(path, "#{@options.copy}#{path}") puts "New #{stat.ftype}, #{path}: #{getinfo(path)} (saved to #{@options.copy})" rescue Errno::EACCES => e puts "New #{stat.ftype}, #{path}: #{getinfo(path)} (Could not save -- #{e})" end else puts "New #{stat.ftype}, #{path}: #{getinfo(path)}" end contents[path] = 1 end end contents.each_key do |thing| next if File.symlink?(thing) if !File.exist?(thing) puts "#{thing} is gone" contents.delete(thing) end end sleep(@options.sleep) end } end threads.each { |t| t.join }