今日はなにの日。

気になったこと勉強になったことのメモ。

今日は、SECCON CTF 2021で解けなかった問題復習の日。

目次

とある日

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}"}

SECCON CTF 2021

解説

まず、バリデーションをクリアします。

バリデーション

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が構築されていると思ってます。

  1. Name = "b"
    1. SELECT * FROM users WHERE name = "b";
  2. name = ""
    1. SELECT * FROM users WHERE name = "";←Nameとnameが小文字に変換されて同じ列と認識して値を空値に書き換える
  3. id = 14
    1. 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が構築されていると思います。

  1. 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が出たらやろうかなと思ってます。