##
# $Id: psnuffle.rb 9929 2010-07-25 21:37:54Z jduck $
##

##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# Framework web site for more information on licensing and terms of use.
# http://metasploit.com/framework/
#
##

##
# dsniff was helping me very often. Too bad that it doesn't work correctly
# anymore. Psnuffle should bring password sniffing into Metasploit local
# and if we get lucky even remote.
#
# Cheers - Max Moser - mmo@remote-exploit.org
##


require 'msf/core'


class Metasploit3 < Msf::Auxiliary

	include Msf::Auxiliary::Report
	include Msf::Exploit::Capture

	def initialize
		super(
			'Name'				=> 'pSnuffle Packet Sniffer',
			'Version'           => '$Revision: 9929 $',
			'Description'       => 'This module sniffs passwords like dsniff did in the past',
			'Author'			=> 'Max Moser  <mmo@remote-exploit.org>',
			'License'			=> MSF_LICENSE,
			'Actions'			=>
				[
					[ 'Sniffer' ],
					[ 'List'    ]
				],
			'PassiveActions' =>
				[
					'Sniffer'
				],
			'DefaultAction'	 => 'Sniffer'
		)

		register_options([
			OptString.new('PROTOCOLS',	[true,	'A comma-delimited list of protocols to sniff or "all".', "all"]),
		], self.class)

		register_advanced_options([
			OptPath.new('ProtocolBase', [true,	'The base directory containing the protocol decoders',
				File.join(Msf::Config.install_root, "data", "exploits", "psnuffle")
			]),
		], self.class)
	end


	def load_protocols
		base = datastore['ProtocolBase']
		if (not File.directory?(base))
			raise RuntimeError,"The ProtocolBase parameter is set to an invalid directory"
		end

		@protos = {}
		decoders = Dir.new(base).entries.grep(/\.rb$/).sort
		decoders.each do |n|
			f = File.join(base, n)
			m = ::Module.new
			begin
				m.module_eval(File.read(f, File.size(f)))
				m.constants.grep(/^Sniffer(.*)/) do
					proto = $1
					klass = m.const_get("Sniffer#{proto}")
					@protos[proto.downcase] = klass.new(framework, self)

					print_status("Loaded protocol #{proto} from #{f}...")
				end
			rescue ::Exception => e
				print_error("Decoder #{n} failed to load: #{e.class} #{e} #{e.backtrace}")
			end
		end
	end

	def run
		# Load all of our existing protocols
		load_protocols

		if(action.name == 'List')
			print_status("Protocols: #{@protos.keys.sort.join(', ')}")
			return
		end

		# Remove protocols not explicitly allowed
		if(datastore['PROTOCOLS'] != 'all')
			allowed = datastore['PROTOCOLS'].split(',').map{|x| x.strip.downcase}
			newlist = {}
			@protos.each_key { |k| newlist[k] = @protos[k] if allowed.include?(k) }
			@protos = newlist
		end

		print_status("Sniffing traffic.....")
		open_pcap

		each_packet do |pkt|
			eth = Racket::L2::Ethernet.new(pkt)
			next if not eth.ethertype == 0x0800

			ip = Racket::L3::IPv4.new(eth.payload)
			next if not ip.protocol == 6

			tcp = Racket::L4::TCP.new(ip.payload)
			next if !(tcp.payload and tcp.payload.length > 0)

			data = {:raw => pkt, :eth => eth, :ip => ip, :tcp => tcp}

			@protos.each_key do |k|
				@protos[k].parse(data)
			end
			true
		end
		close_pcap
		print_status("Finished sniffing")
	end
end

# End module class

# Basic class for taking care of sessions
class BaseProtocolParser

	attr_accessor :framework, :module, :sessions, :dport, :sigs

	def initialize(framework, mod)
		self.framework = framework
		self.module    = mod
		self.sessions  = {}
		self.dport     = 0
		register_sigs()
	end

	def parse(pkt)
		nil
	end

	def register_sigs
		self.sigs = {}
	end

	#
	# Glue methods to bridge parsers to the main module class
	#
	def print_status(msg)
		self.module.print_status(msg)
	end

	def print_error(msg)
		self.module.print_error(msg)
	end

	def report_auth_info(*s)
		self.module.report_auth_info(*s)
	end

	def report_service(*s)
		self.module.report_service(*s)
	end

	def find_session(sessionid)
		sessions.each_key do |ses|
			# Check for cleanup abilities... kills performance in large environments maybe
			if ((sessions[ses][:mtime]-sessions[ses][:ctime])>300)		#When longer than 5 minutes no packet was related to the session, delete it
				# too bad to this session has no action for a long time
				sessions.delete(ses)
			end
		end

		# Does this session already exist?
		if (sessions[sessionid])
			# Refresh the timestamp
			sessions[sessionid][:mtime] = Time.now
		else
			# Create a new session entry along with the host/port from the id
			if (sessionid =~ /^([^:]+):([^-]+)-/s)
				sessions[sessionid] = {
					:host      => $1,
					:target_host => $1,
					:port      => $2,
					:target_port => $2,
					:session   => sessionid,
					:ctime     => Time.now,
					:mtime     => Time.now
				}
			end
		end

		return sessions[sessionid]
	end

	def get_session_src(pkt)
		return "#{pkt[:ip].dst_ip}:#{pkt[:tcp].dst_port}-#{pkt[:ip].src_ip}:#{pkt[:tcp].src_port}" if pkt[:tcp]
		return "#{pkt[:ip].dst_ip}:#{pkt[:udp].dst_port}-#{pkt[:ip].src_ip}:#{pkt[:udp].src_port}" if pkt[:udp]
		return "#{pkt[:ip].dst_ip}:0-#{pkt[:ip].src_ip}:0"
	end

	def get_session_dst(pkt)
		return "#{pkt[:ip].src_ip}:#{pkt[:tcp].src_port}-#{pkt[:ip].dst_ip}:#{pkt[:tcp].dst_port}" if pkt[:tcp]
		return "#{pkt[:ip].src_ip}:#{pkt[:udp].src_port}-#{pkt[:ip].dst_ip}:#{pkt[:udp].dst_port}" if pkt[:udp]
		return "#{pkt[:ip].src_ip}:0-#{pkt[:ip].dst_ip}:0"
	end

end

