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:
Sending the real value fox renders a detail card β emoji, name, description.
So some server-side code looked that name up.
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.
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.
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.
version() says MySQL 8.0.46.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.
animals (the gallery data) and secret_vault with columns id and note. Not subtle. π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.
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{[^}]*}'
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:
// challenge.php $name = base64_decode($_GET['pic']); $sql = "SELECT description FROM animals WHERE name = '" . $name . "'"; // decoded β safe. π₯
$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.
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.