bettercap 1.1.7 → 1.1.8
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/.gitignore +3 -1
- data/README.md +18 -14
- data/TODO.md +11 -4
- data/bettercap.gemspec +1 -0
- data/bin/bettercap +75 -112
- data/lib/bettercap.rb +1 -0
- data/lib/bettercap/context.rb +48 -114
- data/lib/bettercap/discovery/arp.rb +32 -22
- data/lib/bettercap/network.rb +3 -3
- data/lib/bettercap/options.rb +189 -0
- data/lib/bettercap/proxy/proxy.rb +9 -8
- data/lib/bettercap/proxy/request.rb +1 -1
- data/lib/bettercap/proxy/response.rb +10 -2
- data/lib/bettercap/proxy/stream_logger.rb +11 -6
- data/lib/bettercap/proxy/streamer.rb +67 -15
- data/lib/bettercap/proxy/thread_pool.rb +0 -64
- data/lib/bettercap/sniffer/parsers/base.rb +7 -1
- data/lib/bettercap/sniffer/parsers/httpauth.rb +4 -5
- data/lib/bettercap/sniffer/parsers/https.rb +1 -1
- data/lib/bettercap/sniffer/parsers/ntlmss.rb +1 -1
- data/lib/bettercap/sniffer/parsers/post.rb +1 -1
- data/lib/bettercap/sniffer/parsers/url.rb +1 -1
- data/lib/bettercap/sniffer/sniffer.rb +10 -10
- data/lib/bettercap/spoofers/arp.rb +12 -17
- data/lib/bettercap/target.rb +47 -5
- data/lib/bettercap/version.rb +1 -1
- metadata +17 -2
@@ -19,46 +19,56 @@ require 'bettercap/context'
|
|
19
19
|
class ArpAgent < BaseAgent
|
20
20
|
|
21
21
|
def self.parse( ctx )
|
22
|
-
arp = Shell.arp
|
23
22
|
targets = []
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
if
|
32
|
-
|
23
|
+
self.parse_cache do |ip,mac|
|
24
|
+
if ip != ctx.gateway and ip != ctx.ifconfig[:ip_saddr]
|
25
|
+
if ctx.options.ignore_ip?(ip)
|
26
|
+
Logger.debug "Ignoring #{ip} ..."
|
27
|
+
else
|
28
|
+
# reuse Target object if it's already a known address
|
29
|
+
known = ctx.find_target ip, mac
|
30
|
+
if known.nil?
|
31
|
+
targets << Target.new( ip, mac )
|
33
32
|
else
|
34
|
-
|
35
|
-
targets << target
|
36
|
-
Logger.debug "FOUND #{target}"
|
33
|
+
targets << known
|
37
34
|
end
|
38
35
|
end
|
39
36
|
end
|
40
37
|
end
|
41
|
-
|
42
38
|
targets
|
43
39
|
end
|
44
40
|
|
45
|
-
def self.find_address(
|
41
|
+
def self.find_address( address )
|
42
|
+
self.parse_cache do |ip,mac|
|
43
|
+
if ip == address
|
44
|
+
return mac
|
45
|
+
end
|
46
|
+
end
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def self.parse_cache
|
46
53
|
arp = Shell.arp
|
47
|
-
|
54
|
+
|
55
|
+
Logger.debug "ARP CACHE:\n#{arp}"
|
48
56
|
|
49
57
|
arp.split("\n").each do |line|
|
50
|
-
m =
|
58
|
+
m = self.parse_cache_line(line)
|
51
59
|
if !m.nil?
|
52
|
-
|
53
|
-
|
60
|
+
ip = m[1]
|
61
|
+
hw = m[2]
|
62
|
+
if hw != 'ff:ff:ff:ff:ff:ff'
|
63
|
+
yield( ip, hw )
|
54
64
|
end
|
55
65
|
end
|
56
66
|
end
|
57
|
-
|
58
|
-
mac
|
59
67
|
end
|
60
68
|
|
61
|
-
|
69
|
+
def self.parse_cache_line( line )
|
70
|
+
/[^\s]+\s+\(([0-9\.]+)\)\s+at\s+([a-f0-9:]+).+#{Context.get.ifconfig[:iface]}.*/i.match(line)
|
71
|
+
end
|
62
72
|
|
63
73
|
def send_probe( ip )
|
64
74
|
pkt = PacketFu::ARPPacket.new
|
data/lib/bettercap/network.rb
CHANGED
@@ -37,7 +37,7 @@ class Network
|
|
37
37
|
out = nstat.split(/\n/).select {|n| n =~ /UG/ }
|
38
38
|
gw = nil
|
39
39
|
out.each do |line|
|
40
|
-
if line.include?( Context.get.options
|
40
|
+
if line.include?( Context.get.options.iface )
|
41
41
|
tmp = line.split[1]
|
42
42
|
if is_ip?(tmp)
|
43
43
|
gw = tmp
|
@@ -46,7 +46,7 @@ class Network
|
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
|
-
raise BetterCap::Error, "Could not detect the gateway address for interface #{Context.get.options
|
49
|
+
raise BetterCap::Error, "Could not detect the gateway address for interface #{Context.get.options.iface}, "\
|
50
50
|
'make sure you\'ve specified the correct network interface to use and to have the '\
|
51
51
|
'correct network configuration, this could also happen if bettercap '\
|
52
52
|
'is launched from a virtual environment.' unless !gw.nil? and is_ip?(gw)
|
@@ -66,7 +66,7 @@ class Network
|
|
66
66
|
end
|
67
67
|
|
68
68
|
def get_alive_targets( ctx, timeout = 5 )
|
69
|
-
if ctx.options
|
69
|
+
if ctx.options.should_discover_hosts?
|
70
70
|
icmp = IcmpAgent.new timeout
|
71
71
|
udp = UdpAgent.new ctx.ifconfig, ctx.gateway, ctx.ifconfig[:ip_saddr]
|
72
72
|
arp = ArpAgent.new ctx.ifconfig, ctx.gateway, ctx.ifconfig[:ip_saddr]
|
@@ -0,0 +1,189 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
BETTERCAP
|
4
|
+
|
5
|
+
Author : Simone 'evilsocket' Margaritelli
|
6
|
+
Email : evilsocket@gmail.com
|
7
|
+
Blog : http://www.evilsocket.net/
|
8
|
+
|
9
|
+
This project is released under the GPL 3 license.
|
10
|
+
|
11
|
+
=end
|
12
|
+
|
13
|
+
class Options
|
14
|
+
attr_accessor :gateway,
|
15
|
+
:iface,
|
16
|
+
:spoofer,
|
17
|
+
:half_duplex,
|
18
|
+
:target,
|
19
|
+
:logfile,
|
20
|
+
:debug,
|
21
|
+
:arpcache,
|
22
|
+
:ignore,
|
23
|
+
:sniffer,
|
24
|
+
:sniffer_pcap,
|
25
|
+
:sniffer_filter,
|
26
|
+
:sniffer_src,
|
27
|
+
:parsers,
|
28
|
+
:local,
|
29
|
+
:proxy,
|
30
|
+
:proxy_https,
|
31
|
+
:proxy_port,
|
32
|
+
:proxy_https_port,
|
33
|
+
:proxy_pem_file,
|
34
|
+
:proxy_module,
|
35
|
+
:custom_proxy,
|
36
|
+
:custom_proxy_port,
|
37
|
+
:custom_https_proxy,
|
38
|
+
:custom_https_proxy_port,
|
39
|
+
:httpd,
|
40
|
+
:httpd_port,
|
41
|
+
:httpd_path,
|
42
|
+
:check_updates
|
43
|
+
|
44
|
+
def initialize( iface )
|
45
|
+
@gateway = nil
|
46
|
+
@iface = iface
|
47
|
+
@spoofer = 'ARP'
|
48
|
+
@half_duplex = false
|
49
|
+
@target = nil
|
50
|
+
@logfile = nil
|
51
|
+
@debug = false
|
52
|
+
@arpcache = false
|
53
|
+
|
54
|
+
@ignore = nil
|
55
|
+
|
56
|
+
@sniffer = false
|
57
|
+
@sniffer_pcap = nil
|
58
|
+
@sniffer_filter = nil
|
59
|
+
@sniffer_src = nil
|
60
|
+
@parsers = ['*']
|
61
|
+
@local = false
|
62
|
+
|
63
|
+
@proxy = false
|
64
|
+
@proxy_https = false
|
65
|
+
@proxy_port = 8080
|
66
|
+
@proxy_https_port = 8083
|
67
|
+
@proxy_pem_file = nil
|
68
|
+
@proxy_module = nil
|
69
|
+
|
70
|
+
@custom_proxy = nil
|
71
|
+
@custom_proxy_port = 8080
|
72
|
+
|
73
|
+
@custom_https_proxy = nil
|
74
|
+
@custom_https_proxy_port = 8083
|
75
|
+
|
76
|
+
@httpd = false
|
77
|
+
@httpd_port = 8081
|
78
|
+
@httpd_path = './'
|
79
|
+
|
80
|
+
@check_updates = false
|
81
|
+
end
|
82
|
+
|
83
|
+
def should_discover_hosts?
|
84
|
+
!@arpcache
|
85
|
+
end
|
86
|
+
|
87
|
+
def has_proxy_module?
|
88
|
+
!@proxy_module.nil?
|
89
|
+
end
|
90
|
+
|
91
|
+
def has_spoofer?
|
92
|
+
@spoofer == 'NONE' or @spoofer == 'none'
|
93
|
+
end
|
94
|
+
|
95
|
+
def has_http_sniffer_enabled?
|
96
|
+
@sniffer and ( @parsers.include?'*' or @parsers.include?'URL' )
|
97
|
+
end
|
98
|
+
|
99
|
+
def ignore_ip?(ip)
|
100
|
+
!@ignore.nil? and @ignore.include?(ip)
|
101
|
+
end
|
102
|
+
|
103
|
+
def ignore=(value)
|
104
|
+
@ignore = value.split(",")
|
105
|
+
valid = @ignore.select { |target| Network.is_ip?(target) }
|
106
|
+
|
107
|
+
raise BetterCap::Error, "Invalid ignore addresses specified." if valid.empty?
|
108
|
+
|
109
|
+
invalid = @ignore - valid
|
110
|
+
invalid.each do |target|
|
111
|
+
Logger.warn "Not a valid address: #{target}"
|
112
|
+
end
|
113
|
+
|
114
|
+
@ignore = valid
|
115
|
+
|
116
|
+
Logger.warn "Ignoring #{valid.join(", ")} ."
|
117
|
+
end
|
118
|
+
|
119
|
+
def custom_proxy=(value)
|
120
|
+
@custom_proxy = value
|
121
|
+
raise BetterCap::Error, 'Invalid custom HTTP upstream proxy address specified.' unless Network.is_ip? @custom_proxy
|
122
|
+
end
|
123
|
+
|
124
|
+
def custom_https_proxy=(value)
|
125
|
+
@custom_https_proxy = value
|
126
|
+
raise BetterCap::Error, 'Invalid custom HTTPS upstream proxy address specified.' unless Network.is_ip? @custom_https_proxy
|
127
|
+
end
|
128
|
+
|
129
|
+
def to_targets
|
130
|
+
targets = @target.split(",")
|
131
|
+
valid_targets = targets.select { |target| Network.is_ip?(target) }
|
132
|
+
|
133
|
+
raise BetterCap::Error, "Invalid target" if valid_targets.empty?
|
134
|
+
|
135
|
+
invalid_targets = targets - valid_targets
|
136
|
+
invalid_targets.each do |target|
|
137
|
+
Logger.warn "Invalid target #{target}"
|
138
|
+
end
|
139
|
+
|
140
|
+
valid_targets.map { |target| Target.new(target) }
|
141
|
+
end
|
142
|
+
|
143
|
+
def to_spoofers
|
144
|
+
spoofers = []
|
145
|
+
spoofer_modules_names = @spoofer.split(",")
|
146
|
+
spoofer_modules_names.each do |module_name|
|
147
|
+
spoofers << SpooferFactory.get_by_name( module_name )
|
148
|
+
end
|
149
|
+
spoofers
|
150
|
+
end
|
151
|
+
|
152
|
+
def to_redirections ifconfig
|
153
|
+
redirections = []
|
154
|
+
|
155
|
+
if @proxy
|
156
|
+
redirections << Redirection.new( @iface,
|
157
|
+
'TCP',
|
158
|
+
80,
|
159
|
+
ifconfig[:ip_saddr],
|
160
|
+
@proxy_port )
|
161
|
+
end
|
162
|
+
|
163
|
+
if @proxy_https
|
164
|
+
redirections << Redirection.new( @iface,
|
165
|
+
'TCP',
|
166
|
+
443,
|
167
|
+
ifconfig[:ip_saddr],
|
168
|
+
@proxy_https_port )
|
169
|
+
end
|
170
|
+
|
171
|
+
if @custom_proxy
|
172
|
+
redirections << Redirection.new( @iface,
|
173
|
+
'TCP',
|
174
|
+
80,
|
175
|
+
@custom_proxy,
|
176
|
+
@custom_proxy_port )
|
177
|
+
end
|
178
|
+
|
179
|
+
if @custom_https_proxy
|
180
|
+
redirections << Redirection.new( @iface,
|
181
|
+
'TCP',
|
182
|
+
443,
|
183
|
+
@custom_https_proxy,
|
184
|
+
@custom_https_proxy_port )
|
185
|
+
end
|
186
|
+
|
187
|
+
redirections
|
188
|
+
end
|
189
|
+
end
|
@@ -32,7 +32,7 @@ class Proxy
|
|
32
32
|
@running = false
|
33
33
|
@streamer = Streamer.new processor
|
34
34
|
@local_ips = []
|
35
|
-
|
35
|
+
|
36
36
|
begin
|
37
37
|
@local_ips = Socket.ip_address_list.collect { |x| x.ip_address }
|
38
38
|
rescue
|
@@ -93,7 +93,8 @@ class Proxy
|
|
93
93
|
begin
|
94
94
|
@pool << @server.accept
|
95
95
|
rescue Exception => e
|
96
|
-
Logger.warn
|
96
|
+
Logger.warn("Error while accepting #{@type} connection: #{e.inspect}") \
|
97
|
+
unless !@running
|
97
98
|
end
|
98
99
|
end
|
99
100
|
|
@@ -106,7 +107,7 @@ class Proxy
|
|
106
107
|
|
107
108
|
def create_upstream_connection( request )
|
108
109
|
sock = TCPSocket.new( request.host, request.port )
|
109
|
-
|
110
|
+
|
110
111
|
if @is_https
|
111
112
|
ctx = OpenSSL::SSL::SSLContext.new
|
112
113
|
# do we need this? :P ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
|
@@ -196,13 +197,13 @@ class Proxy
|
|
196
197
|
|
197
198
|
rescue Exception => e
|
198
199
|
if request.host
|
199
|
-
Logger.
|
200
|
-
Logger.debug e.backtrace
|
200
|
+
Logger.warn "Error while serving #{request.host}#{request.url}: #{e.inspect}"
|
201
|
+
Logger.debug e.backtrace.join("\n")
|
201
202
|
end
|
202
|
-
ensure
|
203
|
-
client.close
|
204
|
-
server.close unless server.nil?
|
205
203
|
end
|
204
|
+
|
205
|
+
client.close
|
206
|
+
server.close unless server.nil?
|
206
207
|
end
|
207
208
|
end
|
208
209
|
|
@@ -13,7 +13,7 @@ This project is released under the GPL 3 license.
|
|
13
13
|
module Proxy
|
14
14
|
|
15
15
|
class Response
|
16
|
-
attr_reader :content_type, :charset, :content_length, :headers, :code, :headers_done
|
16
|
+
attr_reader :content_type, :charset, :content_length, :chunked, :headers, :code, :headers_done
|
17
17
|
attr_accessor :body
|
18
18
|
|
19
19
|
def initialize
|
@@ -24,6 +24,7 @@ class Response
|
|
24
24
|
@code = nil
|
25
25
|
@headers = []
|
26
26
|
@headers_done = false
|
27
|
+
@chunked = false
|
27
28
|
end
|
28
29
|
|
29
30
|
def self.from_socket(sock)
|
@@ -46,6 +47,8 @@ class Response
|
|
46
47
|
if @headers_done
|
47
48
|
@body << line.force_encoding( @charset )
|
48
49
|
else
|
50
|
+
Logger.debug " RESPONSE LINE: '#{line.chomp}'"
|
51
|
+
|
49
52
|
# parse the response status
|
50
53
|
if @code.nil? and line =~ /^HTTP\/[\d\.]+\s+(.+)/
|
51
54
|
@code = $1.chomp
|
@@ -61,13 +64,18 @@ class Response
|
|
61
64
|
elsif line =~ /^Content-Length:\s+(\d+)\s*$/i
|
62
65
|
@content_length = $1.to_i
|
63
66
|
|
67
|
+
# check if we have a chunked encoding
|
68
|
+
elsif line =~ /^Transfer-Encoding:\s*chunked.*$/i
|
69
|
+
@chunked = true
|
70
|
+
line = nil
|
71
|
+
|
64
72
|
# last line, we're done with the headers
|
65
73
|
elsif line.chomp.empty?
|
66
74
|
@headers_done = true
|
67
75
|
|
68
76
|
end
|
69
77
|
|
70
|
-
@headers << line.chomp
|
78
|
+
@headers << line.chomp unless line.nil?
|
71
79
|
end
|
72
80
|
end
|
73
81
|
|
@@ -14,14 +14,20 @@ require 'bettercap/logger'
|
|
14
14
|
module Proxy
|
15
15
|
class StreamLogger
|
16
16
|
@@MAX_REQ_SIZE = 50
|
17
|
-
|
17
|
+
|
18
18
|
@@CODE_COLORS = {
|
19
19
|
'2' => :green,
|
20
20
|
'3' => :light_black,
|
21
21
|
'4' => :yellow,
|
22
22
|
'5' => :red
|
23
23
|
}
|
24
|
-
|
24
|
+
|
25
|
+
def self.addr2s( addr )
|
26
|
+
target = Context.get.find_target addr, nil
|
27
|
+
return target.to_s_compact unless target.nil?
|
28
|
+
addr
|
29
|
+
end
|
30
|
+
|
25
31
|
def self.log( is_https, client, request, response )
|
26
32
|
request_s = "#{is_https ? 'https' : 'http'}://#{request.host}#{request.url}"
|
27
33
|
response_s = "( #{response.content_type} )"
|
@@ -29,13 +35,12 @@ class StreamLogger
|
|
29
35
|
code = response.code[0]
|
30
36
|
|
31
37
|
if @@CODE_COLORS.has_key? code
|
32
|
-
response_s += " [#{response.code}]".send( @@CODE_COLORS[ code ] )
|
38
|
+
response_s += " [#{response.code}]".send( @@CODE_COLORS[ code ] )
|
33
39
|
else
|
34
|
-
response_s += " [#{response.code}]"
|
40
|
+
response_s += " [#{response.code}]"
|
35
41
|
end
|
36
42
|
|
37
|
-
Logger.write "[#{client}] #{request.verb.light_blue} #{request_s} #{response_s}"
|
43
|
+
Logger.write "[#{self.addr2s(client)}] #{request.verb.light_blue} #{request_s} #{response_s}"
|
38
44
|
end
|
39
45
|
end
|
40
46
|
end
|
41
|
-
|
@@ -14,7 +14,7 @@ require 'bettercap/logger'
|
|
14
14
|
module Proxy
|
15
15
|
class Streamer
|
16
16
|
def initialize( processor )
|
17
|
-
@processor = processor
|
17
|
+
@processor = processor
|
18
18
|
end
|
19
19
|
|
20
20
|
def rickroll( client )
|
@@ -25,12 +25,39 @@ class Streamer
|
|
25
25
|
def html( request, response, from, to )
|
26
26
|
buff = ''
|
27
27
|
|
28
|
-
if response.
|
28
|
+
if response.chunked
|
29
|
+
Logger.debug "Reading response body using chunked encoding ..."
|
30
|
+
|
31
|
+
begin
|
32
|
+
len = nil
|
33
|
+
total = 0
|
34
|
+
|
35
|
+
while true
|
36
|
+
line = from.readline
|
37
|
+
hexlen = line.slice(/[0-9a-fA-F]+/) or raise "Wrong chunk size line: #{line}"
|
38
|
+
len = hexlen.hex
|
39
|
+
break if len == 0
|
40
|
+
begin
|
41
|
+
Logger.debug "Reading chunk of size #{len} ..."
|
42
|
+
tmp = read( from, len )
|
43
|
+
Logger.debug "Read #{tmp.bytesize}/#{len} chunk."
|
44
|
+
response << tmp
|
45
|
+
ensure
|
46
|
+
total += len
|
47
|
+
from.read 2
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
rescue Exception => e
|
52
|
+
Logger.debug e
|
53
|
+
end
|
54
|
+
|
55
|
+
elsif response.content_length.nil?
|
29
56
|
Logger.debug "Reading response body using 1024 bytes chunks ..."
|
30
57
|
|
31
58
|
loop do
|
32
59
|
buff = read( from, 1024 )
|
33
|
-
|
60
|
+
|
34
61
|
break unless buff.size > 0
|
35
62
|
|
36
63
|
response << buff
|
@@ -41,12 +68,12 @@ class Streamer
|
|
41
68
|
buff = read( from, response.content_length )
|
42
69
|
|
43
70
|
Logger.debug "Read #{buff.size} / #{response.content_length} bytes."
|
44
|
-
|
71
|
+
|
45
72
|
response << buff
|
46
73
|
end
|
47
74
|
|
48
75
|
@processor.call( request, response )
|
49
|
-
|
76
|
+
|
50
77
|
# Response::to_s will patch the headers if needed
|
51
78
|
to.write response.to_s
|
52
79
|
end
|
@@ -97,23 +124,48 @@ class Streamer
|
|
97
124
|
end
|
98
125
|
end
|
99
126
|
|
100
|
-
private
|
127
|
+
private
|
101
128
|
|
102
|
-
|
103
|
-
buffer = ''
|
129
|
+
BUFSIZE = 1024 * 16
|
104
130
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
131
|
+
def consume_stream io, size
|
132
|
+
read_timeout = 60.0
|
133
|
+
dest = ''
|
134
|
+
begin
|
135
|
+
dest << io.read_nonblock(size)
|
136
|
+
rescue IO::WaitReadable
|
137
|
+
if IO.select([io], nil, nil, read_timeout)
|
138
|
+
retry
|
139
|
+
else
|
140
|
+
raise Net::ReadTimeout
|
141
|
+
end
|
142
|
+
rescue IO::WaitWritable
|
143
|
+
# OpenSSL::Buffering#read_nonblock may fail with IO::WaitWritable.
|
144
|
+
# http://www.openssl.org/support/faq.html#PROG10
|
145
|
+
if IO.select(nil, [io], nil, read_timeout)
|
146
|
+
retry
|
147
|
+
else
|
148
|
+
raise Net::ReadTimeout
|
110
149
|
end
|
111
150
|
end
|
151
|
+
dest
|
152
|
+
end
|
112
153
|
|
154
|
+
def read( sd, size )
|
155
|
+
buffer = ''
|
156
|
+
begin
|
157
|
+
while size > 0
|
158
|
+
tmp = consume_stream sd, [ BUFSIZE, size ].min
|
159
|
+
unless tmp.nil? or tmp.bytesize == 0
|
160
|
+
buffer << tmp
|
161
|
+
size -= tmp.bytesize
|
162
|
+
end
|
163
|
+
end
|
164
|
+
rescue EOFError
|
165
|
+
;
|
166
|
+
end
|
113
167
|
buffer
|
114
168
|
end
|
115
169
|
|
116
170
|
end
|
117
171
|
end
|
118
|
-
|
119
|
-
|