bettercap 1.1.5 → 1.1.6
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/TODO.md +3 -3
- data/bettercap.gemspec +2 -0
- data/bin/bettercap +28 -5
- data/lib/bettercap.rb +4 -0
- data/lib/bettercap/context.rb +61 -21
- data/lib/bettercap/discovery/base.rb +22 -3
- data/lib/bettercap/firewalls/linux.rb +2 -2
- data/lib/bettercap/firewalls/redirection.rb +23 -0
- data/lib/bettercap/hw-prefixes +21240 -19565
- data/lib/bettercap/proxy/certstore.rb +1 -2
- data/lib/bettercap/proxy/proxy.rb +36 -139
- data/lib/bettercap/proxy/request.rb +10 -7
- data/lib/bettercap/proxy/response.rb +6 -4
- data/lib/bettercap/proxy/stream_logger.rb +41 -0
- data/lib/bettercap/proxy/streamer.rb +119 -0
- data/lib/bettercap/proxy/thread_pool.rb +265 -0
- data/lib/bettercap/version.rb +1 -1
- metadata +7 -3
@@ -29,8 +29,7 @@ module Proxy
|
|
29
29
|
@@frompems[filename]
|
30
30
|
end
|
31
31
|
|
32
|
-
|
33
|
-
def self.get_selfsigned( subject = '/C=BE/O=Test/OU=Test/CN=Test' )
|
32
|
+
def self.get_selfsigned( subject = '/C=US/ST=California/L=Mountain View/O=Google Inc/CN=www.google.com' )
|
34
33
|
if !@@selfsigned.has_key? subject
|
35
34
|
Logger.info "Generating self signed HTTPS certificate for subject '#{subject}' ..."
|
36
35
|
|
@@ -27,11 +27,12 @@ class Proxy
|
|
27
27
|
@type = is_https ? 'HTTPS' : 'HTTP'
|
28
28
|
@sslserver = nil
|
29
29
|
@sslcontext = nil
|
30
|
+
@server = nil
|
30
31
|
@main_thread = nil
|
31
32
|
@running = false
|
32
|
-
@
|
33
|
+
@streamer = Streamer.new processor
|
33
34
|
@local_ips = []
|
34
|
-
|
35
|
+
|
35
36
|
begin
|
36
37
|
@local_ips = Socket.ip_address_list.collect { |x| x.ip_address }
|
37
38
|
rescue
|
@@ -39,11 +40,17 @@ class Proxy
|
|
39
40
|
|
40
41
|
@local_ips = Network.get_local_ips
|
41
42
|
end
|
43
|
+
|
44
|
+
BasicSocket.do_not_reverse_lookup = true
|
45
|
+
|
46
|
+
@pool = ThreadPool.new( 4, 16 ) do |client|
|
47
|
+
client_worker client
|
48
|
+
end
|
42
49
|
end
|
43
50
|
|
44
51
|
def start
|
45
52
|
begin
|
46
|
-
@socket = TCPServer.new( @address, @port )
|
53
|
+
@server = @socket = TCPServer.new( @address, @port )
|
47
54
|
|
48
55
|
if @is_https
|
49
56
|
cert = Context.get.certificate
|
@@ -52,7 +59,7 @@ class Proxy
|
|
52
59
|
@sslcontext.cert = cert[:cert]
|
53
60
|
@sslcontext.key = cert[:key]
|
54
61
|
|
55
|
-
@sslserver = OpenSSL::SSL::SSLServer.new( @socket, @sslcontext )
|
62
|
+
@server = @sslserver = OpenSSL::SSL::SSLServer.new( @socket, @sslcontext )
|
56
63
|
end
|
57
64
|
|
58
65
|
@main_thread = Thread.new &method(:server_thread)
|
@@ -69,6 +76,7 @@ class Proxy
|
|
69
76
|
if @socket and @running
|
70
77
|
@running = false
|
71
78
|
@socket.close
|
79
|
+
@pool.shutdown
|
72
80
|
end
|
73
81
|
rescue
|
74
82
|
end
|
@@ -76,155 +84,40 @@ class Proxy
|
|
76
84
|
|
77
85
|
private
|
78
86
|
|
79
|
-
def async_accept
|
80
|
-
if @is_https
|
81
|
-
begin
|
82
|
-
Thread.new @sslserver.accept, &method(:client_thread)
|
83
|
-
rescue Exception => e
|
84
|
-
Logger.warn "Error while accepting #{@type} connection: #{e.inspect}"
|
85
|
-
end
|
86
|
-
else
|
87
|
-
Thread.new @socket.accept, &method(:client_thread)
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
87
|
def server_thread
|
92
88
|
Logger.info "#{@type} Proxy started on #{@address}:#{@port} ...\n"
|
93
89
|
|
94
90
|
@running = true
|
95
91
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
if @running
|
102
|
-
Logger.error "Error while accepting #{@type} connection: #{e.inspect}"
|
103
|
-
end
|
104
|
-
ensure
|
105
|
-
@socket.close unless @socket.nil?
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
def binary_streaming( from, to, opts = {} )
|
110
|
-
|
111
|
-
total_size = 0
|
112
|
-
|
113
|
-
# if response|request object is available and a content length as well
|
114
|
-
# use it to speed up data streaming with precise data size
|
115
|
-
if not opts[:response].nil?
|
116
|
-
to.write opts[:response].to_s
|
117
|
-
|
118
|
-
total_size = opts[:response].content_length unless opts[:response].content_length.nil?
|
119
|
-
elsif not opts[:request].nil?
|
120
|
-
|
121
|
-
total_size = opts[:request].content_length unless opts[:request].content_length.nil?
|
122
|
-
end
|
123
|
-
|
124
|
-
buff = ''
|
125
|
-
read = 0
|
126
|
-
|
127
|
-
|
128
|
-
if total_size
|
129
|
-
chunk_size = [ 1024, total_size ].min
|
130
|
-
else
|
131
|
-
chunk_size = 1024
|
132
|
-
end
|
133
|
-
|
134
|
-
if chunk_size > 0
|
135
|
-
loop do
|
136
|
-
from.read chunk_size, buff
|
137
|
-
|
138
|
-
# nothing more to read?
|
139
|
-
break unless buff.size > 0
|
140
|
-
|
141
|
-
to.write buff
|
142
|
-
|
143
|
-
read += buff.size
|
144
|
-
|
145
|
-
# collect into the proper object
|
146
|
-
if not opts[:request].nil? and opts[:request].post?
|
147
|
-
opts[:request] << buff
|
148
|
-
end
|
149
|
-
|
150
|
-
# we've done reading?
|
151
|
-
break unless read != total_size
|
152
|
-
end
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
def html_streaming( request, response, from, to )
|
157
|
-
buff = ''
|
158
|
-
|
159
|
-
if response.content_length.nil?
|
160
|
-
loop do
|
161
|
-
from.read 1024, buff
|
162
|
-
|
163
|
-
break unless buff.size > 0
|
164
|
-
|
165
|
-
response << buff
|
92
|
+
while @running do
|
93
|
+
begin
|
94
|
+
@pool << @server.accept
|
95
|
+
rescue Exception => e
|
96
|
+
Logger.warn "Error while accepting #{@type} connection: #{e.inspect}"
|
166
97
|
end
|
167
|
-
else
|
168
|
-
from.read response.content_length, buff
|
169
|
-
response << buff
|
170
|
-
end
|
171
|
-
|
172
|
-
@processor.call( request, response )
|
173
|
-
|
174
|
-
# Response::to_s will patch the headers if needed
|
175
|
-
to.write response.to_s
|
176
|
-
end
|
177
|
-
|
178
|
-
def log_stream( client, request, response )
|
179
|
-
client_s = "[#{client}]"
|
180
|
-
verb_s = request.verb
|
181
|
-
request_s = "#{@is_https ? 'https' : 'http'}://#{request.host}#{request.url}"
|
182
|
-
response_s = "( #{response.content_type} )"
|
183
|
-
request_s = request_s.slice(0..50) + '...' unless request_s.length <= 50
|
184
|
-
|
185
|
-
verb_s = verb_s.light_blue
|
186
|
-
|
187
|
-
if response.code[0] == '2'
|
188
|
-
response_s += " [#{response.code}]".green
|
189
|
-
elsif response.code[0] == '3'
|
190
|
-
response_s += " [#{response.code}]".light_black
|
191
|
-
elsif response.code[0] == '4'
|
192
|
-
response_s += " [#{response.code}]".yellow
|
193
|
-
elsif response.code[0] == '5'
|
194
|
-
response_s += " [#{response.code}]".red
|
195
|
-
else
|
196
|
-
response_s += " [#{response.code}]"
|
197
98
|
end
|
198
99
|
|
199
|
-
|
100
|
+
@socket.close unless @socket.nil?
|
200
101
|
end
|
201
102
|
|
202
103
|
def is_self_request?(request)
|
203
104
|
@local_ips.include? IPSocket.getaddress(request.host)
|
204
105
|
end
|
205
106
|
|
206
|
-
def rickroll_lamer(client)
|
207
|
-
client.write "HTTP/1.1 302 Found\n"
|
208
|
-
client.write "Location: https://www.youtube.com/watch?v=dQw4w9WgXcQ\n\n"
|
209
|
-
end
|
210
|
-
|
211
107
|
def create_upstream_connection( request )
|
108
|
+
sock = TCPSocket.new( request.host, request.port )
|
109
|
+
|
212
110
|
if @is_https
|
213
|
-
sock = TCPSocket.new( request.host, request.port )
|
214
|
-
|
215
111
|
ctx = OpenSSL::SSL::SSLContext.new
|
216
|
-
|
217
112
|
# do we need this? :P ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
|
218
113
|
|
219
|
-
|
220
|
-
|
221
|
-
|
114
|
+
sock = OpenSSL::SSL::SSLSocket.new(sock, ctx).tap do |socket|
|
115
|
+
sock.sync_close = true
|
116
|
+
sock.connect
|
222
117
|
end
|
223
|
-
|
224
|
-
socket
|
225
|
-
else
|
226
|
-
TCPSocket.new( request.host, request.port )
|
227
118
|
end
|
119
|
+
|
120
|
+
sock
|
228
121
|
end
|
229
122
|
|
230
123
|
def get_client_details( client )
|
@@ -237,7 +130,7 @@ class Proxy
|
|
237
130
|
[ client_ip, client_port ]
|
238
131
|
end
|
239
132
|
|
240
|
-
def
|
133
|
+
def client_worker( client )
|
241
134
|
client_ip, client_port = get_client_details client
|
242
135
|
|
243
136
|
Logger.debug "New #{@type} connection from #{client_ip}:#{client_port}"
|
@@ -255,7 +148,7 @@ class Proxy
|
|
255
148
|
|
256
149
|
Logger.warn "#{client_ip} is connecting to us directly."
|
257
150
|
|
258
|
-
|
151
|
+
@streamer.rickroll client
|
259
152
|
|
260
153
|
elsif request.verb == 'CONNECT'
|
261
154
|
|
@@ -267,13 +160,17 @@ class Proxy
|
|
267
160
|
|
268
161
|
server = create_upstream_connection request
|
269
162
|
|
270
|
-
|
163
|
+
sreq = request.to_s
|
164
|
+
|
165
|
+
Logger.debug "Sending request:\n#{sreq}"
|
166
|
+
|
167
|
+
server.write sreq
|
271
168
|
|
272
169
|
# this is probably a POST request, collect incoming data
|
273
170
|
if request.content_length > 0
|
274
171
|
Logger.debug "Getting #{request.content_length} bytes from client"
|
275
172
|
|
276
|
-
|
173
|
+
@streamer.binary client, server, request: request
|
277
174
|
end
|
278
175
|
|
279
176
|
Logger.debug 'Reading response ...'
|
@@ -281,17 +178,17 @@ class Proxy
|
|
281
178
|
response = Response.from_socket server
|
282
179
|
|
283
180
|
if response.textual?
|
284
|
-
|
181
|
+
StreamLogger.log( @is_https, client_ip, request, response )
|
285
182
|
|
286
183
|
Logger.debug 'Detected textual response'
|
287
184
|
|
288
|
-
|
185
|
+
@streamer.html request, response, server, client
|
289
186
|
else
|
290
187
|
Logger.debug "[#{client_ip}] -> #{request.host}#{request.url} [#{response.code}]"
|
291
188
|
|
292
189
|
Logger.debug 'Binary streaming'
|
293
190
|
|
294
|
-
|
191
|
+
@streamer.binary server, client, response: response
|
295
192
|
end
|
296
193
|
|
297
194
|
Logger.debug "#{@type} client served."
|
@@ -70,15 +70,18 @@ class Request
|
|
70
70
|
elsif line =~ /^Content-Length:\s+(\d+)\s*$/i
|
71
71
|
@content_length = $1.to_i
|
72
72
|
|
73
|
-
|
74
|
-
|
75
|
-
|
73
|
+
# we don't want to have hundreds of threads running
|
74
|
+
elsif line =~ /^Connection: keep-alive/i
|
75
|
+
line = 'Connection: close'
|
76
76
|
|
77
|
-
|
78
|
-
|
79
|
-
line = 'Accept-Encoding: identity'
|
77
|
+
elsif line =~ /^Proxy-Connection: (.+)/i
|
78
|
+
line = "Connection: #{$1}"
|
80
79
|
|
81
|
-
|
80
|
+
# disable gzip, chunked, etc encodings
|
81
|
+
elsif line =~ /^Accept-Encoding:.*/i
|
82
|
+
line = 'Accept-Encoding: identity'
|
83
|
+
|
84
|
+
end
|
82
85
|
|
83
86
|
@lines << line
|
84
87
|
end
|
@@ -44,7 +44,7 @@ class Response
|
|
44
44
|
def <<(line)
|
45
45
|
# we already parsed the heders, collect response body
|
46
46
|
if @headers_done
|
47
|
-
@body
|
47
|
+
@body << line.force_encoding( @charset )
|
48
48
|
else
|
49
49
|
# parse the response status
|
50
50
|
if @code.nil? and line =~ /^HTTP\/[\d\.]+\s+(.+)/
|
@@ -67,11 +67,11 @@ class Response
|
|
67
67
|
|
68
68
|
end
|
69
69
|
|
70
|
-
@headers << line.chomp
|
70
|
+
@headers << line.chomp
|
71
71
|
end
|
72
72
|
end
|
73
73
|
|
74
|
-
def textual?
|
74
|
+
def textual?
|
75
75
|
@content_type and ( @content_type =~ /^text\/.+/ or @content_type =~ /^application\/.+/ )
|
76
76
|
end
|
77
77
|
|
@@ -81,7 +81,9 @@ class Response
|
|
81
81
|
# update content length in case the body was
|
82
82
|
# modified
|
83
83
|
if header =~ /Content-Length:\s*(\d+)/i
|
84
|
-
"
|
84
|
+
Logger.debug "Updating response content length from #{$1} to #{@body.bytesize}"
|
85
|
+
|
86
|
+
"Content-Length: #{@body.bytesize}"
|
85
87
|
else
|
86
88
|
header
|
87
89
|
end
|
@@ -0,0 +1,41 @@
|
|
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
|
+
require 'bettercap/logger'
|
13
|
+
|
14
|
+
module Proxy
|
15
|
+
class StreamLogger
|
16
|
+
@@MAX_REQ_SIZE = 50
|
17
|
+
|
18
|
+
@@CODE_COLORS = {
|
19
|
+
'2' => :green,
|
20
|
+
'3' => :light_black,
|
21
|
+
'4' => :yellow,
|
22
|
+
'5' => :red
|
23
|
+
}
|
24
|
+
|
25
|
+
def self.log( is_https, client, request, response )
|
26
|
+
request_s = "#{is_https ? 'https' : 'http'}://#{request.host}#{request.url}"
|
27
|
+
response_s = "( #{response.content_type} )"
|
28
|
+
request_s = request_s.slice(0..@@MAX_REQ_SIZE) + '...' unless request_s.length <= @@MAX_REQ_SIZE
|
29
|
+
code = response.code[0]
|
30
|
+
|
31
|
+
if @@CODE_COLORS.has_key? code
|
32
|
+
response_s += " [#{response.code}]".send( @@CODE_COLORS[ code ] )
|
33
|
+
else
|
34
|
+
response_s += " [#{response.code}]"
|
35
|
+
end
|
36
|
+
|
37
|
+
Logger.write "[#{client}] #{request.verb.light_blue} #{request_s} #{response_s}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
@@ -0,0 +1,119 @@
|
|
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
|
+
require 'bettercap/logger'
|
13
|
+
|
14
|
+
module Proxy
|
15
|
+
class Streamer
|
16
|
+
def initialize( processor )
|
17
|
+
@processor = processor
|
18
|
+
end
|
19
|
+
|
20
|
+
def rickroll( client )
|
21
|
+
client.write "HTTP/1.1 302 Found\n"
|
22
|
+
client.write "Location: https://www.youtube.com/watch?v=dQw4w9WgXcQ\n\n"
|
23
|
+
end
|
24
|
+
|
25
|
+
def html( request, response, from, to )
|
26
|
+
buff = ''
|
27
|
+
|
28
|
+
if response.content_length.nil?
|
29
|
+
Logger.debug "Reading response body using 1024 bytes chunks ..."
|
30
|
+
|
31
|
+
loop do
|
32
|
+
buff = read( from, 1024 )
|
33
|
+
|
34
|
+
break unless buff.size > 0
|
35
|
+
|
36
|
+
response << buff
|
37
|
+
end
|
38
|
+
else
|
39
|
+
Logger.debug "Reading response body using #{response.content_length} bytes buffer ..."
|
40
|
+
|
41
|
+
buff = read( from, response.content_length )
|
42
|
+
|
43
|
+
Logger.debug "Read #{buff.size} / #{response.content_length} bytes."
|
44
|
+
|
45
|
+
response << buff
|
46
|
+
end
|
47
|
+
|
48
|
+
@processor.call( request, response )
|
49
|
+
|
50
|
+
# Response::to_s will patch the headers if needed
|
51
|
+
to.write response.to_s
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
def binary( from, to, opts = {} )
|
56
|
+
total_size = 0
|
57
|
+
|
58
|
+
# if response|request object is available and a content length as well
|
59
|
+
# use it to speed up data streaming with precise data size
|
60
|
+
if not opts[:response].nil?
|
61
|
+
to.write opts[:response].to_s
|
62
|
+
|
63
|
+
total_size = opts[:response].content_length unless opts[:response].content_length.nil?
|
64
|
+
elsif not opts[:request].nil?
|
65
|
+
|
66
|
+
total_size = opts[:request].content_length unless opts[:request].content_length.nil?
|
67
|
+
end
|
68
|
+
|
69
|
+
buff = ''
|
70
|
+
read = 0
|
71
|
+
|
72
|
+
if total_size
|
73
|
+
chunk_size = [ 1024, total_size ].min
|
74
|
+
else
|
75
|
+
chunk_size = 1024
|
76
|
+
end
|
77
|
+
|
78
|
+
if chunk_size > 0
|
79
|
+
loop do
|
80
|
+
buff = read( from, chunk_size )
|
81
|
+
|
82
|
+
# nothing more to read?
|
83
|
+
break unless buff.size > 0
|
84
|
+
|
85
|
+
to.write buff
|
86
|
+
|
87
|
+
read += buff.size
|
88
|
+
|
89
|
+
# collect into the proper object
|
90
|
+
if not opts[:request].nil? and opts[:request].post?
|
91
|
+
opts[:request] << buff
|
92
|
+
end
|
93
|
+
|
94
|
+
# we've done reading?
|
95
|
+
break unless read != total_size
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def read( sd, size )
|
103
|
+
buffer = ''
|
104
|
+
|
105
|
+
while size > 0
|
106
|
+
tmp = sd.read(size)
|
107
|
+
unless tmp.nil?
|
108
|
+
buffer << tmp
|
109
|
+
size -= tmp.bytesize
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
buffer
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
|