SECCON CTF 2021 writeup

Writer:b1uef0x / Webページ建造途中

概要

チームで参加し、Web問を1問解いた。突貫でWriteup作成。

目次

Vulnerabilities (web)

脆弱性情報を選択するとその脆弱性のロゴとURLが表示されるWebページが与えられる。

Vulnerabilities

プログラムはGO言語で書かれており、データベースの操作にGORMを利用している。

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

以下部分で脆弱性の一覧をdbに登録しているが、一番最後にflagが入力されている。

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

POSTリクエストを処理する部分は以下の通り。

	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,
		})
	})

POSTリクエストではJSON形式のデータ、例えばHeartbleedを検索する場合は{"Name":"Heartbleed"}がリクエストされる。

まずリクエストデータがJSONでなければJSON error 1を返し、JSONデータにNameパラメータがなければno "Name"を返す。

続いてJSONデータをVulnerability構造体のquery変数にバインドし、バインドできなければJSON error 2を返す。

すべてパスすると、queryを使ってdb.Whereでデータベースを検索して該当したものの一番目を返すようになっている。

上手い具合にflagの入ったデータベースを検索できればよい。

GROMについて調べたところ、構造体定義時のgorm.Modelが使えることに気づいた。

type Vulnerability struct {
	gorm.Model
	Name string
	Logo string
	URL  string
}

gorm.Modelを宣言すると、自動的にID変数がint型で定義されるようになる。例えば{"Name":"Heartbleed","ID":1}を検索すると正常にHeartbleedが検索されるため、HeartbleedのIDは1である。連番でIDが振られていくと、flagのIDは14になるはずだ。

ID:14で検索をかけるには、Nameパラメータのチェックを回避しなければいけない。Nameがなかったり空であったりするとエラーを返して検索にたどり着けない。これを回避するために次のようなJSONをPOSTした。

{"Name":"Heartbleed","ID":14,"name":""}

これはJSONのNameパラメータを調べる際にはNameが参照されるが、構造体にバインドされる際には大文字小文字が区別されず空のnameが使用されるため、{Name:"",ID:14}で検索をかけることができる。

Flag

SECCON{LE4RNING_FR0M_7HE_PA5T_FINDIN6_N0TABLE_VULNERABILITIE5}

case-insensitive (misc)

bcryptでハッシュの生成と検証を行えるサービス。

ソルバーの作成が競技終了に間に合わず、また基本的な解法は他の参加者の方の知恵だが、ソルバーが書けたのでせっかくだから掲載しておく。

サーバーに接続してプログラムを実行すると、mode.1でメッセージからハッシュ計算、mode.2でメッセージの検証ができる。

$ nc case-insensitive.qeccon.jp 8080
1. sign
2. verify
mode: 1
message: AAAA
mac: $2b$05$rAbqdzbfM6YwvclwvrGFiuprIAx5QoDkJzK3QHeKGEfnj3xuF6sVy
1. sign
2. verify
mode: 2
mac: $2b$05$rAbqdzbfM6YwvclwvrGFiuprIAx5QoDkJzK3QHeKGEfnj3xuF6sVy
message: AAAA
result: True

ソースコードは以下の通り。

#!/usr/bin/env python3

from flag import flag
import signal
import bcrypt

def check_and_upper(message):
    if len(message) > 24:
        return None
    
    message = message.upper()

    for c in message:
        c = ord(c)
        if ord("A") > c or c > ord("Z"):
            return None
    
    return message

signal.alarm(600)
while True:
    mode = input(
        """1. sign
2. verify
mode: """
    ).strip()

    ## sign mode ##
    if mode == "1":
        message = check_and_upper(input("message: ")) # case insensitive

        if message == None:
            print("invalid")
            continue

        salt = bcrypt.gensalt(5)
        print("mac:", bcrypt.hashpw((message + flag).encode(), salt).decode("utf-8"))

    ## verify mode ##
    else:
        mac = input("mac: ")
        message = check_and_upper(input("message: ")) # case insensitive

        if message is None:
            print("invalid")
            continue

        print("result:", bcrypt.checkpw((message + flag).encode(), mac.encode()))

コードを確認すると、ハッシュの計算はmessage+flagで行っている。また、messageは24文字以下に制限され、大文字にしたあとにA-Zになるものに制限されている。

他の参加者の方の知恵により、bcryptのメッセージは72文字以上は無視される点と、大文字にすると複数文字になる文字が存在することを利用して解くことができる。

「ß」を大文字にするとSSになることがわかっていたが、さらに調べると「ffl」が3文字のFFLになることがわかった。

つまり「fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflffl」をmessageとして入力すると72文字の大文字アルファベットになり、ハッシュ計算時にflagを排除することができる。ここから文字数を減らしてflagを少しずつ入れたハッシュのverifyをブルートフォースすることでflagを1文字ずつ入手することが可能。大文字にすると1文字、2文字、3文字になるUnicode文字を組み合わせて、flagを1文字ずつブルートフォースする。

ソルバーは以下の通り。

import bcrypt
import string
from pwn import *

str3 = "ffl"
str2 = "ß"
str1 = "A"

strs = string.ascii_letters+string.digits+string.punctuation

res = ""
c = 71
io = remote("case-insensitive.quals.seccon.jp",8080)

while(c>0):
	i3 = int(c/3)
	i2 = int((c-i3*3)/2)
	i1 = c - i3*3 - i2*2
	msg = (str3)* i3 + (str2)* i2 + (str1)* i1
	print("msg = "+msg)
	io.recvuntil("mode: ")
	io.sendline("1".encode())
	io.recvuntil("message: ")
	io.sendline(msg.encode())
	mac = io.recvline()[5:65].decode()
	print("mac = "+mac)
	ch = True
	for i in strs:
		if bcrypt.checkpw((msg.upper() + res +i).encode(), mac.encode()):
			res += i
			print("flag= "+res)
			ch = False
			break
	if ch :
		break
	c -= 1
io.close()

実行するとflagを獲得できる。

$ python3 solve.py 
[+] Opening connection to case-insensitive.quals.seccon.jp on port 8080: Done
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflß
mac = $2b$05$wp68kzqXRN2eADD8OK/xKuLBMNTo0J8z4ZONHJa6l5MMZ8nOwcAZe
flag= S
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflA
mac = $2b$05$vIfcYrZnv1oJhyQs9EtuL.HyOwC7X1yBqUCL6E09DNKQ.hlj4R8lK
flag= SE
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflffl
mac = $2b$05$iXjrMxnOoRFNBoj.buYuLu7dUr7jku.b0tHzVXowM5FqiRRr.j6N2
flag= SEC
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflß
mac = $2b$05$0vsItWF9pPGleaPY7hRJEey3oy5KbbtbMVzI7VahcptYM2QCDknnO
flag= SECC
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflA
mac = $2b$05$HrpDS43RIGCOdNUGXR4xHuZWOrdonnmLqUVAJoRwTMugeggPjdgiC
flag= SECCO
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflffl
mac = $2b$05$SNTuekap14O2Xg9VUXuA9uxTwQWalhVEVIjbqX3Nzan/RUmoXZCau
flag= SECCON
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflß
mac = $2b$05$ZYQ6ShUh2gc6bgwbAkyXxeTTiEChz6acrH8BaIZ55a0NzjTPhdava
flag= SECCON{
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflA
mac = $2b$05$ZcgP.osXx6XYEPnQEcvY5OWdzj5mgTyq.v9dltEW5X/Upq7iOUmzO
flag= SECCON{u
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflffl
mac = $2b$05$9FV2OdP8f3U5U2sit.HcNe9MlV7Zo5D/NM7k7uWD6frR9wAECwKqK
flag= SECCON{uP
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflß
mac = $2b$05$Bx40sORBBopotsYfWvHZj.FnSb4Y2mQfbKzubVC5J0tTFakiCspny
flag= SECCON{uPP
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflA
mac = $2b$05$wyJgWMSAq6pCsMAVh1mP1.4XaH8j6GqMQ4YKbWjuZ8DY6gvPRy626
flag= SECCON{uPPE
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflffl
mac = $2b$05$.15uT2SUf2wyj1ZhQfUOye9W7lwcviB0G/Doa3d9wKdnBgI9EoKpS
flag= SECCON{uPPEr
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflß
mac = $2b$05$XjZpW.mcpFaMjdRm257Lb.geeCOMuizbFu4.x7JYtlS12mPG66zF2
flag= SECCON{uPPEr_
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflA
mac = $2b$05$KOzyBAyPsH2jq0fUDeEpO.fDpdRtd313Hu2RSCWSeJo7BjGN5UAaC
flag= SECCON{uPPEr_i
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflffl
mac = $2b$05$QkN/EHVFwLTGvVCAZbzTwOTcIuYcFmRvbxiLnyNeyCOiEJIOZCiTC
flag= SECCON{uPPEr_is
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflß
mac = $2b$05$GFZ6Lxivg.4M/RYzNt8I5ubnK5KW7B2jn3EkExRG0JC05w1Z5VnGi
flag= SECCON{uPPEr_is_
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflA
mac = $2b$05$SUHAJ.bOmPhHKkpN7WucXeHadLfqYOyUJI9CcbizBPcwWLrxYSgCu
flag= SECCON{uPPEr_is_M
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflffl
mac = $2b$05$XLWY8tEuWBiBtBDytA3KjedQ6sPptQEd4avNr/QnLcz2haLIbqAwq
flag= SECCON{uPPEr_is_M4
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflß
msg = $2b$05$O25h.NAOA6YlL/Hubg3y9.ixHoSq3yd69pyUz7hmybJPR.0zPA3Zu
mac = SECCON{uPPEr_is_M4g
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflA
mac = $2b$05$040otB.kDbZMLSnI9BpT5eWL7t8BGIgWlN3BaDhB2JD7r5a3iDY7u
flag= SECCON{uPPEr_is_M4g1
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflffl
mac = $2b$05$rSNzrtjl9DwD7heV4lWjfu8mrM87xkux0byfRQBfdcVCq.U7ni12i
flag= SECCON{uPPEr_is_M4g1c
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflß
mac = $2b$05$n8yylOP9MiFmoi0XSK9YGubvSMuHQzMXXF9UIEokjzvd8sXKg2edG
flag= SECCON{uPPEr_is_M4g1c}
msg = fflfflfflfflfflfflfflfflfflfflfflfflfflfflfflfflA
mac = $2b$05$iD9Hxnd9vrbSmSwT4Od1s.5Q10VyUutU2gVByPSSQAN2ufQ9/5Fxq
[*] Closed connection to case-insensitive.quals.seccon.jp port 8080

SECCON{uPPEr_is_M4g1c}