Intigriti monthly challenge Β· Sept 2026 Β· solved

Breaking the
Critter Gallery

This is my solution to the September 2026 Intigriti CTF. The whole challenge was one SQL injection hidden behind base64 encoding. Scroll through the steps below β€” every request shown is a real one I sent, with the real response the server gave back.

🎯 Union SQLi 🐘 PHP 8.2.33 πŸ—„οΈ MySQL 8.0.46 🧩 web CTF 🎨 challenge by @khanhdlq
solved & written byRYD3R-0 Β· github.com/RYD3R-0
Start with step 1 β†’
πŸ–±οΈ drag the background to spin the 3D scene β–Ά press SEND in any terminal to replay a request
01
Recon

A gallery that speaks base64

The target is /challenge.php β€” a small animal gallery with eight critters and no forms anywhere. Every tile links to itself with one parameter: ?pic=Zm94. Decode Zm94 and you get fox. So the server takes my value, base64-decodes it, and does something with it. That "something" is the whole challenge.

Worth remembering: base64 is encoding, not protection. It hides the value from a casual glance, nothing more. Try it:

BASE64 β€” type anything
↓ btoa()
try:

Sending the real value fox renders a detail card β€” emoji, name, description. So some server-side code looked that name up.

what this step proved The only attack surface is one decoded string, and the description on the page comes from a server-side lookup.
02
Error fingerprinting

One quote kills the page

First I checked the reflection: send <h1>test</h1> as the name and the page prints it back encoded β€” so no XSS there. But the value has to go somewhere. Next move: add a quote and see who complains.

The result was clean: fox' β†’ the page comes back completely empty. fox'' β†’ normal page. Odd quotes break it, even quotes survive. That's a SQL error with display_errors turned off β€” my input is landing inside a SQL string.

value sent (decoded)responsemeaning
fox'200 Β· 0 bytesodd quotes β†’ broken SQL β†’ PHP fatal
fox''200 Β· full pagebalanced quotes β†’ valid literal
fox'''200 Β· 0 bytesodd again β†’ dead
fox"200 Β· full pagedouble quote ignored β†’ single-quote context
what this step proved A blank page is still an answer. Quote parity is the quickest way to prove input is spliced into a query β€” no error text needed.
03
Confirmation

Asking for x, getting the fox

If the name really ends up in a SQL WHERE clause, I can change what the query returns. This payload closes the quote, adds OR '1'='1' (always true), and comments out the rest. I asked for a critter called x β€” the page showed me the fox, the first row in the table.

what this step proved SQL injection confirmed β€” and the description box prints whatever the query returns. That box is about to become my output channel.
04
UNION calibration

How many columns?

UNION SELECT appends my own row to the query's result, but the column count has to match the original query. So I swept it: UNION SELECT '1', then '1','2', and so on. The number of columns I can inject shows up right in the description box.

Here only one column works. Everything else crashes the query. One is all I need.

what this step proved The query returns a single column and renders it back to me. version() says MySQL 8.0.46.
05
Enumeration

Mapping the database

MySQL keeps a catalog of itself in information_schema, readable by default. With GROUP_CONCAT I can pull whole lists in a single request instead of one value at a time. Three questions: which database, which tables, which columns.

what this step proved Two tables: animals (the gallery data) and secret_vault with columns id and note. Not subtle. πŸ‘€
06
Extraction

Opening the secret_vault

One injectable column, one target table. Pick what the query should answer below and send it β€” the last option is the one that ends the CTF.

PAYLOAD BUILDER β€” what should the query say?
the whole solve in one line curl -s "https://challenge-0926.challenges.intigriti.io/challenge.php?pic=$(printf "%s" "zzz' UNION SELECT note FROM secret_vault -- -" | base64 -w0)" | grep -o 'INTIGRITI{[^}]*}'
07
Root cause

Why it worked, and the fix

The bug is one line: the decoded value was concatenated straight into the query. Base64 looked like a defense but wasn't one β€” decode is not sanitize. A prepared statement makes the whole thing impossible:

βœ• the bug (reconstructed)
// challenge.php
$name = base64_decode($_GET['pic']);

$sql = "SELECT description FROM animals
        WHERE name = '" . $name . "'";
// decoded β‰  safe. πŸ’₯
βœ“ the fix
$stmt = $pdo->prepare(
  "SELECT description FROM animals
   WHERE name = ?");
$stmt->execute([
  base64_decode($_GET['pic'])
]);
// + allow-list the 8 critter names
// + generic error page instead of blank 200
πŸ›‘οΈ

Prepared statements

Data stays data. No quote can change the shape of the query.

βœ…

Allow-list

Eight critters exist β€” accept those eight names, reject everything else.

πŸ”’

Fail properly

The blank 200 page was a free error oracle. Return a generic error, log the details.

🀫

Less fingerprints

X-Powered-By: PHP/8.2.33 tells attackers the stack for free.

submission summary Flag INTIGRITI{01a09f56-74a2-700b-a849-ffe6742327b2} Β· payload zzz' UNION SELECT note FROM secret_vault -- - (base64'd into ?pic=) Β· recon β†’ quote parity β†’ tautology β†’ 1-column UNION β†’ information_schema β†’ dump secret_vault.note.