クッキーインジェクションを試したけど再現できなかった


追試しました クッキーインジェクションを試したけど再現できなかった(2)

ももいろテクノロジー「Cookie InjectionによるHTTPSハイジャックについて調べてみる」

クッキーの仕様を逆手に取ったなかなか興味深い攻撃方法があるよと会社の人から先週教えてもらったので試してみたんだけど、再現できなくて、ブラウザですでに対応されたっぽいね、というのを確認したのでメモ。

クッキーインジェクションとは

リンク先が詳しいのですが、手短に書くと、HTTPSで保護されたページ用のsecureなクッキーに対して、同一スコープのドメインがあるとHTTPなサブドメインから上書きできちゃったり(Cookie Overwriting)、ドメイン指定の優先度の高いクッキーを使うことでHTTPSなサイトのクッキーを隠す(Cookie Shadowing)可能性があるよ、という問題。HTTPのサイトからHTTPSのクッキーを上書きなり隠蔽できてしまうということは、HTTPSの防御を崩していることになります。

今確認したけど、プロフェッショナルSSL/TLSの5.3でも詳しく紹介されていますね。

外部からクッキーが設定できると、ログイン前後でセッショントークンを変更しない(ユーザーが持っているセッショントークンに後から権限付与みたいな)しょぼ実装サーバーに対してセッションID固定化攻撃が成功しちゃいます。攻撃者が用意したセッショントークンのIDを、正規ユーザーにインジェクションして、正規ユーザーがその後にログイン操作をすると、そのセッショントークンIDが正規ユーザーの権限になってしまうスンポー。

本当にそれはありえるの?という疑問

最初に聞いた時は「おおー、これは鮮やかな手口」と思ったけど、サブドメインとパスのどっちを優先するかはRFCには書かれてない(ブラウザがクッキーを処理するときはパスが長い詳細な方を先にソートしよう、ぐらいしか見当たらなかった)ので、これが成功するかどうかはブラウザの実装次第なのかな、と疑問が湧いてきました。 普通に考えると、コンテンツをツリー構造として考えると、サブドメインの方が先にある(上位の)分類な気がするので、パスが優先されるのはおかしいような・・・でもそういう実装があるということなのかな・・・。分からないので試してみました。

実験

複数のドメインを用意するのはめんどいので、/etc/hostsファイルで済ませた。

127.0.0.1 foo.example.com
127.0.0.1 bar.example.com

ブログの記事だと、次のサンプルでevilを設定するところはMITM攻撃で作られたページでHTTPを使い、正規のページがHTTPSということになっていますが、両方共HTTPにしています。

Overwritingの再現実験

ハンドラを2つ用意します。同じサーバーに実装しちゃっているけど、一応ドメイン違いでアクセスされるイメージ。

package main

import (
    "fmt"
    "net/http"
    "net/http/httputil"
)

func main() {
    var httpServer http.Server
    // foo.example.com
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        dump, err := httputil.DumpRequest(r, true)
        if err != nil {
            http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
            return
        }
        fmt.Println(string(dump))
        http.SetCookie(w, &http.Cookie{Name: "test-cookie", Value: "secret", Path: "/", Domain: "example.com", MaxAge: 1000})
        fmt.Fprintf(w, "<html><body>hello</body></html>\n")
    })
    // bar.example.com/sub
    http.HandleFunc("/sub", func(w http.ResponseWriter, r *http.Request) {
        dump, err := httputil.DumpRequest(r, true)
        if err != nil {
            http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
            return
        }
        fmt.Println(string(dump))
        http.SetCookie(w, &http.Cookie{Name: "test-cookie", Value: "evil", Path: "/sub", Domain: "example.com", MaxAge: 1000})
        fmt.Fprintf(w, "<html><body>sub</body></html>\n")
    })

    fmt.Println("start http listening :80")
    httpServer.Addr = ":80"
    fmt.Println(httpServer.ListenAndServe())
}

80はwell known portなので、実行にはsudoが必要だったりします。

$ sudo go run cookie_injection.go

期待する動作は次の通り。

  • http://foo.example.comにアクセス(test-cookieというキーがsecretになる)
  • http://bar.example.com/subにアクセス(test-cookieというキーがevilになる)
  • http://foo.example.comにアクセスしたときに、クッキーの値(subで設定したevilがサーバーに送信される)

でしたが、ChromeでもFirefoxでも最後のステップでevilになりませんでした。開発者ツールで見ても、fooの時はbarで広域設定されたクッキーは無視されているっぽい。

Shadowingの再現実験

最初の実験の最初のハンドラのドメインを"example.com"から"foo.example.com"に変えればいいはず。

http.SetCookie(w, &http.Cookie{Name: "test-cookie", Value: "secret", Path: "/", Domain: "foo.example.com", MaxAge: 1000})

期待する動作は同じです。

  • http://foo.example.comにアクセス(test-cookieというキーがsecretになる)
  • http://foo.example.com/subにアクセス(test-cookieというキーがevilになる)
  • http://foo.example.comにアクセスしたときに、クッキーの値(subで設定したevilがサーバーに送信される)

でしたが、ChromeでもFirefoxでも最後のステップでevilになりませんでした。その後 /sub にアクセスすると、 Cookie: test-cookie=evil; test-cookie=secret となっているので、両方認識はされていて、たしかにパスの詳細順になっていますが、ルートのパスにアクセスするとやっぱりsecretだけになっています。開発者ツールで見ても、ルートのパスの時は/subのクッキーが見えないですね。ドメインまたぎで設定できないように動作が変わったのかも。

まとめ

ブラウザベンダーすっごーーい!君はセキュリティが得意なフレンズなんだね!