Skip to content

Commit 0ed356f

Browse files
committed
Move Kademlia stuff to a more OO model, etc, per reviews
All of the work is done in rex. The msf mixin just prevents the desire to call rex directly from the module
1 parent e255db9 commit 0ed356f

File tree

17 files changed

+414
-262
lines changed

17 files changed

+414
-262
lines changed

lib/msf/core/auxiliary/kademlia.rb

Lines changed: 1 addition & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# -*- coding: binary -*-
2+
23
require 'rex/proto/kademlia'
34

45
module Msf
@@ -10,109 +11,5 @@ module Msf
1011
###
1112
module Auxiliary::Kademlia
1213
include Rex::Proto::Kademlia
13-
14-
# Opcode for a BOOTSTRAP request
15-
BOOTSTRAP_REQ = 0x01
16-
# Opcode for a BOOTSTRAP response
17-
BOOTSTRAP_RES = 0x09
18-
# Opcode for a PING request
19-
PING = 0x60
20-
# Opcode for a PING response
21-
PONG = 0x61
22-
# The minimum size of a peer in a KADEMLIA2_BOOTSTRAP_RES message:
23-
# peer ID (16-bytes), IP (4 bytes), UDP port (2 bytes), TCP port (2 bytes)
24-
# and version (1 byte)
25-
BOOTSTRAP_PEER_SIZE = 25
26-
27-
# Builds a BOOTSTRAP request
28-
#
29-
# @return [String] a BOOTSTRAP request
30-
def bootstrap
31-
Message.new(BOOTSTRAP_REQ)
32-
end
33-
34-
# Decodes a BOOTSTRAP response
35-
#
36-
# @param response [String] the response to decode
37-
# @return [Array] the discovered peer ID, TCP port, version and a list of peers
38-
# if the response if valid, nil otherwise
39-
def decode_bootstrap_res(response)
40-
message = Message.from_data(response)
41-
# abort if this isn't a valid response
42-
return nil unless message.type = BOOTSTRAP_RES
43-
return nil unless message.body.size >= 23
44-
peer_id = decode_peer_id(message.body.slice!(0,16))
45-
tcp_port, version, num_peers = message.body.slice!(0,5).unpack('vCv')
46-
# protocol says there are no peers and the body confirms this, so just return with no peers
47-
return [ tcp_port, version, []] if num_peers == 0 && message.body.blank?
48-
peers = decode_bootstrap_peers(message.body)
49-
# abort if the peer data was invalid
50-
return nil unless peers
51-
[ peer_id, tcp_port, version, peers ]
52-
end
53-
54-
# Builds a PING request
55-
#
56-
# @return [String] a PING request
57-
def ping
58-
Message.new(PING)
59-
end
60-
61-
# Decode a PING response, PONG
62-
#
63-
# @param response [String] the response to decode
64-
# @return [Integer] the source port from the PING response if the response is valid, nil otherwise
65-
def decode_pong(response)
66-
message = Message.from_data(response)
67-
# abort if this isn't a pong
68-
return nil unless message.type == PONG
69-
# abort if the response is too large/small
70-
return nil unless message.body && message.body.size == 2
71-
# this should always be equivalent to the source port from which the PING was received
72-
message.body.unpack('v')[0]
73-
end
74-
75-
# Decode a list of peers from a BOOTSTRAP response
76-
#
77-
# @param peers_data [String] the peers data from a BOOTSTRAP response
78-
# @return [Array] a list of the peers and their associated metadata extracted
79-
# from the response if valid, nil otherwise
80-
def decode_bootstrap_peers(peers_data)
81-
# sanity check total size
82-
return nil unless peers_data.size % BOOTSTRAP_PEER_SIZE == 0
83-
peers = []
84-
until peers_data.blank?
85-
peers << decode_bootstrap_peer(peers_data.slice!(0, BOOTSTRAP_PEER_SIZE))
86-
end
87-
peers
88-
end
89-
90-
# Decodes a single set of peer data from a BOOTSTRAP reseponse
91-
#
92-
# @param peer-data [String] the peer data for one peer from a BOOSTRAP response
93-
# @return [Array] the peer ID, IPv4 addresss, UDP port, TCP port and version of this peer
94-
def decode_bootstrap_peer(peer_data)
95-
# sanity check the size of this peer's data
96-
return nil unless peer_data.size == BOOTSTRAP_PEER_SIZE
97-
# TODO; interpret this properly
98-
peer_id = peer_data.slice!(0, 16)
99-
ip, udp_port, tcp_port, version = peer_data.unpack('VvvC')
100-
[ decode_peer_id(peer_id), Rex::Socket.addr_itoa(ip), udp_port, tcp_port, version ]
101-
end
102-
103-
# Decodes an on-the-wire representation of a Kademlia peer to its 16-character hex equivalent
104-
#
105-
# @param bytes [String] the on-the-wire representation of a Kademlia peer
106-
# @return [String] the peer ID if valid, nil otherwise
107-
def decode_peer_id(bytes)
108-
peer_id = 0
109-
return nil unless bytes.size == 16
110-
bytes.unpack('VVVV').map { |p| peer_id <<= 32; peer_id ^= p; }
111-
peer_id.to_s(16).upcase
112-
end
113-
114-
# TODO
115-
# def encode_peer_id(id)
116-
# end
11714
end
11815
end

lib/rex/proto/kademlia.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
# -*- coding: binary -*-
22

3+
require 'rex/proto/kademlia/bootstrap_request'
4+
require 'rex/proto/kademlia/bootstrap_response'
35
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: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
attr_reader :peer_id
15+
attr_reader :tcp_port
16+
attr_reader :version
17+
# An array of hashes containing the peer ID, IP address, UDP and TCP ports as well as the type/version
18+
attr_reader :peers
19+
20+
def initialize(peer_id, tcp_port, version, peers)
21+
@peer_id = peer_id
22+
@tcp_port = tcp_port
23+
@version = version
24+
@peers = peers
25+
end
26+
27+
# The minimum size of a peer in a KADEMLIA2_BOOTSTRAP_RES message:
28+
# peer ID (16-bytes), IP (4 bytes), UDP port (2 bytes), TCP port (2 bytes)
29+
# and version (1 byte)
30+
BOOTSTRAP_PEER_SIZE = 25
31+
32+
# Builds a bootstrap response from given data
33+
#
34+
# @param data [String] the data to decode
35+
# @return [BootstrapResponse] the bootstrap response if the data is valid, nil otherwise
36+
def self.from_data(data)
37+
message = Message.from_data(data)
38+
# abort if this isn't a valid response
39+
return unless message
40+
return unless message.type == BOOTSTRAP_RESPONSE
41+
return unless message.body.size >= 23
42+
bootstrap_peer_id = Rex::Proto::Kademlia.decode_peer_id(message.body.slice!(0, 16))
43+
bootstrap_tcp_port, bootstrap_version, num_peers = message.body.slice!(0, 5).unpack('vCv')
44+
# protocol says there are no peers and the body confirms this, so just return with no peers
45+
if num_peers == 0 && message.body.blank?
46+
peers = []
47+
else
48+
peers_data = message.body
49+
# peers data is too long/short, abort
50+
return if peers_data.size % BOOTSTRAP_PEER_SIZE != 0
51+
peers = []
52+
until peers_data.blank?
53+
peer_data = peers_data.slice!(0, BOOTSTRAP_PEER_SIZE)
54+
peer_id = Rex::Proto::Kademlia.decode_peer_id(peer_data.slice!(0, 16))
55+
ip, udp_port, tcp_port, version = peer_data.unpack('VvvC')
56+
peers << {
57+
id: peer_id,
58+
ip: Rex::Socket.addr_itoa(ip),
59+
tcp_port: tcp_port,
60+
udp_port: udp_port,
61+
version: version
62+
}
63+
end
64+
end
65+
BootstrapResponse.new(bootstrap_peer_id, bootstrap_tcp_port, bootstrap_version, peers)
66+
end
67+
end
68+
end
69+
end
70+
end

lib/rex/proto/kademlia/message.rb

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ module Proto
1717
#
1818
##
1919
module Kademlia
20-
# The header that non-compressed Kad messages use
21-
STANDARD_PACKET = 0xE4
22-
# The header that compressed Kad messages use, which is currently unsupported
23-
COMPRESSED_PACKET = 0xE5
24-
2520
class Message
21+
# The header that non-compressed Kad messages use
22+
STANDARD_PACKET = 0xE4
23+
# The header that compressed Kad messages use, which is currently unsupported
24+
COMPRESSED_PACKET = 0xE5
25+
2626
attr_accessor :type, :body
2727

2828
# @param type [String] the message type
@@ -36,7 +36,7 @@ def self.from_data(data)
3636
return if data.length < 2
3737
header, type = data.unpack('CC')
3838
if header == COMPRESSED_PACKET
39-
fail NotImplementedError, "Unable to handle #{message.length}-byte compressed Kademlia message"
39+
fail NotImplementedError, "Unable to handle #{data.length}-byte compressed Kademlia message"
4040
end
4141
return if header != STANDARD_PACKET
4242
Message.new(type, data[2, data.length])
@@ -45,6 +45,10 @@ def self.from_data(data)
4545
def to_str
4646
[STANDARD_PACKET, @type].pack('CC') + @body
4747
end
48+
49+
def ==(other)
50+
type == other.type && body == other.body
51+
end
4852
end
4953
end
5054
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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
# 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+
def self.from_data(data)
22+
message = super(data)
23+
return if message.type != PONG
24+
return if message.body.size != 2
25+
Pong.new(message.body.unpack('v')[0])
26+
end
27+
28+
def to_str
29+
super + [@port].pack('v')
30+
end
31+
end
32+
end
33+
end
34+
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

modules/auxiliary/scanner/kademlia/server_info.rb

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ def initialize(info = {})
4646
def build_probe
4747
@probe ||= case action.name
4848
when 'BOOTSTRAP'
49-
bootstrap
49+
BootstrapRequest.new
5050
when 'PING'
51-
ping
51+
Ping.new
5252
end
5353
end
5454

@@ -58,22 +58,22 @@ def scanner_process(response, src_host, src_port)
5858

5959
case action.name
6060
when 'BOOTSTRAP'
61-
peer_id, tcp_port, version, peers = decode_bootstrap_res(response)
62-
info = {
63-
peer_id: peer_id,
64-
tcp_port: tcp_port,
65-
version: version,
66-
peers: peers
67-
}
68-
if datastore['VERBOSE']
69-
else
70-
print_good("#{peer} ID #{peer_id}, TCP port #{tcp_port}, version #{version}, #{peers.size} peers")
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")
7170
end
7271
when 'PING'
73-
udp_port = decode_pong(response)
74-
print_good("#{peer} PONG")
75-
# udp_port should match the port we contacted it from. TODO: validate this?
76-
info = { udp_port: udp_port }
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
7777
end
7878

7979
return unless info

0 commit comments

Comments
 (0)