Skip to content

Commit 80efd19

Browse files
committed
Add a test module for UDP and TCP channels
1 parent 47f60e1 commit 80efd19

File tree

1 file changed

+366
-0
lines changed

1 file changed

+366
-0
lines changed
Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
lib = File.join(Msf::Config.install_root, "test", "lib")
2+
$LOAD_PATH.push(lib) unless $LOAD_PATH.include?(lib)
3+
require 'module_test'
4+
5+
class MetasploitModule < Msf::Post
6+
7+
include Msf::Exploit::Retry
8+
include Msf::ModuleTest::PostTest
9+
10+
def initialize(info = {})
11+
super(
12+
update_info(
13+
info,
14+
'Name' => 'Socket Channel Tests',
15+
'Description' => %q{ This module will socket channels },
16+
'License' => MSF_LICENSE,
17+
'Author' => [ 'Spencer McIntyre' ],
18+
'Platform' => [ 'linux', 'osx', 'windows' ],
19+
'SessionTypes' => [ 'shell', 'meterpreter' ] # SSH sessions are reported as 'shell'
20+
)
21+
)
22+
end
23+
24+
def run
25+
if session.type == 'shell' && !session.is_a?(Msf::Sessions::SshCommandShellBind)
26+
print_error("Session #{datastore["SESSION"]} is a shell session.")
27+
print_error('Only SSH shell sessions support socket channels.')
28+
return
29+
end
30+
31+
super
32+
end
33+
34+
def tcp_client_socket_pair(params={}, timeout: 5)
35+
params = Rex::Socket::Parameters.new('Proto' => 'tcp', 'PeerHost' => '127.0.0.1', **params)
36+
37+
server = TCPSocketServer.new(host: params.peerhost, port: params.peerport)
38+
params.peerport = server.port
39+
client = session.create(params)
40+
server_client = server.start(timeout: timeout)
41+
server.stop
42+
[client, server_client]
43+
end
44+
45+
def tcp_server_socket_trio(params={}, timeout: 5)
46+
params = Rex::Socket::Parameters.new('Proto' => 'tcp', 'LocalHost' => '127.0.0.1', 'Server' => true, **params)
47+
48+
server = session.create(params)
49+
client_connector = TCPSocketClient.new(host: server.params.localhost, port: server.params.localport)
50+
client = client_connector.start(timeout: timeout)
51+
server_client = server.accept
52+
client_connector.stop
53+
54+
[client, server_client, server]
55+
end
56+
57+
def tcp_server_socket_pair(params={}, timeout: 5)
58+
client, server_client, server = tcp_server_socket_trio(params, timeout: timeout)
59+
server.close
60+
[client, server_client]
61+
end
62+
63+
def udp_socket_pair(params={})
64+
params = Rex::Socket::Parameters.new('Proto' => 'udp', 'PeerHost' => '127.0.0.1', **params)
65+
66+
server = UDPSocket.new
67+
server.bind(params.peerhost, params.peerport)
68+
params.peerport = server.addr[1] if params.peerport == 0
69+
client = session.create(params)
70+
[client, server]
71+
end
72+
73+
def test_tcp_client_channel
74+
print_status('Running TCP client channel tests...')
75+
76+
it '[TCP-Client] Allows binding to port 0' do
77+
# if this fails all the other tests will fail
78+
# it's critical that we allow the OS to pick the port and that it's sent back to Metasploit
79+
client, server_client, server = tcp_client_socket_pair({'LocalPort' => 0})
80+
ret = client.localport != 0
81+
client.close
82+
server_client.close
83+
ret
84+
end
85+
86+
it '[TCP-Client] Has the correct peer information' do
87+
client, server_client = tcp_client_socket_pair
88+
address = server_client.local_address
89+
ret = client.peerhost == address.ip_address && client.peerport == address.ip_port
90+
client.close
91+
server_client.close
92+
ret
93+
end
94+
95+
it '[TCP-Client] Receives data from the peer' do
96+
client, server_client = tcp_client_socket_pair
97+
data = Random.new.bytes(rand(10..100))
98+
server_client.write(data)
99+
ret = client.read(data.length) == data
100+
client.close
101+
server_client.close
102+
ret
103+
end
104+
105+
it '[TCP-Client] Sends data to the peer' do
106+
client, server_client = tcp_client_socket_pair
107+
data = Random.new.bytes(rand(10..100))
108+
client.write(data)
109+
ret = server_client.read(data.length) == data
110+
client.close
111+
server_client.close
112+
ret
113+
end
114+
115+
it '[TCP-Client] Propagates close events to the peer' do
116+
client, server_client = tcp_client_socket_pair
117+
client.close
118+
ret = retry_until_truthy(timeout: 5) { server_client.eof? }
119+
server_client.close
120+
ret
121+
end
122+
123+
it '[TCP-Client] Propagates close events from the peer' do
124+
client, server_client = tcp_client_socket_pair
125+
server_client.close
126+
# this behavior is wrong, it should just be an EOF, but when the channel is cleaned up, the socket is closed
127+
# this is how it's worked for years, so we'll test that it's still consistently wrong
128+
retry_until_truthy(timeout: 5) { client.closed? }
129+
end
130+
end
131+
132+
def test_tcp_server_channel
133+
print_status('Running TCP server channel tests...')
134+
135+
it '[TCP-Server] Allows binding to port 0' do
136+
# if this fails all the other tests will fail
137+
# it's critical that we allow the OS to pick the port and that it's sent back to Metasploit
138+
client, server_client, server = tcp_server_socket_trio({'LocalPort' => 0})
139+
ret = server.params.localport != 0
140+
server.close
141+
client.close
142+
server_client.close
143+
ret
144+
end
145+
146+
it '[TCP-Server] Accepts a connection' do
147+
client, server_client = tcp_server_socket_pair
148+
ret = !server_client.nil?
149+
server_client&.close
150+
client&.close
151+
ret
152+
end
153+
154+
it '[TCP-Server] Has the correct peer information' do
155+
client, server_client = tcp_server_socket_pair
156+
address = client.local_address
157+
ret = server_client.peerhost == address.ip_address && server_client.peerport == address.ip_port
158+
server_client.close
159+
client.close
160+
ret
161+
end
162+
163+
it '[TCP-Server] Receives data from the peer' do
164+
client, server_client = tcp_server_socket_pair
165+
data = Random.new.bytes(rand(10..100))
166+
client.write(data)
167+
ret = server_client.read(data.length) == data
168+
server_client.close
169+
client.close
170+
ret
171+
end
172+
173+
it '[TCP-Server] Sends data to the peer' do
174+
client, server_client = tcp_server_socket_pair
175+
data = Random.new.bytes(rand(10..100))
176+
server_client.write(data)
177+
ret = client.read(data.length) == data
178+
server_client.close
179+
client.close
180+
ret
181+
end
182+
183+
it '[TCP-Server] Propagates close events to the server' do
184+
client, server_client, server = tcp_server_socket_trio
185+
server.close
186+
187+
# Try to connect a new client - should fail since server is closed
188+
ret = retry_until_truthy(timeout: 5) do
189+
begin
190+
new_client = TCPSocket.new(server.params.localhost, server.params.localport)
191+
new_client.close
192+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET
193+
true
194+
else
195+
false
196+
end
197+
end
198+
199+
server_client.close
200+
client.close
201+
ret
202+
end
203+
204+
it '[TCP-Server] Propagates close events to the peer' do
205+
client, server_client = tcp_server_socket_pair
206+
server_client.close
207+
ret = retry_until_truthy(timeout: 5) { client.eof? }
208+
client.close
209+
ret
210+
end
211+
212+
it '[TCP-Server] Propagates close events from the peer' do
213+
client, server_client = tcp_server_socket_pair
214+
client.close
215+
# this behavior is wrong, it should just be an EOF, but when the channel is cleaned up, the socket is closed
216+
# this is how it's worked for years, so we'll test that it's still consistently wrong
217+
ret = retry_until_truthy(timeout: 5) { server_client.closed? }
218+
ret
219+
end
220+
end
221+
222+
def test_udp_channel
223+
if session.is_a?(Msf::Sessions::SshCommandShellBind)
224+
print_warning('UDP channels are not supported by SSH sessions.')
225+
return
226+
end
227+
228+
print_status('Running UDP channel tests...')
229+
230+
it '[UDP] Allows binding to port 0' do
231+
# if this fails all the other tests will fail
232+
# it's critical that we allow the OS to pick the port and that it's sent back to Metasploit
233+
client, server_client = udp_socket_pair({'LocalPort' => 0})
234+
ret = client.localport != 0
235+
client.close
236+
server_client.close
237+
ret
238+
end
239+
240+
it '[UDP] Has the correct peer information' do
241+
# this one is expected to fail because #recvfrom just returns a string address which is inconsistent
242+
client, server_client = udp_socket_pair
243+
data = Random.new.bytes(rand(10..100))
244+
# Now server can send to the client's address
245+
server_client.send(data, 0, client.localhost, client.localport)
246+
_, addrinfo = client.recvfrom(data.length)
247+
ret = addrinfo.is_a?(Array)
248+
ret &&= addrinfo[0] == 'AF_INET'
249+
ret &&= addrinfo[1] == server_client.local_address.ip_port
250+
ret &&= addrinfo[3] == server_client.local_address.ip_address
251+
client.close
252+
server_client.close
253+
ret
254+
end
255+
256+
it '[UDP] Receives data from the peer' do
257+
client, server_client = udp_socket_pair
258+
data = Random.new.bytes(rand(10..100))
259+
server_client.send(data, 0, client.localhost, client.localport)
260+
received, _ = client.recvfrom(data.length)
261+
ret = received == data
262+
client.close
263+
server_client.close
264+
ret
265+
end
266+
267+
it '[UDP] Sends data to the peer' do
268+
client, server_client = udp_socket_pair
269+
data = Random.new.bytes(rand(10..100))
270+
client.send(data, 0, server_client.local_address.ip_address, server_client.local_address.ip_port)
271+
received, _ = server_client.recvfrom(data.length)
272+
ret = received == data
273+
client.close
274+
server_client.close
275+
ret
276+
end
277+
end
278+
279+
class TCPSocketServer
280+
attr_reader :host, :port, :client_socket
281+
282+
def initialize(host: '127.0.0.1', port: 0)
283+
@host = host
284+
@server = TCPServer.new(host, port)
285+
@port = @server.addr[1]
286+
@client_socket = nil
287+
@mutex = Mutex.new
288+
@cv = ConditionVariable.new
289+
@error = nil
290+
end
291+
292+
def start(timeout: 5)
293+
@thread = Thread.new do
294+
begin
295+
@client_socket = @server.accept
296+
@mutex.synchronize { @cv.signal }
297+
rescue => e
298+
@mutex.synchronize do
299+
@error = e
300+
@cv.signal
301+
end
302+
end
303+
end
304+
305+
# Wait for connection with timeout
306+
@mutex.synchronize do
307+
unless @cv.wait(@mutex, timeout)
308+
@thread.kill
309+
raise "Timeout waiting for client connection after #{timeout}s"
310+
end
311+
312+
raise @error if @error
313+
end
314+
315+
@client_socket
316+
end
317+
318+
def stop
319+
@server.close
320+
@thread&.join(1)
321+
end
322+
end
323+
324+
class TCPSocketClient
325+
attr_reader :host, :port, :server_socket
326+
327+
def initialize(host:, port:)
328+
@host = host
329+
@port = port
330+
@server_socket = nil
331+
@mutex = Mutex.new
332+
@cv = ConditionVariable.new
333+
@error = nil
334+
end
335+
336+
def start(timeout: 5)
337+
@thread = Thread.new do
338+
begin
339+
@server_socket = TCPSocket.new(@host, @port)
340+
@mutex.synchronize { @cv.signal }
341+
rescue => e
342+
@mutex.synchronize do
343+
@error = e
344+
@cv.signal
345+
end
346+
end
347+
end
348+
349+
# Wait for connection with timeout
350+
@mutex.synchronize do
351+
unless @cv.wait(@mutex, timeout)
352+
@thread.kill
353+
raise "Timeout waiting for client connection after #{timeout}s"
354+
end
355+
356+
raise @error if @error
357+
end
358+
359+
@server_socket
360+
end
361+
362+
def stop
363+
@thread&.join(1)
364+
end
365+
end
366+
end

0 commit comments

Comments
 (0)