bettercap 1.2.2 → 1.2.3

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: 54766752ef4193add9d30875e978316b4489c9f2
4
- data.tar.gz: dd4a0475d6156464f1bbb7b2b0bc55be09e32afc
3
+ metadata.gz: 5a754804a6aed2eab487b2fbe33417c222946953
4
+ data.tar.gz: 0ba2e64fc445000c10b7dd9d2d3ec94c47108dbe
5
5
  SHA512:
6
- metadata.gz: e073d1158b3cc7f6125d7e7b98cf6f41712c491d4687418bed7d647a2435c1ff10f8c41cda3987a19c894a33351648c1fdda1e5db090286280e8f7224136e284
7
- data.tar.gz: 538f6f46aaf601822f7b582e5da4a3432bbe57f3bb17f71f197163ea46447cac9377f909dcf98fca39cd18f67385cb2299a528a495d0137ade3530e25364e93a
6
+ metadata.gz: 4bda0263ceae34eee565a7d5b46400f573b623a7a46f343dd9396a95078cdf2a4118fe2580f748d5c77a0174f589a5b505f466d125585824f5d54dd43457dbb3
7
+ data.tar.gz: 4fa729af4ff2b4a890ab87f53aed5559702e8d82efee9e55986467f28337f652234ca5322b69c20761758d6f6ec0916368f8ae922649100dda50c1d8695e268a
@@ -18,6 +18,7 @@ module Agents
18
18
  class Arp < Discovery::Agents::Base
19
19
  private
20
20
 
21
+ # Build a PacketFu::ARPPacket instance for the specified +ip+ address.
21
22
  def get_probe( ip )
22
23
  pkt = PacketFu::ARPPacket.new
23
24
 
@@ -38,6 +38,7 @@ class Base
38
38
 
39
39
  private
40
40
 
41
+ # Each Discovery::Agent::Base derived class should implement this method.
41
42
  def get_probe( ip )
42
43
  Logger.warn "#{self.class.name}#get_probe not implemented!"
43
44
  end
@@ -17,8 +17,7 @@ module Agents
17
17
  # Class responsible to do a ping-sweep on the network.
18
18
  class Icmp
19
19
  # Create a thread which will perform a ping-sweep on the network in order
20
- # to populate the ARP cache with active targets, with a +ctx.timeout+ seconds
21
- # timeout.
20
+ # to populate the ARP cache with active targets.
22
21
  def initialize( ctx )
23
22
  Factories::Firewall.get.enable_icmp_bcast(true)
24
23
 
@@ -18,6 +18,7 @@ module Agents
18
18
  class Udp < Discovery::Agents::Base
19
19
  private
20
20
 
21
+ # Build an UDP packet for the specified +ip+ address.
21
22
  def get_probe( ip )
22
23
  # send dummy udp packet, just to fill ARP table
23
24
  [ ip.to_s, 137, "\x10\x12\x85\x00\x00" ]
@@ -40,6 +40,8 @@ class Thread
40
40
 
41
41
  private
42
42
 
43
+ # This method implements the main discovery logic, it will be executed within
44
+ # the spawned thread.
43
45
  def worker
44
46
  Logger.debug( 'Network discovery thread started.' ) unless @ctx.options.arpcache
45
47
 
@@ -48,7 +50,7 @@ class Thread
48
50
  @ctx.targets = Network.get_alive_targets(@ctx).sort_by { |t| t.sortable_ip }
49
51
 
50
52
  if was_empty and not @ctx.targets.empty?
51
- Logger.info "Collected #{@ctx.targets.size} total targets."
53
+ Logger.info "Collected #{@ctx.targets.size} total target#{if @ctx.targets.size > 1 then "s" else "" end}."
52
54
 
53
55
  msg = "\n"
54
56
  @ctx.targets.each do |target|
@@ -42,6 +42,7 @@ class Spoofer
42
42
 
43
43
  private
44
44
 
45
+ # Return true if +name+ is a valid spoofer name, otherwise false.
45
46
  def available?(name)
46
47
  available.include?(name)
47
48
  end
@@ -68,6 +68,7 @@ class Base
68
68
 
69
69
  private
70
70
 
71
+ # Method used to raise NotImplementedError exception.
71
72
  def not_implemented_method!
72
73
  raise NotImplementedError, 'Firewalls::Base: Unimplemented method!'
73
74
  end
@@ -73,6 +73,7 @@ module Logger
73
73
 
74
74
  private
75
75
 
76
+ # Main logger logic.
76
77
  def worker
77
78
  loop do
78
79
  message = @@queue.pop
@@ -82,6 +83,7 @@ module Logger
82
83
  end
83
84
  end
84
85
 
86
+ # Emit the +message+.
85
87
  def emit(message)
86
88
  puts message
87
89
  unless @@logfile.nil?
@@ -91,6 +93,7 @@ module Logger
91
93
  end
92
94
  end
93
95
 
96
+ # Format +message+ for the given +message_type+.
94
97
  def formatted_message(message, message_type)
95
98
  "[#{message_type}] #{message}"
96
99
  end
@@ -58,6 +58,8 @@ class ArpReader
58
58
 
59
59
  private
60
60
 
61
+ # Read the computer ARP cache and parse each line, it will yield each
62
+ # ip and mac address it will be able to extract.
61
63
  def self.parse_cache
62
64
  iface = Context.get.ifconfig[:iface]
63
65
  Shell.arp.split("\n").each do |line|
@@ -72,6 +74,7 @@ class ArpReader
72
74
  end
73
75
  end
74
76
 
77
+ # Parse a single ARP cache +line+ related to the +iface+ network interface.
75
78
  def self.parse_cache_line( iface, line )
76
79
  /[^\s]+\s+\(([0-9\.]+)\)\s+at\s+([a-f0-9:]+).+#{iface}.*/i.match(line)
77
80
  end
@@ -133,6 +133,8 @@ class << self
133
133
 
134
134
  private
135
135
 
136
+ # Start discovery agents and wait for +ctx.timeout+ seconds for them to
137
+ # complete their job.
136
138
  def start_agents( ctx )
137
139
  [ 'Icmp', 'Udp', 'Arp' ].each do |name|
138
140
  BetterCap::Loader.load("BetterCap::Discovery::Agents::#{name}").new(ctx)
@@ -140,6 +142,8 @@ class << self
140
142
  ctx.packets.wait_empty( ctx.timeout )
141
143
  end
142
144
 
145
+ # Search for the MAC address associated to +ip_address+ inside the +cap+
146
+ # PacketFu::Capture object.
143
147
  def get_mac_from_capture( cap, ip_address )
144
148
  cap.stream.each do |p|
145
149
  arp_response = PacketFu::Packet.parse(p)
@@ -52,6 +52,8 @@ class PacketQueue
52
52
 
53
53
  private
54
54
 
55
+ # Unpack [ ip, port, data ] from +packet+ and send it using the global
56
+ # UDPSocket instance.
55
57
  def dispatch_udp_packet(packet)
56
58
  ip, port, data = packet
57
59
  @mutex.synchronize {
@@ -60,6 +62,7 @@ class PacketQueue
60
62
  }
61
63
  end
62
64
 
65
+ # Use the global Pcap injection instance to send the +packet+.
63
66
  def dispatch_raw_packet(packet)
64
67
  @mutex.synchronize {
65
68
  Logger.debug "Sending #{packet.class.name} packet ..."
@@ -67,6 +70,7 @@ class PacketQueue
67
70
  }
68
71
  end
69
72
 
73
+ # Main PacketQueue logic.
70
74
  def worker
71
75
  Logger.debug "PacketQueue worker started."
72
76
 
@@ -114,6 +114,7 @@ class Target
114
114
 
115
115
  private
116
116
 
117
+ # Attempt to perform a NBNS name resolution for this target.
117
118
  def resolve!
118
119
  resp, sock = nil, nil
119
120
  begin
@@ -133,10 +134,12 @@ private
133
134
  end
134
135
  end
135
136
 
137
+ # Given the +resp+ NBNS response, parse the hostname from it.
136
138
  def parse_nbns_response resp
137
139
  resp[0][57,15].to_s.strip
138
140
  end
139
141
 
142
+ # Lookup the given +mac+ address in order to find its vendor.
140
143
  def self.lookup_vendor( mac )
141
144
  if @@prefixes == nil
142
145
  Logger.debug 'Preloading hardware vendor prefixes ...'
@@ -56,8 +56,12 @@ class Options
56
56
  attr_accessor :proxy_https
57
57
  # HTTP proxy port.
58
58
  attr_accessor :proxy_port
59
+ # List of HTTP ports, [ 80 ] by default.
60
+ attr_accessor :http_ports
59
61
  # HTTPS proxy port.
60
62
  attr_accessor :proxy_https_port
63
+ # List of HTTPS ports, [ 443 ] by default.
64
+ attr_accessor :https_ports
61
65
  # File name of the PEM certificate to use for the HTTPS proxy.
62
66
  attr_accessor :proxy_pem_file
63
67
  # File name of the transparent proxy module to load.
@@ -100,7 +104,8 @@ class Options
100
104
  @no_target_nbns = false
101
105
  @kill = false
102
106
  @packet_throttle = 0.0
103
-
107
+ @http_ports = [ 80 ]
108
+ @https_ports = [ 443 ]
104
109
  @ignore = nil
105
110
 
106
111
  @sniffer = false
@@ -241,6 +246,14 @@ class Options
241
246
  ctx.options.proxy_port = v.to_i
242
247
  end
243
248
 
249
+ 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|
250
+ ctx.options.http_ports = v
251
+ end
252
+
253
+ 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|
254
+ ctx.options.https_ports = v
255
+ end
256
+
244
257
  opts.on( '--proxy-https-port PORT', 'Set HTTPS proxy port, default to ' + ctx.options.proxy_https_port.to_s + ' .' ) do |v|
245
258
  ctx.options.proxy = true
246
259
  ctx.options.proxy_https = true
@@ -397,6 +410,32 @@ class Options
397
410
  raise BetterCap::Error, 'Invalid custom HTTPS upstream proxy address specified.' unless Network.is_ip? @custom_https_proxy
398
411
  end
399
412
 
413
+ # Parse a comma separated list of ports and return an array containing only
414
+ # valid ports, raise BetterCap::Error if that array is empty.
415
+ def to_ports(value)
416
+ ports = []
417
+ value.split(",").each do |v|
418
+ v = v.strip.to_i
419
+ if v > 0 and v <= 65535
420
+ ports << v
421
+ end
422
+ end
423
+ raise BetterCap::Error, 'Invalid ports specified.' if ports.empty?
424
+ ports
425
+ end
426
+
427
+ # Setter for the #http_ports attribute, will raise a BetterCap::Error if +value+
428
+ # is not a valid comma separated list of ports.
429
+ def http_ports=(value)
430
+ @http_ports = to_ports(value)
431
+ end
432
+
433
+ # Setter for the #https_ports attribute, will raise a BetterCap::Error if +value+
434
+ # is not a valid comma separated list of ports.
435
+ def https_ports=(value)
436
+ @https_ports = to_ports(value)
437
+ end
438
+
400
439
  # Split specified targets and parse them ( either as IP or MAC ), will raise a
401
440
  # BetterCap::Error if one or more invalid addresses are specified.
402
441
  def to_targets
@@ -430,35 +469,43 @@ class Options
430
469
  redirections = []
431
470
 
432
471
  if @proxy
433
- redirections << Firewalls::Redirection.new( @iface,
434
- 'TCP',
435
- 80,
436
- ifconfig[:ip_saddr],
437
- @proxy_port )
472
+ @http_ports.each do |port|
473
+ redirections << Firewalls::Redirection.new( @iface,
474
+ 'TCP',
475
+ port,
476
+ ifconfig[:ip_saddr],
477
+ @proxy_port )
478
+ end
438
479
  end
439
480
 
440
481
  if @proxy_https
441
- redirections << Firewalls::Redirection.new( @iface,
442
- 'TCP',
443
- 443,
444
- ifconfig[:ip_saddr],
445
- @proxy_https_port )
482
+ @https_ports.each do |port|
483
+ redirections << Firewalls::Redirection.new( @iface,
484
+ 'TCP',
485
+ port,
486
+ ifconfig[:ip_saddr],
487
+ @proxy_https_port )
488
+ end
446
489
  end
447
490
 
448
491
  if @custom_proxy
449
- redirections << Firewalls::Redirection.new( @iface,
450
- 'TCP',
451
- 80,
452
- @custom_proxy,
453
- @custom_proxy_port )
492
+ @http_ports.each do |port|
493
+ redirections << Firewalls::Redirection.new( @iface,
494
+ 'TCP',
495
+ port,
496
+ @custom_proxy,
497
+ @custom_proxy_port )
498
+ end
454
499
  end
455
500
 
456
501
  if @custom_https_proxy
457
- redirections << Firewalls::Redirection.new( @iface,
458
- 'TCP',
459
- 443,
460
- @custom_https_proxy,
461
- @custom_https_proxy_port )
502
+ @https_ports.each do |port|
503
+ redirections << Firewalls::Redirection.new( @iface,
504
+ 'TCP',
505
+ port,
506
+ @custom_https_proxy,
507
+ @custom_https_proxy_port )
508
+ end
462
509
  end
463
510
 
464
511
  redirections
@@ -82,6 +82,8 @@ class Module
82
82
 
83
83
  private
84
84
 
85
+ # Loop each available BetterCap::Proxy::Proxy module and yield each
86
+ # one of them for the given code block.
85
87
  def self.each_module
86
88
  Object.constants.each do |klass|
87
89
  const = Kernel.const_get(klass)
@@ -9,10 +9,15 @@ Blog : http://www.evilsocket.net/
9
9
  This project is released under the GPL 3 license.
10
10
 
11
11
  =end
12
+
13
+ # This proxy module will take care of CSS code injection.
12
14
  class Injectcss < BetterCap::Proxy::Module
15
+ # CSS data to be injected.
13
16
  @@cssdata = nil
17
+ # CSS file URL to be injected.
14
18
  @@cssurl = nil
15
19
 
20
+ # Add custom command line arguments to the +opts+ OptionParser instance.
16
21
  def self.on_options(opts)
17
22
  opts.separator ""
18
23
  opts.separator "Inject CSS Proxy Module Options:"
@@ -20,7 +25,7 @@ class Injectcss < BetterCap::Proxy::Module
20
25
 
21
26
  opts.on( '--css-data STRING', 'CSS code to be injected.' ) do |v|
22
27
  @@cssdata = v
23
- unless @@jsdata.include?("<style>")
28
+ unless @@cssdata.include?("<style>")
24
29
  @@cssdata = "<style>\n#{@@cssdata}\n</style>"
25
30
  end
26
31
  end
@@ -39,21 +44,24 @@ class Injectcss < BetterCap::Proxy::Module
39
44
  end
40
45
  end
41
46
 
47
+ # Create an instance of this module and raise a BetterCap::Error if command
48
+ # line arguments weren't correctly specified.
49
+ def initialize
50
+ raise BetterCap::Error, "No --css-file, --css-url or --css-data options specified for the proxy module." if @@cssdata.nil? and @@cssurl.nil?
51
+ end
52
+
53
+ # Called by the BetterCap::Proxy::Proxy processor on each HTTP +request+ and
54
+ # +response+.
42
55
  def on_request( request, response )
43
56
  # is it a html page?
44
57
  if response.content_type =~ /^text\/html.*/
45
- # check command line arguments.
46
- if @@cssdata.nil? and @@cssurl.nil?
47
- BetterCap::Logger.warn "No --css-file or --css-url options specified, this proxy module won't work."
58
+ BetterCap::Logger.info "Injecting CSS #{if @@cssdata.nil? then "URL" else "file" end} into http://#{request.host}#{request.url}"
59
+ # inject URL
60
+ if @@cssdata.nil?
61
+ response.body.sub!( '</head>', " <link rel=\"stylesheet\" href=\"#{@cssurl}\"></script></head>" )
62
+ # inject data
48
63
  else
49
- BetterCap::Logger.info "Injecting CSS #{if @@cssdata.nil? then "URL" else "file" end} into http://#{request.host}#{request.url}"
50
- # inject URL
51
- if @@cssdata.nil?
52
- response.body.sub!( '</head>', " <link rel=\"stylesheet\" href=\"#{@cssurl}\"></script></head>" )
53
- # inject data
54
- else
55
- response.body.sub!( '</head>', "#{@@cssdata}</head>" )
56
- end
64
+ response.body.sub!( '</head>', "#{@@cssdata}</head>" )
57
65
  end
58
66
  end
59
67
  end
@@ -9,10 +9,15 @@ Blog : http://www.evilsocket.net/
9
9
  This project is released under the GPL 3 license.
10
10
 
11
11
  =end
12
+
13
+ # This proxy module will take care of Javascript code injection.
12
14
  class Injectjs < BetterCap::Proxy::Module
15
+ # JS data to be injected.
13
16
  @@jsdata = nil
17
+ # JS file URL to be injected.
14
18
  @@jsurl = nil
15
19
 
20
+ # Add custom command line arguments to the +opts+ OptionParser instance.
16
21
  def self.on_options(opts)
17
22
  opts.separator ""
18
23
  opts.separator "Inject JS Proxy Module Options:"
@@ -39,21 +44,24 @@ class Injectjs < BetterCap::Proxy::Module
39
44
  end
40
45
  end
41
46
 
47
+ # Create an instance of this module and raise a BetterCap::Error if command
48
+ # line arguments weren't correctly specified.
49
+ def initialize
50
+ raise BetterCap::Error, "No --js-file, --js-url or --js-data options specified for the proxy module." if @@jsdata.nil? and @@jsurl.nil?
51
+ end
52
+
53
+ # Called by the BetterCap::Proxy::Proxy processor on each HTTP +request+ and
54
+ # +response+.
42
55
  def on_request( request, response )
43
56
  # is it a html page?
44
57
  if response.content_type =~ /^text\/html.*/
45
- # check command line arguments.
46
- if @@jsdata.nil? and @@jsurl.nil?
47
- BetterCap::Logger.warn "No --js-file or --js-url options specified, this proxy module won't work."
58
+ BetterCap::Logger.info "Injecting javascript #{if @@jsdata.nil? then "URL" else "file" end} into http://#{request.host}#{request.url}"
59
+ # inject URL
60
+ if @@jsdata.nil?
61
+ response.body.sub!( '</head>', "<script src=\"#{@@jsurl}\" type=\"text/javascript\"></script></head>" )
62
+ # inject data
48
63
  else
49
- BetterCap::Logger.info "Injecting javascript #{if @@jsdata.nil? then "URL" else "file" end} into http://#{request.host}#{request.url}"
50
- # inject URL
51
- if @@jsdata.nil?
52
- response.body.sub!( '</head>', "<script src=\"#{@@jsurl}\" type=\"text/javascript\"></script></head>" )
53
- # inject data
54
- else
55
- response.body.sub!( '</head>', "#{@@jsdata}</head>" )
56
- end
64
+ response.body.sub!( '</head>', "#{@@jsdata}</head>" )
57
65
  end
58
66
  end
59
67
  end
@@ -87,6 +87,8 @@ class Proxy
87
87
 
88
88
  private
89
89
 
90
+ # Main server thread, will accept incoming connections and push them to
91
+ # the thread pool.
90
92
  def server_thread
91
93
  Logger.info "#{@type} Proxy started on #{@address}:#{@port} ...\n"
92
94
 
@@ -103,100 +105,31 @@ class Proxy
103
105
  @socket.close unless @socket.nil?
104
106
  end
105
107
 
108
+ # Return true if the +request+ host header contains one of this computer
109
+ # ip addresses.
106
110
  def is_self_request?(request)
107
111
  @local_ips.include? IPSocket.getaddress(request.host)
108
112
  end
109
113
 
110
- def create_upstream_connection( request )
111
- sock = TCPSocket.new( request.host, request.port )
112
-
113
- if @is_https
114
- ctx = OpenSSL::SSL::SSLContext.new
115
- # do we need this? :P ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
116
-
117
- sock = OpenSSL::SSL::SSLSocket.new(sock, ctx).tap do |socket|
118
- sock.sync_close = true
119
- sock.connect
120
- end
121
- end
122
-
123
- sock
124
- end
125
-
126
- def get_client_details( client )
127
- unless @is_https
128
- client_port, client_ip = Socket.unpack_sockaddr_in(client.getpeername)
129
- else
130
- _, client_port, _, client_ip = client.peeraddr
131
- end
132
-
133
- [ client_ip, client_port ]
134
- end
135
-
114
+ # Handle a new +client+.
136
115
  def client_worker( client )
137
- client_ip, client_port = get_client_details client
138
-
139
- Logger.debug "New #{@type} connection from #{client_ip}:#{client_port}"
140
-
141
- server = nil
142
116
  request = Request.new @is_https ? 443 : 80
143
117
 
144
118
  begin
145
119
  Logger.debug 'Reading request ...'
146
120
 
147
- request.read client
121
+ request.read(client)
148
122
 
149
123
  # someone is having fun with us =)
150
124
  if is_self_request? request
151
-
152
- Logger.warn "#{client_ip} is connecting to us directly."
153
-
154
125
  @streamer.rickroll client
155
-
156
- elsif request.verb == 'CONNECT'
157
-
158
- Logger.error "You're using bettercap as a normal HTTP(S) proxy, it wasn't designed to handle CONNECT requests:\n\n#{request.to_s}"
159
-
126
+ # handle request
160
127
  else
161
-
162
- Logger.debug 'Creating upstream connection ...'
163
-
164
- server = create_upstream_connection request
165
-
166
- sreq = request.to_s
167
-
168
- Logger.debug "Sending request:\n#{sreq}"
169
-
170
- server.write sreq
171
-
172
- # this is probably a POST request, collect incoming data
173
- if request.content_length > 0
174
- Logger.debug "Getting #{request.content_length} bytes from client"
175
-
176
- @streamer.binary client, server, request: request
177
- end
178
-
179
- Logger.debug 'Reading response ...'
180
-
181
- response = Response.from_socket server
182
-
183
- if response.textual?
184
- StreamLogger.log_http( @is_https, client_ip, request, response )
185
-
186
- Logger.debug 'Detected textual response'
187
-
188
- @streamer.html request, response, server, client
189
- else
190
- Logger.debug "[#{client_ip}] -> #{request.host}#{request.url} [#{response.code}]"
191
-
192
- Logger.debug 'Binary streaming'
193
-
194
- @streamer.binary server, client, response: response
195
- end
196
-
197
- Logger.debug "#{@type} client served."
128
+ @streamer.handle( request, client, @is_https )
198
129
  end
199
130
 
131
+ Logger.debug "#{@type} client served."
132
+
200
133
  rescue Exception => e
201
134
  if request.host
202
135
  Logger.warn "Error while serving #{request.host}#{request.url}: #{e.inspect}"
@@ -205,7 +138,6 @@ class Proxy
205
138
  end
206
139
 
207
140
  client.close
208
- server.close unless server.nil?
209
141
  end
210
142
  end
211
143
 
@@ -24,8 +24,12 @@ class Request
24
24
  attr_reader :host
25
25
  # Request port.
26
26
  attr_reader :port
27
+ # Request headers hash.
28
+ attr_reader :headers
27
29
  # Content length.
28
30
  attr_reader :content_length
31
+ # Request body.
32
+ attr_reader :body
29
33
 
30
34
  # Initialize this object setting #port to +default_port+.
31
35
  def initialize( default_port = 80 )
@@ -34,7 +38,9 @@ class Request
34
38
  @url = nil
35
39
  @host = nil
36
40
  @port = default_port
41
+ @headers = {}
37
42
  @content_length = 0
43
+ @body = nil
38
44
  end
39
45
 
40
46
  # Read lines from the +sock+ socket and parse them.
@@ -53,6 +59,11 @@ class Request
53
59
  end
54
60
 
55
61
  raise "Couldn't extract host from the request." unless @host
62
+
63
+ # keep reading the request body if needed
64
+ if @content_length > 0
65
+ @body = sock.read(@content_length)
66
+ end
56
67
  end
57
68
 
58
69
  # Parse a single request line, patch it if needed and append it to #lines.
@@ -93,6 +104,11 @@ class Request
93
104
  line = 'Accept-Encoding: identity'
94
105
  end
95
106
 
107
+ # collect headers
108
+ if line =~ /^([^:\s]+)\s*:\s*(.+)$/i
109
+ @headers[$1] = $2
110
+ end
111
+
96
112
  @lines << line
97
113
  end
98
114
 
@@ -103,7 +119,7 @@ class Request
103
119
 
104
120
  # Return a string representation of the HTTP request.
105
121
  def to_s
106
- @lines.join("\n") + "\n"
122
+ @lines.join("\n") + "\n" + ( @body || '' )
107
123
  end
108
124
  end
109
125
  end
@@ -25,7 +25,7 @@ class Response
25
25
  # A list of response headers.
26
26
  attr_reader :headers
27
27
  # Response status code.
28
- attr_reader :code
28
+ attr_accessor :code
29
29
  # True if the parser finished to parse the headers, otherwise false.
30
30
  attr_reader :headers_done
31
31
  # Response body.
@@ -43,21 +43,15 @@ class Response
43
43
  @chunked = false
44
44
  end
45
45
 
46
- # Read lines from the +sock+ socket until all headers are correctly parsed
47
- # and return a BetterCap::Proxy::Response instance.
48
- def self.from_socket(sock)
49
- response = Response.new
50
-
51
- # read all response headers
52
- loop do
53
- line = sock.readline
54
-
55
- response << line
56
-
57
- break unless not response.headers_done
46
+ # Convert a webrick response to this class.
47
+ def convert_webrick_response!(response)
48
+ self << "HTTP/#{response.http_version} #{response.code} #{response.msg}"
49
+ response.each do |key,value|
50
+ self << "#{key}: #{value}"
58
51
  end
59
-
60
- response
52
+ self << "\n"
53
+ @code = response.code
54
+ @body = response.body || ''
61
55
  end
62
56
 
63
57
  # Parse a single response +line+.
@@ -15,9 +15,6 @@ module BetterCap
15
15
  module Proxy
16
16
  # Handle data streaming between clients and servers for the BetterCap::Proxy::Proxy.
17
17
  class Streamer
18
- # Default buffer size for data streaming.
19
- BUFSIZE = 1024 * 16
20
-
21
18
  # Initialize the class with the given +processor+ routine.
22
19
  def initialize( processor )
23
20
  @processor = processor
@@ -25,155 +22,89 @@ class Streamer
25
22
 
26
23
  # Redirect the +client+ to a funny video.
27
24
  def rickroll( client )
25
+ Logger.warn "#{client_ip} is connecting to us directly."
26
+
28
27
  client.write "HTTP/1.1 302 Found\n"
29
28
  client.write "Location: https://www.youtube.com/watch?v=dQw4w9WgXcQ\n\n"
30
29
  end
31
30
 
32
- # Perform HTML streaming for the given +request+, applying the #processor
33
- # to the +response+.
34
- # +from+ and +to+ are the two TCP endpoints.
35
- def html( request, response, from, to )
36
- buff = ''
37
-
38
- if response.chunked
39
- Logger.debug "Reading response body using chunked encoding ..."
40
-
41
- begin
42
- len = nil
43
- total = 0
44
-
45
- while true
46
- line = from.readline
47
- hexlen = line.slice(/[0-9a-fA-F]+/) or raise "Wrong chunk size line: #{line}"
48
- len = hexlen.hex
49
- break if len == 0
50
- begin
51
- Logger.debug "Reading chunk of size #{len} ..."
52
- tmp = read( from, len )
53
- Logger.debug "Read #{tmp.bytesize}/#{len} chunk."
54
- response << tmp
55
- ensure
56
- total += len
57
- from.read 2
58
- end
59
- end
60
-
61
- rescue Exception => e
62
- Logger.debug e
63
- end
64
-
65
- elsif response.content_length.nil?
66
- Logger.debug "Reading response body using 1024 bytes chunks ..."
31
+ # Handle the HTTP +request+ from +client+, if +is_https+ is true it will be
32
+ # forwarded as a HTTPS request.
33
+ def handle( request, client, is_https )
34
+ response = Response.new
35
+ client_ip, client_port = get_client_details( is_https, client )
67
36
 
68
- loop do
69
- buff = read( from, 1024 )
37
+ Logger.debug "Handling #{request.verb} request from #{client_ip}:#{client_port} ..."
70
38
 
71
- break unless buff.size > 0
39
+ begin
40
+ self.send( "do_#{request.verb}", request, response )
72
41
 
73
- response << buff
42
+ if response.textual?
43
+ StreamLogger.log_http( is_https, client_ip, request, response )
44
+ else
45
+ Logger.debug "[#{client_ip}] -> #{request.host}#{request.url} [#{response.code}]"
74
46
  end
75
- else
76
- Logger.debug "Reading response body using #{response.content_length} bytes buffer ..."
77
-
78
- buff = read( from, response.content_length )
79
47
 
80
- Logger.debug "Read #{buff.size} / #{response.content_length} bytes."
48
+ @processor.call( request, response )
81
49
 
82
- response << buff
50
+ client.write response.to_s
51
+ rescue NoMethodError
52
+ Logger.warn "Could not handle #{request.verb} request from #{client_ip}:#{client_port} ..."
83
53
  end
84
-
85
- @processor.call( request, response )
86
-
87
- # Response::to_s will patch the headers if needed
88
- to.write response.to_s
89
54
  end
90
55
 
91
- # Perform binary streaming using the +opts+ dictionary.
92
- # If response|request object is available inside +opts+ and a content length
93
- # as well use it to speed up data streaming with precise data size
94
- # +from+ and +to+ are the two TCP endpoints.
95
- def binary( from, to, opts = {} )
96
- total_size = 0
97
-
98
- if not opts[:response].nil?
99
- to.write opts[:response].to_s
100
-
101
- total_size = opts[:response].content_length unless opts[:response].content_length.nil?
102
- elsif not opts[:request].nil?
103
-
104
- total_size = opts[:request].content_length unless opts[:request].content_length.nil?
105
- end
106
-
107
- buff = ''
108
- read = 0
56
+ private
109
57
 
110
- if total_size
111
- chunk_size = [ 1024, total_size ].min
58
+ # Return the +client+ ip address and port.
59
+ def get_client_details( is_https, client )
60
+ unless is_https
61
+ client_port, client_ip = Socket.unpack_sockaddr_in(client.getpeername)
112
62
  else
113
- chunk_size = 1024
63
+ _, client_port, _, client_ip = client.peeraddr
114
64
  end
115
65
 
116
- if chunk_size > 0
117
- loop do
118
- buff = read( from, chunk_size )
66
+ [ client_ip, client_port ]
67
+ end
119
68
 
120
- # nothing more to read?
121
- break unless buff.size > 0
69
+ # Use a Net::HTTP object in order to perform the +req+ BetterCap::Proxy::Request
70
+ # object, will return a BetterCap::Proxy::Response object instance.
71
+ def perform_proxy_request(req, res)
72
+ path = req.url
73
+ response = nil
74
+ http = Net::HTTP.new( req.host, req.port )
75
+ http.use_ssl = ( req.port == 443 )
122
76
 
123
- to.write buff
77
+ http.start do
78
+ response = yield( http, path, req.headers )
79
+ end
124
80
 
125
- read += buff.size
81
+ res.convert_webrick_response!(response)
82
+ end
126
83
 
127
- # collect into the proper object
128
- if not opts[:request].nil? and opts[:request].post?
129
- opts[:request] << buff
130
- end
84
+ # Handle a CONNECT request, +req+ is the request object and +res+ the response.
85
+ def do_CONNECT(req, res)
86
+ Logger.error "You're using bettercap as a normal HTTP(S) proxy, it wasn't designed to handle CONNECT requests:\n\n#{req.to_s}"
87
+ end
131
88
 
132
- # we've done reading?
133
- break unless read != total_size
134
- end
89
+ # Handle a GET request, +req+ is the request object and +res+ the response.
90
+ def do_GET(req, res)
91
+ perform_proxy_request(req, res) do |http, path, header|
92
+ http.get(path, header)
135
93
  end
136
94
  end
137
95
 
138
- private
139
-
140
- def consume_stream io, size
141
- read_timeout = 60.0
142
- dest = ''
143
- begin
144
- dest << io.read_nonblock(size)
145
- rescue IO::WaitReadable
146
- if IO.select([io], nil, nil, read_timeout)
147
- retry
148
- else
149
- raise Net::ReadTimeout
150
- end
151
- rescue IO::WaitWritable
152
- # OpenSSL::Buffering#read_nonblock may fail with IO::WaitWritable.
153
- # http://www.openssl.org/support/faq.html#PROG10
154
- if IO.select(nil, [io], nil, read_timeout)
155
- retry
156
- else
157
- raise Net::ReadTimeout
158
- end
96
+ # Handle a HEAD request, +req+ is the request object and +res+ the response.
97
+ def do_HEAD(req, res)
98
+ perform_proxy_request(req, res) do |http, path, header|
99
+ http.head(path, header)
159
100
  end
160
- dest
161
101
  end
162
102
 
163
- def read( sd, size )
164
- buffer = ''
165
- begin
166
- while size > 0
167
- tmp = consume_stream sd, [ BUFSIZE, size ].min
168
- unless tmp.nil? or tmp.bytesize == 0
169
- buffer << tmp
170
- size -= tmp.bytesize
171
- end
172
- end
173
- rescue EOFError
174
- ;
103
+ # Handle a POST request, +req+ is the request object and +res+ the response.
104
+ def do_POST(req, res)
105
+ perform_proxy_request(req, res) do |http, path, header|
106
+ http.post(path, req.body || "", header)
175
107
  end
176
- buffer
177
108
  end
178
109
 
179
110
  end
@@ -52,6 +52,7 @@ class Sniffer
52
52
 
53
53
  private
54
54
 
55
+ # Return the current PCAP stream.
55
56
  def self.stream
56
57
  if @@ctx.options.sniffer_src.nil?
57
58
  @@cap.stream
@@ -62,12 +63,14 @@ class Sniffer
62
63
  end
63
64
  end
64
65
 
66
+ # Return true if the +pkt+ packet instance must be skipped.
65
67
  def self.skip_packet?( pkt )
66
68
  !@@ctx.options.local and
67
69
  ( pkt.ip_saddr == @@ctx.ifconfig[:ip_saddr] or
68
70
  pkt.ip_daddr == @@ctx.ifconfig[:ip_saddr] )
69
71
  end
70
72
 
73
+ # Apply each parser on the given +parsed+ packet.
71
74
  def self.parse_packet( parsed )
72
75
  @@parsers.each do |parser|
73
76
  begin
@@ -78,6 +81,7 @@ class Sniffer
78
81
  end
79
82
  end
80
83
 
84
+ # Append the packet +p+ to the current PCAP file.
81
85
  def self.append_packet( p )
82
86
  begin
83
87
  @@pcap.array_to_file(
@@ -89,6 +93,7 @@ class Sniffer
89
93
  end
90
94
  end
91
95
 
96
+ # Setup all needed objects for the sniffer using the +ctx+ Context instance.
92
97
  def self.setup( ctx )
93
98
  @@ctx = ctx
94
99
 
@@ -80,37 +80,51 @@ class Arp < Base
80
80
 
81
81
  @ctx.targets.each do |target|
82
82
  unless target.ip.nil? or target.mac.nil?
83
- begin
84
- send_spoofed_packet( @gateway.ip, @gateway.mac, target.ip, target.mac )
85
- send_spoofed_packet( target.ip, target.mac, @gateway.ip, @gateway.mac ) unless @ctx.options.half_duplex
86
- rescue; end
83
+ spoof(target)
87
84
  end
88
85
  end
89
86
  end
90
87
 
91
88
  private
92
89
 
90
+ # Send an ARP spoofing packet to +target+, if +restore+ is true it will
91
+ # restore its ARP cache instead.
92
+ def spoof( target, restore = false )
93
+ if restore
94
+ send_spoofed_packet( @gateway.ip, @ctx.ifconfig[:eth_saddr], target.ip, target.mac )
95
+ send_spoofed_packet( target.ip, @ctx.ifconfig[:eth_saddr], @gateway.ip, @gateway.mac ) unless @ctx.options.half_duplex
96
+ else
97
+ send_spoofed_packet( @gateway.ip, @gateway.mac, target.ip, target.mac )
98
+ send_spoofed_packet( target.ip, target.mac, @gateway.ip, @gateway.mac ) unless @ctx.options.half_duplex
99
+ end
100
+ end
101
+
102
+ # Main spoofer loop.
93
103
  def arp_spoofer
94
104
  spoof_loop(1) { |target|
95
105
  unless target.ip.nil? or target.mac.nil?
96
- send_spoofed_packet( @gateway.ip, @ctx.ifconfig[:eth_saddr], target.ip, target.mac )
97
- send_spoofed_packet( target.ip, @ctx.ifconfig[:eth_saddr], @gateway.ip, @gateway.mac ) unless @ctx.options.half_duplex
106
+ spoof(target, true)
98
107
  end
99
108
  }
100
109
  end
101
110
 
111
+ # Return true if the +pkt+ packet is an ARP 'who-has' query coming
112
+ # from some network endpoint.
113
+ def is_arp_query?(pkt)
114
+ # we're only interested in 'who-has' packets
115
+ pkt.arp_opcode == 1 and \
116
+ pkt.arp_dst_mac.to_s == '00:00:00:00:00:00' and \
117
+ pkt.arp_src_ip.to_s != @ctx.ifconfig[:ip_saddr]
118
+ end
119
+
120
+ # Will watch for incoming ARP requests and spoof the source address.
102
121
  def arp_watcher
103
122
  Logger.debug 'ARP watcher started ...'
104
123
 
105
124
  sniff_packets('arp') { |pkt|
106
- # we're only interested in 'who-has' packets
107
- if pkt.arp_opcode == 1 and pkt.arp_dst_mac.to_s == '00:00:00:00:00:00'
108
- is_from_us = ( pkt.arp_src_ip.to_s == @ctx.ifconfig[:ip_saddr] )
109
- unless is_from_us
110
- Logger.info "[ARP] #{pkt.arp_src_ip.to_s} is asking who #{pkt.arp_dst_ip.to_s} is."
111
-
112
- send_spoofed_packet pkt.arp_dst_ip.to_s, @ctx.ifconfig[:eth_saddr], pkt.arp_src_ip.to_s, pkt.arp_src_mac.to_s
113
- end
125
+ if is_arp_query?(pkt)
126
+ Logger.info "[#{'ARP'.green}] #{pkt.arp_src_ip.to_s} is asking who #{pkt.arp_dst_ip.to_s} is."
127
+ send_spoofed_packet pkt.arp_dst_ip.to_s, @ctx.ifconfig[:eth_saddr], pkt.arp_src_ip.to_s, pkt.arp_src_mac.to_s
114
128
  end
115
129
  }
116
130
  end
@@ -28,6 +28,8 @@ class Base
28
28
 
29
29
  private
30
30
 
31
+ # Will create a PacketFu::Capture object using the specified +filter+ and
32
+ # will yield every parsed packet to the given code block.
31
33
  def sniff_packets( filter )
32
34
  begin
33
35
  @capture = PacketFu::Capture.new(
@@ -57,21 +59,49 @@ private
57
59
  end
58
60
  end
59
61
 
62
+ # Print informations about new and lost targets.
63
+ def print_differences( prev_targets )
64
+ size = @ctx.targets.size
65
+ prev_size = prev_targets.size
66
+ diff = nil
67
+ label = nil
68
+
69
+ if size > prev_size
70
+ diff = @ctx.targets - prev_targets
71
+ delta = diff.size
72
+ label = 'NEW'.green
73
+
74
+ Logger.warn "Acquired #{delta} new target#{if delta > 1 then "s" else "" end}."
75
+ elsif size < prev_size
76
+ diff = prev_targets - @ctx.targets
77
+ delta = diff.size
78
+ label = 'LOST'.red
79
+
80
+ Logger.warn "Lost #{delta} target#{if delta > 1 then "s" else "" end}."
81
+ end
82
+
83
+ unless diff.nil?
84
+ msg = "\n"
85
+ diff.each do |target|
86
+ msg += " [#{label}] #{target.to_s(false)}\n"
87
+ end
88
+ msg += "\n"
89
+ Logger.raw msg
90
+ end
91
+ end
92
+
93
+ # Main spoof loop repeated each +delay+ seconds.
60
94
  def spoof_loop( delay )
61
- prev_size = @ctx.targets.size
95
+ prev_targets = @ctx.targets
96
+
62
97
  loop do
63
- if not @running
98
+ unless @running
64
99
  Logger.debug 'Stopping spoofing thread ...'
65
100
  Thread.exit
66
101
  break
67
102
  end
68
103
 
69
- size = @ctx.targets.size
70
- if size > prev_size
71
- Logger.warn "Aquired #{size - prev_size} new targets."
72
- elsif size < prev_size
73
- Logger.warn "Lost #{prev_size - size} targets."
74
- end
104
+ print_differences prev_targets
75
105
 
76
106
  Logger.debug "Spoofing #{@ctx.targets.size} targets ..."
77
107
 
@@ -81,12 +111,13 @@ private
81
111
  yield(target)
82
112
  end
83
113
 
84
- prev_size = @ctx.targets.size
114
+ prev_targets = @ctx.targets
85
115
 
86
116
  sleep(delay)
87
117
  end
88
118
  end
89
119
 
120
+ # Get the MAC address of the gateway and update it.
90
121
  def update_gateway!
91
122
  hw = Network.get_hw_address( @ctx.ifconfig, @ctx.gateway )
92
123
  raise BetterCap::Error, "Couldn't determine router MAC" if hw.nil?
@@ -95,6 +126,7 @@ private
95
126
  Logger.info "[#{'GATEWAY'.green}] #{@gateway.to_s(false)}"
96
127
  end
97
128
 
129
+ # Update each target that needs to be updated.
98
130
  def update_targets!
99
131
  @ctx.targets.each do |target|
100
132
  # targets could change, update mac addresses if needed
@@ -122,6 +154,7 @@ private
122
154
  end
123
155
  end
124
156
 
157
+ # Used to raise a NotImplementedError exception.
125
158
  def not_implemented_method!
126
159
  raise NotImplementedError, 'Spoofers::Base: Unimplemented method!'
127
160
  end
@@ -30,6 +30,7 @@ class ICMPRedirectPacket < PacketFu::Packet
30
30
 
31
31
  attr_accessor :eth_header, :ip_header, :icmp_header, :ip_encl_header
32
32
 
33
+ # Create a ICMPRedirectPacket instance.
33
34
  def initialize(args={})
34
35
  @eth_header = PacketFu::EthHeader.new(args).read(args[:eth])
35
36
 
@@ -54,6 +55,8 @@ class ICMPRedirectPacket < PacketFu::Packet
54
55
  super
55
56
  end
56
57
 
58
+ # Update this packet with the correct +gateway+, +target+, +local+ address
59
+ # and +address2redirect+.
57
60
  def update!( gateway, target, local, address2redirect )
58
61
  @eth_header.eth_src = PacketFu::EthHeader.mac2str(gateway.mac)
59
62
  @ip_header.ip_saddr = gateway.ip
@@ -145,6 +148,7 @@ class Icmp < Base
145
148
 
146
149
  private
147
150
 
151
+ # Return true if the +pkt+ packet comes from one of our targets.
148
152
  def is_interesting_packet?(pkt)
149
153
  return false if pkt.ip_saddr == @local
150
154
  @ctx.targets.each do |target|
@@ -155,6 +159,7 @@ class Icmp < Base
155
159
  false
156
160
  end
157
161
 
162
+ # DNS watcher logic.
158
163
  def dns_watcher
159
164
  Logger.debug 'DNS watcher started ...'
160
165
 
@@ -188,6 +193,7 @@ class Icmp < Base
188
193
  }
189
194
  end
190
195
 
196
+ # Main spoofer loop.
191
197
  def icmp_spoofer
192
198
  spoof_loop(3) { |target|
193
199
  unless target.ip.nil? or target.mac.nil?
@@ -19,13 +19,41 @@ class None < Base
19
19
  # Initialize the non-spoofing class.
20
20
  def initialize
21
21
  Logger.warn 'Spoofing disabled.'
22
+
23
+ @ctx = Context.get
24
+ @gateway = nil
25
+ @thread = nil
26
+ @running = false
27
+
28
+ update_gateway!
29
+ end
30
+
31
+ # Start the "NONE" spoofer.
32
+ def start
33
+ stop() if @running
34
+ @running = true
35
+
36
+ @thread = Thread.new { fake_spoofer }
22
37
  end
23
38
 
24
- # This does nothing.
25
- def start; end
39
+ # Stop the "NONE" spoofer.
40
+ def stop
41
+ return unless @running
42
+
43
+ @running = false
44
+ begin
45
+ @thread.exit
46
+ rescue
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ # Main fake spoofer loop.
53
+ def fake_spoofer
54
+ spoof_loop(1) { |target| }
55
+ end
26
56
 
27
- # This does nothing.
28
- def stop; end
29
57
  end
30
58
  end
31
59
  end
@@ -11,7 +11,7 @@ This project is released under the GPL 3 license.
11
11
  =end
12
12
  module BetterCap
13
13
  # Current version of bettercap.
14
- VERSION = '1.2.2'
14
+ VERSION = '1.2.3'
15
15
  # Program banner.
16
16
  BANNER = File.read( File.dirname(__FILE__) + '/banner' ).gsub( '#VERSION#', "v#{VERSION}")
17
17
  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.2.2
4
+ version: 1.2.3
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-01-16 00:00:00.000000000 Z
11
+ date: 2016-01-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize