June 23, 2026
The Database That Couldn’t Keep a Secret
I asked a MySQL server one rude question. It answered with its full name, version, and operating system — in front of everyone

By morgan_hack
6 min read
PortSwigger SQL Injection Series — Lab 4 of the run. We've climbed steadily: Lab 1, pulling hidden data out of a WHERE clause. Lab 2, walking past a login without a password. Lab 3, making an Oracle database confess its version through a UNION. Today is Lab 4 — the same version-confession move, but against MySQL. One technique, a new dialect. If you followed the Oracle write-up, you already know 90% of this. Watch what changes.
Yesterday, an Oracle database told me its version.
Today, I went back for the sequel — and made MySQL talk too.
Same con. New accent.
That's the trick this part of the series drills into you: it isn't ten attacks, it's one attack wearing ten costumes. Learn the skeleton once. Then you just swap the grammar for whichever database is hiding behind the website. Oracle yesterday. MySQL today. The move never changes — only the words do.
Here's how it falls, with zero hints, the way a real tester walks in.
The bug in plain words
Picture a website as a clerk who takes your request and walks to the back room to ask the database.
You click "Lifestyle." The clerk writes a note: "Bring me every product where the category is Lifestyle," and slides it through the window.
SQL injection (SQLi) is what happens when the clerk is dumb enough to copy your words onto the note exactly as you say them. So instead of saying "Lifestyle," you say "Lifestyle — oh, and also bring me the keys to the vault." And the clerk, never looking up, writes the whole thing down and walks to the back.
The magic word today is UNION. In SQL, UNION welds the results of a second question onto the first. If the clerk copies your words, you slip your own question in behind theirs — and the answer comes back out the same window, sitting right next to the products.
What I Noticed
I wasn't hacking. I was shopping.
Clicking through the store like any bored customer — Gifts, Lifestyle, Food & Drink. And every click left a fingerprint in the address bar:
/filter?category=Gifts/filter?category=Gifts
There it is. category is my word, going somewhere into the back room. And whenever I see my own input ride into a database, I do the rudest possible thing first — I jam a single quote into it and listen for the crash.
I sent it through Burp Repeater (a tool that lets me fire the same web request again and again, tweaking one character at a time):
/filter?category=Gifts'/filter?category=Gifts'The page didn't blink. It collapsed:
HTTP/2 500 Internal Server ErrorHTTP/2 500 Internal Server ErrorOne apostrophe should never knock over a website. This one did — because my quote didn't land in a search box. It landed inside the database's sentence and snapped its grammar in half.
That 500 isn't an error. It's an unlocked door swinging open.
The action
Step 1 — count the seats at the table.
A UNION is fussy. My smuggled question must return the exact same number of columns as the original, or the whole thing rejects. So before anything clever, I count.
ORDER BY n means "sort by column number n." Ask for a column that doesn't exist, and the database snaps back with an error. So I climb the stairs one at a time:
/filter?category=Lifestyle' ORDER BY 1-- -
/filter?category=Lifestyle' ORDER BY 2-- -
/filter?category=Lifestyle' ORDER BY 3-- -/filter?category=Lifestyle' ORDER BY 1-- -
/filter?category=Lifestyle' ORDER BY 2-- -
/filter?category=Lifestyle' ORDER BY 3-- -(That -- - on the end is a SQL comment — it tells the database "ignore everything after this," so the leftovers of the original sentence don't trip me. MySQL is picky: the comment needs a trailing space, which is why it's dash-dash-space-dash. Miss the space and you'll chase a ghost for an hour.)
The results drew me a map:
ORDER BY 2→ page loads, calm as ever.ORDER BY 3→ 500.
There's no third column. So the table has exactly 2 columns. That crash wasn't failure — it was the database answering my question without meaning to.
Step 2 — make sure my fake question fits.
Now I send a UNION with exactly two columns. And here's the dialect twist from the Oracle lab: Oracle is precious — it refuses a SELECT with no table, so over there I had to bolt on FROM dual. MySQL doesn't care. A bare SELECT is perfectly happy. No FROM needed.
/filter?category=Gifts' UNION SELECT NULL,NULL-- -/filter?category=Gifts' UNION SELECT NULL,NULL-- -
(NULL is just an empty seat — it fits any column type, so nothing clashes while I'm still feeling out the shape.)
Page loads clean. My question fits the window perfectly.
Step 3 — ask the rude question.
MySQL keeps its version stamped in a built-in tag called @@version. I drop that into the first seat and send it:
/filter?category=Gifts' UNION SELECT @@version,NULL --/filter?category=Gifts' UNION SELECT @@version,NULL --What fell out
Where a tidy little category name should have been, the page now read:
8.0.42-0ubuntu0.20.04.18.0.42-0ubuntu0.20.04.1I'll translate. The database had just announced, to a stranger, in public: "I am MySQL 8.0.42, and I live on Ubuntu 20.04."
Lab solved.
It looks like a boring string of numbers. It is the opposite of boring. A version number is a treasure map — it tells an attacker exactly which known weaknesses fit this exact build. And more than that, it's proof: if I can make the database hand me its own name, I can make it hand me anything it's holding.
Why This Works
The root cause is one tired shortcut: the app builds its SQL by string concatenation — literally pasting my words into the sentence, like this:
SELECT name, price FROM products WHERE category = 'INPUT_HERE'SELECT name, price FROM products WHERE category = 'INPUT_HERE'When INPUT_HERE becomes Lifestyle' UNION SELECT @@version,NULL-- -, the database doesn't see a shopping category. It sees a brand-new command and dutifully runs it.
Why do developers keep doing this? Because gluing strings together is the obvious way to build a sentence, and it works flawlessly in testing — no normal customer types an apostrophe when they click "Lifestyle." The real fix (parameterised queries — a method that locks your input as data forever, never code) is one extra line and a habit nobody bothered to teach. So the shortcut ships. And it ships into half the internet.
Real Bug Bounty Payouts
If this still feels like a toy, look at the receipts. Asking a database its type and version is step one of nearly every serious SQLi report ever filed:
- Starbucks — $4,000 (HackerOne). A researcher found a time-based SQL injection, handed it to sqlmap, and the tool spat back the version: Microsoft SQL Server 2012. Behind that one injection sat nearly a million records of real accounting data. The exact "make the database say its name" move you just read — pointed at a global coffee empire.
- Valve / Steam — $25,000 (HackerOne). A SQL injection through a
countryFilter[]parameter in a reporting page. A filter dropdown — the same species of input as the category filter I just cracked — became a door into one of the biggest platforms in gaming. - Mail.ru — $15,000 (HackerOne). A time-based blind SQL injection on their ride-hailing service. Nothing ever printed on the page here — the database leaked its secrets through timing instead. That's today's trick's meaner, quieter cousin.
- Zomato — $1,000 (HackerOne). A UNION-based injection that also had to slip past a WAF (Web Application Firewall — a bouncer that blocks obvious-looking payloads). Same
UNION SELECTI used, with the real-world catch of having to dodge the doorman.
From a grand to twenty-five, every one of them started where I started today: a single parameter that trusted what it was told.
Where to Hunt This in Real Apps
The field checklist for the next live target:
- Go for inputs that fetch or filter data — category, search, sort,
id=lookups, anything that returns a list. Those talk straight to the database. - Throw the single quote first. A 500, a blank page, a result that suddenly changes — any of those is a tell.
- Nothing changed? Go boolean. Pit
' AND 1=1-- -(true) against' AND 1=2-- -(false). Different responses mean injection, even when errors are hidden. - Count columns with
ORDER BYbefore you reach for UNION. The number that finally errors, minus one, is your width. - Let the crash name the engine.
ORA-screams Oracle; a complaint aboutFROM, or@@versionworking, points at MySQL or SQL Server. The error text fingerprints the database for free. - Then ask for the version in the right accent —
@@version(MySQL/MSSQL) orbanner FROM v$version(Oracle). - Confirm by hand, then call sqlmap. Real targets sit behind WAFs that flag sqlmap's noisy defaults the second it opens its mouth. Prove the bug manually, then automate for the report.
- Never forget the business impact. Version disclosure leads to targeted exploits. A full dump leads to breach-response bills, GDPR-class fines, and customers who never come back. One careless filter can end as a server takeover.