CVE-2011-3192 Range header DoS vulnerability Apache HTTPD 1.3/2.x

数日前からFull Disclosureで騒がれてたけどやっとCVE採番されたので。
以前のISC BINDの脆弱性(CVE-2011-1910)とかに比べるとzero-day状態に
なったにも関わらずApache側の動きが遅い気もします。(表に見えてなかっただけ?)


アドバイザリは以下


DoSだけといってしまえばそれまでですが、Apache HTTPDでは久しぶりに

  • 現時点で出ている全バージョンが対象 (2011/09/03追記 1.3系は対象外)
  • 特に変わった設定していなくても影響を受ける (静的コンテンツ置いてるだけでもNG)
  • 攻撃コスト << 効果 (HEADリクエストで可能なのでトラフィックも殆ど必要ない)
  • HTTPDだけでは済まない可能性が高い
  • とても簡単

と、される側からすると嫌な感じ満載の脆弱性になっています。


HTTPへのDoSといえばかなり前に見つかっても、まだ対策してない所が大量に見られる
Slowlorisとかもあります。これはApache HTTPDのChildプロセス(スレッド)占有するだけなので、
MaxClientsまで達してHTTPでのアクセスが出来なくなるだけです。(だけというとアレですが)


一方今回のはプロセスの肥大化を伴うので、実メモリ消費して更にスワップ
使い尽くしてOS毎激重になったあげくLinuxとかの場合はOOM Killer発動と、他の
プロセスや場合によってはOSを巻き込んで逝ってしまいます。
そうでなくても限界までスワップアウトした場合は、処理が戻ってくるまで待ちきれなくて
サーバ毎リセット掛ける事が多いかもしれませんが。


メモリリークしてるわけではないので、1プロセスあたりは際限なく増えていく
のでは無く一定サイズで止まります。が、増えたプロセスサイズは減らず、
更にリクエストの度に毎回メモリアクセスもされるのでスワップアウトして
放置するわけにも行かずと行った感じで、サーバに対する効率的な攻撃に
なってしまっています。


該当箇所は多分この辺のループです。Rangeヘッダ処理はフィルタとして実装されて
おり複数の区間要求があった場合は multipart/byteranges として返されます。

@@httpd-2.2.19/modules/http/byterange_filter.c
AP_CORE_DECLARE_NONSTD(apr_status_t) ap_byterange_filter(ap_filter_t *f,
                                                         apr_bucket_brigade *bb)
...
    /* this brigade holds what we will be sending */
    bsend = apr_brigade_create(r->pool, c->bucket_alloc);

    while ((current = ap_getword(r->pool, &r->range, ','))
           && (rv = parse_byterange(current, clength, &range_start,
                                    &range_end))) {
        apr_bucket *e2;
        apr_bucket *ec;
...
        do {
            apr_bucket *foo;
            const char *str;
            apr_size_t len;

            if (apr_bucket_copy(ec, &foo) != APR_SUCCESS) {
                /* As above; this should not fail since the bucket has
                 * a known length, but just to be sure, this takes
                 * care of uncopyable buckets that do somehow manage
                 * to slip through.  */
                /* XXX: check for failure? */
                apr_bucket_read(ec, &str, &len, APR_BLOCK_READ);
                apr_bucket_copy(ec, &foo);
            }
            APR_BRIGADE_INSERT_TAIL(bsend, foo);
            ec = APR_BUCKET_NEXT(ec);
        } while (ec != e2);
    }


で、結局の所対策どうすれば良いというのは、今のところ1リクエストで複数の区間
要求するリクエストを拒否or無視するしかありません。
正式版でどう直してくるのか気になるところではあります。

## これはOKだし、普通のブラウザも発行する
GET /xxxx HTTP/1.1
Host: xxxx
Range: bytes=1000-2000
## こういうのは無視したい
GET /xxxx HTTP/1.1
Host: xxxx
Range: bytes=1000-2000,1000-2001,1000-2002,...

アドバイザリにも何種類か対策方法が載っており以下はそのうちの一つですが
一定以上の区間要求が来た場合のみ無かったことにしています。

## 6区間以上は無視してヘッダ削除(無かったことに)
SetEnvIf Range (,.*?){5,} bad-range=1
RequestHeader unset Range env=bad-range

複数の区間要求するRangeヘッダとか誰がこんなの使ってるのと云うと、Acrobat(Adobe) Reader Plugin
がこういうリクエストを送ってくる様です。様ですというのは手元のAdobe Reader Xでは、
前者の単一区間のリクエストしか観測出来ませんでした。
出すのを止めたのか何か条件が足りないのかは不明ですが。




===
抜けがあったので補足。
削る場合はRangeヘッダだけでは不十分でした。

## Rangeヘッダ用
SetEnvIf Range (,.*?){5,} bad-range=1
RequestHeader unset Range env=bad-range
## Request-Rangeヘッダ用
SetEnvIf Request-Range (,.*?){5,} bad-range=1
RequestHeader unset Request-Range env=bad-range

該当箇所はこの辺。互換性のために残ってる部分も対応が必要です。

@@httpd-2.2.19/modules/http/byterange_filter.c
    /* Check for Range request-header (HTTP/1.1) or Request-Range for
     * backwards-compatibility with second-draft Luotonen/Franks
     * byte-ranges (e.g. Netscape Navigator 2-3).
     *
     * We support this form, with Request-Range, and (farther down) we
     * send multipart/x-byteranges instead of multipart/byteranges for
     * Request-Range based requests to work around a bug in Netscape
     * Navigator 2-3 and MSIE 3.
     */

    if (!(range = apr_table_get(r->headers_in, "Range"))) {
        range = apr_table_get(r->headers_in, "Request-Range");
    }


更に2.0ではenvを用いたRequestHeaderディレクティブによるヘッダ削除が効かないようです。
header unset takes two arguments とか出た場合はダメです。
諦めてRequestHeader unset Rangeで丸ごと消してしまうか、Deny from env=xxxで
エラー返すなりになるんじゃ無いかと思います。
ソースの該当箇所は以下。

@@httpd-2.2.19/modules/metadata/mod_headers.c
    if (new->action == hdr_unset) {
        if (value) {
            if (envclause) {
                return "header unset takes two arguments";
            }
            envclause = value;
            value = NULL;
        }
    }
@@httpd-2.0.64/modules/metadata/mod_headers.c
    if (new->action == hdr_unset) {
        if (value)
            return "header unset takes two arguments";
    }


===
更に追記(2011/08/27)
アドバイザリが更新されてRequest-Rangeヘッダの考慮が追加されてます。


ソースコードリポジトリの修正を見る限り結構大幅に変えていますね。
まだ正式リリースされていないので変わるかもしれませんが、重複する区間
マージするようになっているみたいです。
たとえば以下のような区間要求の場合、修正の前後でレスポンスに差異が出ます。

## リクエスト
GET /xxxx HTTP/1.1
Host: xxxx
Range: bytes=100-200,150-300,500-600
## 2.2.19のレスポンス
##  被っていようが要求されたとおりの区間で返す
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=xxxxxxxxxx
...
--xxxxxxxxxx
Content-range: bytes 100-200/100000
...
--xxxxxxxxxx
Content-range: bytes 150-300/100000
...
--xxxxxxxxxx
Content-range: bytes 500-600/100000
...
--xxxxxxxxxx--
## trunk revision 1162297時点のコードでのレスポンス
##  連続または被っている区間はまとめられる (注意:この修正は2.2.20には採用されませんでした)
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=xxxxxxxxxx
...
--xxxxxxxxxx
Content-range: bytes 100-300/100000
...
--xxxxxxxxxx
Content-range: bytes 500-600/100000
...
--xxxxxxxxxx--

被っている区間も別々に返ってくることを想定して作られているアプリの場合は
破綻してしまいそうな気もします。そんな使い方するなってことかもしれませんが。


ともあれ当面はヘッダ削除対応で凌いでおいた方がトラブルが少ない感じがします。
内部だけではなく外部から見た動作も変わっているので、バージョンアップで対応する場合は
しっかりテストをした方が良いと思います。




====
2.2.20が出たので追記(2011/08/31)
Rangeヘッダ例にbytes=が抜けてた部分を修正。
trunkバージョンの動作説明に注意書きを追加。


====
1.3系対象外の旨を追記(2011/09/03)
アドバイザリUPDATE 3のドラフトより。
正式版出たらファイル消えそうなのでこのへんのコメント欄参照で。


====
2.2.21が出たので追記(2011/09/13)