The Challenge
“Segnalazione Cinghiali Cittadini” — Citizens’ Wild Boar Reporting — is a Node.js/TypeScript Express web application backed by SQLite. The app lets users submit boar sightings and view them at /report/:id. Source and a Docker environment are provided.
Looking at init.sql, the database has at least two tables: one for reports and one for the flag. The flag table isn’t exposed through any normal UI flow, but the /report/:id route directly interpolates the :id parameter into a SQL query with no sanitization.
Approach
The first thing I did after spinning up the Docker environment was read the route handler. The query looked roughly like:
|
|
No prepared statement, just string interpolation. SQLite supports UNION SELECT, and I can query sqlite_master to enumerate tables and columns — which confirms the flag table’s name and schema.
Since this is a SQLite app, sqlite_master is the information schema equivalent:
|
|
Once I had the table name (flag) and column name (flag), it was a one-shot union injection to extract the value.
The tricky part in practice is counting columns: the reports table query returns a fixed number of columns, and the UNION query must match that count. I used NULL padding to probe the column count first, incrementally adding NULLs until the union didn’t error. Once the count was right, I swapped one NULL for flag from the flag table.
Solution
All done manually through the browser or curl. The injection payloads:
Probe column count (adjust until no error):
|
|
Enumerate tables:
|
|
This reveals the flag table and its schema.
Extract the flag:
|
|
The flag appears in the first field of the returned report. The id=0 trick ensures no real report rows match (so the response contains only the injected row), making the output unambiguous.
The Node.js Express app delivers the result as JSON or HTML depending on the Accept header — either way the flag value shows up in the response body.
What I Learned
Source-code access makes SQL injection trivial to exploit: you read the query, count the columns, and write the payload. The interesting part was the reminder that modern web frameworks don’t prevent SQL injection by default — the developer has to explicitly use parameterized queries. ORM doesn’t save you if you drop to raw SQL.