Skip to content

Commit f676b72

Browse files
author
HD Moore
committed
Add Kademlia scanner, lands rapid7#4210
2 parents e088a48 + 338cce0 commit f676b72

File tree

18 files changed

+620
-0
lines changed

18 files changed

+620
-0
lines changed

lib/msf/core/auxiliary/kademlia.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# -*- coding: binary -*-
2+
3+
require 'rex/proto/kademlia'
4+
5+
module Msf
6+
7+
###
8+
#
9+
# This module provides methods for working with Kademlia
10+
#
11+
###
12+
module Auxiliary::Kademlia
13+
include Rex::Proto::Kademlia
14+
end
15+
end

lib/msf/core/auxiliary/mixins.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
require 'msf/core/auxiliary/login'
2020
require 'msf/core/auxiliary/rservices'
2121
require 'msf/core/auxiliary/cisco'
22+
require 'msf/core/auxiliary/kademlia'
2223
require 'msf/core/auxiliary/nmap'
2324
require 'msf/core/auxiliary/natpmp'
2425
require 'msf/core/auxiliary/iax2'

lib/rex/proto/kademlia.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# -*- coding: binary -*-
2+
3+
require 'rex/proto/kademlia/bootstrap_request'
4+
require 'rex/proto/kademlia/bootstrap_response'
5+
require 'rex/proto/kademlia/message'
6+
require 'rex/proto/kademlia/ping'
7+
require 'rex/proto/kademlia/pong'
8+
require 'rex/proto/kademlia/util'
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# -*- coding: binary -*-
2+
3+
require 'rex/proto/kademlia/message'
4+
5+
module Rex
6+
module Proto
7+
module Kademlia
8+
# Opcode for a BOOTSTRAP request
9+
BOOTSTRAP_REQUEST = 0x01
10+
11+
# A Kademlia bootstrap request message
12+
class BootstrapRequest < Message
13+
def initialize
14+
super(BOOTSTRAP_REQUEST)
15+
end
16+
end
17+
end
18+
end
19+
end
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# -*- coding: binary -*-
2+
3+
require 'rex/proto/kademlia/message'
4+
require 'rex/proto/kademlia/util'
5+
6+
module Rex
7+
module Proto
8+
module Kademlia
9+
# Opcode for a bootstrap response
10+
BOOTSTRAP_RESPONSE = 0x09
11+
12+
# A Kademlia bootstrap response message
13+
class BootstrapResponse < Message
14+
# @return [String] the ID of the peer that send the bootstrap response
15+
attr_reader :peer_id
16+
# @return [Integer] the TCP port that the responding peer is listening on
17+
attr_reader :tcp_port
18+
# @return [Integer] the version of this peer
19+
attr_reader :version
20+
# @return [Array<Hash<String, Object>>] the peer ID, IP address, UDP/TCP ports and version of each peer
21+
attr_reader :peers
22+
23+
# Constructs a new bootstrap response
24+
#
25+
# @param peer_id [String] the ID of this peer
26+
# @param tcp_port [Integer] the TCP port that this peer is listening on
27+
# @param version [Integer] the version of this peer
28+
# @param peers [Array<Hash<String, Object>>] the peer ID, IP address, UDP/TCP ports and version of each peer
29+
def initialize(peer_id, tcp_port, version, peers)
30+
@peer_id = peer_id
31+
@tcp_port = tcp_port
32+
@version = version
33+
@peers = peers
34+
end
35+
36+
# The minimum size of a peer in a KADEMLIA2_BOOTSTRAP_RES message:
37+
# peer ID (16-bytes), IP (4 bytes), UDP port (2 bytes), TCP port (2 bytes)
38+
# and version (1 byte)
39+
BOOTSTRAP_PEER_SIZE = 25
40+
41+
# Builds a bootstrap response from given data
42+
#
43+
# @param data [String] the data to decode
44+
# @return [BootstrapResponse] the bootstrap response if the data is valid, nil otherwise
45+
def self.from_data(data)
46+
message = Message.from_data(data)
47+
# abort if this isn't a valid response
48+
return unless message
49+
return unless message.type == BOOTSTRAP_RESPONSE
50+
return unless message.body.size >= 23
51+
bootstrap_peer_id = Rex::Proto::Kademlia.decode_peer_id(message.body.slice!(0, 16))
52+
bootstrap_tcp_port, bootstrap_version, num_peers = message.body.slice!(0, 5).unpack('vCv')
53+
# protocol says there are no peers and the body confirms this, so just return with no peers
54+
if num_peers == 0 && message.body.blank?
55+
peers = []
56+
else
57+
peers_data = message.body
58+
# peers data is too long/short, abort
59+
return if peers_data.size % BOOTSTRAP_PEER_SIZE != 0
60+
peers = []
61+
until peers_data.blank?
62+
peer_data = peers_data.slice!(0, BOOTSTRAP_PEER_SIZE)
63+
peer_id = Rex::Proto::Kademlia.decode_peer_id(peer_data.slice!(0, 16))
64+
ip, udp_port, tcp_port, version = peer_data.unpack('VvvC')
65+
peers << {
66+
id: peer_id,
67+
ip: Rex::Socket.addr_itoa(ip),
68+
tcp_port: tcp_port,
69+
udp_port: udp_port,
70+
version: version
71+
}
72+
end
73+
end
74+
BootstrapResponse.new(bootstrap_peer_id, bootstrap_tcp_port, bootstrap_version, peers)
75+
end
76+
end
77+
end
78+
end
79+
end

lib/rex/proto/kademlia/message.rb

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# -*- coding: binary -*-
2+
3+
module Rex
4+
module Proto
5+
##
6+
#
7+
# Minimal support for the newer Kademlia protocol, referred to here and often
8+
# elsewhere as Kademlia2. It is unclear how this differs from the old protocol.
9+
#
10+
# Protocol details are hard to come by because most documentation is academic
11+
# in nature and glosses over the low-level network details. The best
12+
# documents I found on the protocol are:
13+
#
14+
# http://gbmaster.wordpress.com/2013/05/05/botnets-surrounding-us-an-initial-focus-on-kad/
15+
# http://gbmaster.wordpress.com/2013/06/16/botnets-surrounding-us-sending-kademlia2_bootstrap_req-kademlia2_hello_req-and-their-strict-cousins/
16+
# http://gbmaster.wordpress.com/2013/11/23/botnets-surrounding-us-performing-requests-sending-out-kademlia2_req-and-asking-contact-where-art-thou/
17+
#
18+
##
19+
module Kademlia
20+
# A simple Kademlia message
21+
class Message
22+
# The header that non-compressed Kad messages use
23+
STANDARD_PACKET = 0xE4
24+
# The header that compressed Kad messages use, which is currently unsupported
25+
COMPRESSED_PACKET = 0xE5
26+
27+
# @return [Integer] the message type
28+
attr_reader :type
29+
# @return [String] the message body
30+
attr_reader :body
31+
32+
# Construct a new Message from the provided type and body
33+
#
34+
# @param type [String] the message type
35+
# @param body [String] the message body
36+
def initialize(type, body = '')
37+
@type = type
38+
@body = body
39+
end
40+
41+
# Construct a new Message from the provided data
42+
#
43+
# @param data [String] the data to interpret as a Kademlia message
44+
# @return [Message] the message if valid, nil otherwise
45+
def self.from_data(data)
46+
return if data.length < 2
47+
header, type = data.unpack('CC')
48+
if header == COMPRESSED_PACKET
49+
fail NotImplementedError, "Unable to handle #{data.length}-byte compressed Kademlia message"
50+
end
51+
return if header != STANDARD_PACKET
52+
Message.new(type, data[2, data.length])
53+
end
54+
55+
# Get this Message as a String
56+
#
57+
# @return [String] the string representation of this Message
58+
def to_str
59+
[STANDARD_PACKET, @type].pack('CC') + @body
60+
end
61+
62+
# Compares this Message and another Message for equality
63+
#
64+
# @param other [Message] the Message to compare
65+
# @return [Boolean] true iff the two messages have equal types and bodies, false otherwise
66+
def ==(other)
67+
type == other.type && body == other.body
68+
end
69+
end
70+
end
71+
end
72+
end

lib/rex/proto/kademlia/ping.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# -*- coding: binary -*-
2+
3+
require 'rex/proto/kademlia/message'
4+
5+
module Rex
6+
module Proto
7+
module Kademlia
8+
# Opcode for a PING request
9+
PING = 0x60
10+
11+
# A Kademlia ping message.
12+
class Ping < Message
13+
def initialize
14+
super(PING)
15+
end
16+
end
17+
end
18+
end
19+
end

lib/rex/proto/kademlia/pong.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# -*- coding: binary -*-
2+
3+
require 'rex/proto/kademlia/message'
4+
5+
module Rex
6+
module Proto
7+
module Kademlia
8+
# Opcode for a PING response
9+
PONG = 0x61
10+
11+
# A Kademlia pong message.
12+
class Pong < Message
13+
# @return [Integer] the source port from which the PING was received
14+
attr_reader :port
15+
16+
def initialize(port = nil)
17+
super(PONG)
18+
@port = port
19+
end
20+
21+
# Builds a pong from given data
22+
#
23+
# @param data [String] the data to decode
24+
# @return [Pong] the pong if the data is valid, nil otherwise
25+
def self.from_data(data)
26+
message = super(data)
27+
return if message.type != PONG
28+
return if message.body.size != 2
29+
Pong.new(message.body.unpack('v')[0])
30+
end
31+
32+
# Get this Pong as a String
33+
#
34+
# @return [String] the string representation of this Pong
35+
def to_str
36+
super + [@port].pack('v')
37+
end
38+
end
39+
end
40+
end
41+
end

lib/rex/proto/kademlia/util.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# -*- coding: binary -*-
2+
3+
module Rex
4+
module Proto
5+
module Kademlia
6+
# Decodes an on-the-wire representation of a Kademlia peer to its 16-character hex equivalent
7+
#
8+
# @param bytes [String] the on-the-wire representation of a Kademlia peer
9+
# @return [String] the peer ID if valid, nil otherwise
10+
def self.decode_peer_id(bytes)
11+
peer_id = 0
12+
return nil unless bytes.size == 16
13+
bytes.unpack('VVVV').map { |p| peer_id = ((peer_id << 32) ^ p) }
14+
peer_id.to_s(16).upcase
15+
end
16+
17+
# TODO
18+
# def encode_peer_id(id)
19+
# end
20+
end
21+
end
22+
end
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
8+
class Metasploit3 < Msf::Auxiliary
9+
include Msf::Auxiliary::Report
10+
include Msf::Auxiliary::UDPScanner
11+
include Msf::Auxiliary::Kademlia
12+
13+
def initialize(info = {})
14+
super(
15+
update_info(
16+
info,
17+
'Name' => 'Gather Kademlia Server Information',
18+
'Description' => %q(
19+
This module uses the Kademlia BOOTSTRAP and PING messages to identify
20+
and extract information from Kademlia speaking UDP endpoints,
21+
typically belonging to eMule/eDonkey/BitTorrent servers or other P2P
22+
applications.
23+
),
24+
'Author' => 'Jon Hart <jon_hart[at]rapid7.com>',
25+
'References' =>
26+
[
27+
# There are lots of academic papers on the protocol but they tend to lack usable
28+
# protocol details. This is the best I've found
29+
['URL', 'http://gbmaster.wordpress.com/2013/06/16/botnets-surrounding-us-sending-kademlia2_bootstrap_req-kademlia2_hello_req-and-their-strict-cousins/#more-125']
30+
],
31+
'License' => MSF_LICENSE,
32+
'Actions' => [
33+
['BOOTSTRAP', 'Description' => 'Use a Kademlia2 BOOTSTRAP'],
34+
['PING', 'Description' => 'Use a Kademlia2 PING']
35+
],
36+
'DefaultAction' => 'BOOTSTRAP'
37+
)
38+
)
39+
40+
register_options(
41+
[
42+
Opt::RPORT(4672)
43+
], self.class)
44+
end
45+
46+
def build_probe
47+
@probe ||= case action.name
48+
when 'BOOTSTRAP'
49+
BootstrapRequest.new
50+
when 'PING'
51+
Ping.new
52+
end
53+
end
54+
55+
def scanner_process(response, src_host, src_port)
56+
return if response.blank?
57+
peer = "#{src_host}:#{src_port}"
58+
59+
case action.name
60+
when 'BOOTSTRAP'
61+
if bootstrap_res = BootstrapResponse.from_data(response)
62+
info = {
63+
peer_id: bootstrap_res.peer_id,
64+
tcp_port: bootstrap_res.tcp_port,
65+
version: bootstrap_res.version,
66+
peers: bootstrap_res.peers
67+
}
68+
print_good("#{peer} ID #{bootstrap_res.peer_id}, TCP port #{bootstrap_res.tcp_port}," +
69+
" version #{bootstrap_res.version}, #{bootstrap_res.peers.size} peers")
70+
end
71+
when 'PING'
72+
if pong = Pong.from_data(response)
73+
print_good("#{peer} PONG port #{pong.port}")
74+
# port should match the port we contacted it from. TODO: validate this?
75+
info = { udp_port: pong.port }
76+
end
77+
end
78+
79+
return unless info
80+
@results[src_host] ||= []
81+
@results[src_host] << info
82+
end
83+
84+
def scanner_postscan(_batch)
85+
@results.each_pair do |host, info|
86+
report_host(host: host)
87+
report_service(
88+
host: host,
89+
proto: 'udp',
90+
port: rport,
91+
name: 'kademlia',
92+
info: info
93+
)
94+
end
95+
end
96+
end

0 commit comments

Comments
 (0)