blog

CVE-についての簡単な雑談

これだけ多くの人が回しているところを見ると、すごいエクスプロイトに違いないので、クリックしてみると...。 これはGrafanaのバージョン3.0.1-7.0.1に関わるssrfの不正な脆弱性です。以...

Nov 11, 2020 · 3 min. read
シェア

今日はいつものように、2時間だけ早く出勤しました。

まだ出勤時間には早いので、今日はどの幸運なお偉方を拝んでいるのだろうと思いながらツイッターを閲覧していると、いつの間にか多くの人がCVE-2020-13379脆弱性に関するツイートをリツイートしていることに気づきました。

これだけ多くの人が回しているのを見ると、すごい抜け道に違いないと思い、クリックしてみると......。

これは、バージョン 3.0.1-7.0.1 に関わる Grafana の ssrf の不正な脆弱性です。脆弱性の詳細は以下の通りです。

脆弱性の詳細

脆弱性のエントリーポイントは/avatar/:hashインターフェースで、Grafanaのapi.goファイルでは以下のルートで確認できます。

r.Get("/avatar/:hash", avatarCacheServer.Handler)

このルートはインターフェイス/avatar/:hashのハッシュ値を受け取り、secure.grafana.comにルーティングします。コードの実装はおおよそ次のとおりです:

const (
	gravatarSource = "https://..com/avatar/"
)
...
case err = <-thunder.GoFetch(gravatarSource+this.hash+"?"+this.reqParams, this):

secure.gravartar.comそして、これには次のような特徴があります:

この機能の仕組みは?

実際にはリダイレクトです。https://..com/avatar/test?d=http%3a%2f%.com%.png

にリダイレクトされます。

http://..com/.com/.png

カールを使って次のような実験ができます。

i0.wp.com/imageDomain/pathofImageそれから、あなたは困惑するかもしれませんが、私はアドレスが指定されていない、どのように私はi0.wp.comホストにジャンプしましたああ!

また、このホストはどうやって私が指定したアドレスのイメージを入手したのでしょうか?

そして、お察しの通り、ここにもリダイレクトがあります。

imageDoamin/pathofImageへのリクエストが行われると、そのアドレスにリダイレクトされますが、ここでのimageDoaminは単なるドメイン名ではなく、*.bp.blogspot.comサブドメイン配下のドメイン名でなければなりません。

これはホワイトリストの設定であり、必要なのはこのホワイトリストを迂回することであり、一度迂回すれば完了です!

もう少し調べてみたところ、以下のような迂回テクニックを見つけました。

https://grafanaHost/avatar/test%3fd%.com%25253f%253b%..com

上記のテクニックを使えば、簡単にホワイトリスト制限を回避してaxin.comにリクエストをリダイレクトし、ssrf全体を完了させることができます。再現の過程でi0.wp.comがこのバイパスを修正していることがわかりましたので、本家からのイメージを貼っておきます。

イメージからわかるように、指定されたドメインにリダイレクトされますが、それを修正した後、上記のリクエストを送信すると、再びリダイレクトされず、004が返されます。

悲しい話です。

最終的なペイロードは次のようになります:test%3fd%3daxin.com%25253f%253b%252fbp.blogspot.com

そのプロセスを振り返ってみましょう。

https://..com/avatar/anything?d=.com%253f%3b/...com/Grafanaは文字列を:hashとして扱い、その時点でターゲットサーバーは

http://..com/.com%3f%;/...com/

それから、d= パラメータが使われているので、リクエストは再び

http://.com?;/...com

最後に、ホワイトリストの規則性に欠陥があるため、指定されたホストアドレスaxin.comにリダイレクトします。

... if avatar.Expired() { // The cache item is either expired or newly created, update it from the server if err := avatar.Update(); err != nil { log.Trace("avatar update error: %v", err) avatar = this.notFound } } if avatar.notFound { avatar = this.notFound } else if !exists { if err := this.cache.Add(hash, avatar, gocache.DefaultExpiration); err != nil { log.Trace("Error adding avatar to cache: %s", err) } } ctx.Resp.Header().Add("Content-Type", "image/jpeg") if !setting.EnableGzip { ctx.Resp.Header().Add("Content-Length", strconv.Itoa(len(avatar.data.Bytes()))) } ctx.Resp.Header().Add("Cache-Control", "private, max-age=3600") if err := avatar.Encode(ctx.Resp); err != nil { log.Warn("avatar encode error: %v", err) ctx.WriteHeader(500) }

最後に、ターゲットサーバーは上記のアドレスをリクエストし、Content-Typeがimage/jpegのレスポンスを以下のコードで返します:

...
if avatar.Expired() {
	// The cache item is either expired or newly created, update it from the server
	if err := avatar.Update(); err != nil {
		log.Trace("avatar update error: %v", err)
		avatar = this.notFound
	}
}
if avatar.notFound {
	avatar = this.notFound
} else if !exists {
	if err := this.cache.Add(hash, avatar, gocache.DefaultExpiration); err != nil {
		log.Trace("Error adding avatar to cache: %s", err)
	}
}
ctx.Resp.Header().Add("Content-Type", "image/jpeg")
if !setting.EnableGzip {
	ctx.Resp.Header().Add("Content-Length", strconv.Itoa(len(avatar.data.Bytes())))
}
ctx.Resp.Header().Add("Cache-Control", "private, max-age=3600")
if err := avatar.Encode(ctx.Resp); err != nil {
	log.Warn("avatar encode error: %v", err)
	ctx.WriteHeader(500)
}

最後に、エクスプロイトの作者が自身のペイロードの1つである

https://grafanaHost/avatar/test%3fd%..com%25253f%253b%..com%252fYOURHOSTHERE

しかし、i0.wp.comが規則性のバグを修正したため、このペイロードは無効になったようです...。

Read next

Flutterウィークリー65号

チュートリアル Flutter WaterfallとUniversal List Solutions CanonicalはLinux上でFlutterデスクトップアプリサポートプラグインkoukiconsを提供します。

Nov 10, 2020 · 4 min read