bettercap 1.2.2 → 1.2.3

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: 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