How a Fox and a Single Quote Broke the Critter Gallery — SQLi in Intigriti Challenge 0926
My write-up for Intigriti Challenge 0926: a cute animal gallery hiding a textbook SQL injection behind a base64 ?pic= parameter. Solved 27/09/2026, submission INTIGRITI-TAPV7AA2 accepted.
The challenge
The Critter Gallery shows one animal per page. The animal is picked via a base64-encoded ?pic= query parameter that decodes to the critter's name, e.g. ?pic=Rk9Y → FOX. Beneath each picture sits a short description pulled from a database. Somewhere on the server, a second table — secret_vault — holds the flag.
The official tip: "the gallery speaks different languages" — the same input is processed differently by two layers of the stack.
First look: two code paths, one input
Requesting FOX vs Fox was the first tell. Both returned the fox description, but the ASCII art differed — one fell back to a default drawing. So the image lookup and the description lookup clearly don't share the same comparison logic: the art is matched one way (PHP, exact/case-sensitive), the description another (MySQL, case-insensitive collation with fullwidth folding). Two languages, exactly as the tip promised.
That mismatch told me the description path talks directly to SQL — worth poking.
Finding the injection: the blank page
Classic quote test, base64-wrapped:
-
'→ blank white page (zero-length response) -
\→ blank page too -
"→ normal page, unaffected
A single quote killing the page while a double quote does nothing means the input lands inside a single-quoted SQL string. The backslash breaking it too confirms the quote isn't being escaped — it's raw string concatenation.
Confirming output: dumping all descriptions
Next, a boolean break-out to prove the query result reaches the page:
?pic=JyBPUiAnMSc9JzE= # base64(' OR '1'='1)
The description box printed all 8 rows instead of one. Injection confirmed, and the output lands in div.desc.
Column count: UNION needs exactly one
UNION-based extraction needs the right column count. Probing ORDER BY / UNION with 1..n columns showed the query selects a single column (SELECT desc ...), and UNION output renders in the same description div.
Fingerprinting via UNION:
?pic=JyBVTklPTiBTRUxFQ1QgQEB2ZXJzaW9uIC0tIC0= # base64(' UNION SELECT @@version -- -)
→ MySQL 8.0.46, database critter_gallery, tables animals and secret_vault.
The vault
secret_vault has two columns: id, note. One shot to dump it:
zzz' UNION SELECT GROUP_CONCAT(id,'~',note SEPARATOR '|') FROM secret_vault -- -
?pic=enp6JyBVTklPTiBTRUxFQ1QgR1JPVVBfQ09OQ0FUKGlkLCd+Jyxub3RlIFNFUEFSQVRPUiAnfCcpIEZST00gc2VjcmV0X3ZhdWx0IC0tIC0=
The description box returned:
1~INTIGRITI{01a09f56-74a2-700b-a849-ffe6742327b2}
(flag as rendered; submitted value INTIGRITI{01a09f56-74a2-700b-a849-ffe6742327b2}, accepted ✅)
Lessons
-
Behavioral diffs are the real tip. The
FOX/Foxart mismatch revealed two comparison semantics before I sent a single quote. When one input gets two treatments, one of them is usually injectable. -
A blank page is an answer. Zero-length responses on
'and\(but not") fingerprint a single-quoted context with no escaping — no error message needed. -
Dump small, then go wide.
' OR '1'='1(8 rows in the desc box) proved output control before I spent requests on schema enumeration. - Know your collations. PHP exact-match vs MySQL case-insensitive/fullwidth-folding comparison is a classic desync primitive — it shows up in auth bypasses too, not just galleries.
Thanks to the Intigriti team for a fun challenge. Full probe trail (14 rounds, from recon to flag) is documented in my notes.
Top comments (0)