CTF/CODEGATE2022

[Clear] CODEGATE2022 superbee Writeup

Vardy 2022. 3. 1. 14:21

주어진 페이지에 접근해보자.

404 Error page가 나와 다소 당황스럽지만, beego 2.0.0 을 사용한다는 점을 확인 하고 주어진 소스코드를 분석해보자.

 

우선 app.conf 파일에 글로벌 파라미터처럼 보이는 값들이 존재하지만, auth_key, password, flag 값은 숨겨져있다.

### app.conf

app_name = superbee
auth_key = [----------REDEACTED------------]
id = admin
password = [----------REDEACTED------------]
flag = [----------REDEACTED------------]

아래와 같이 index.html과 login.html 페이지가 존재함을 알 수 있다.

 

index.html 페이지에 접근하면 위 글로벌 설정 값에서 flag를 읽어서 바로 출력해준다.

### index.html
<html>
    <head>
        <title>{{.app_name}}</title>
    </head>
    <body>
        <h3>Index</h3>
        {{.flag}}
    </body>
</html>

login.html 페이지가 별도로 존재하는것으로 보아, admin 계정으로 로그인하면 위 index.html 페이지에서 바로 플래그를 출력해주는 듯 하다.

### login.html
<html>
    <head>
        <title>{{.app_name}}</title>
    </head>
    <body>
        <h3>Login</h3>
        <form action="./auth" method="post">
            ID : <input type="text" name="id"><br>
            PW : <input type="password" name="password"><br>
            <input type="submit" value="login">
        </form>
    </body>
</html>

 

가장 핵심적인 main.go 페이지를 분석해보자.

package main

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/md5"
	"encoding/hex"
	"bytes"
	"github.com/beego/beego/v2/server/web"
)

type BaseController struct {
	web.Controller
	controllerName string
	actionName string
}

type MainController struct {
	BaseController
}

type LoginController struct {
	BaseController
}

type AdminController struct {
	BaseController
}

var admin_id string
var admin_pw string
var app_name string
var auth_key string
var auth_crypt_key string
var flag string

func AesEncrypt(origData, key []byte) ([]byte, error) {
	padded_key := Padding(key, 16)
	block, err := aes.NewCipher(padded_key)
	if err != nil {
		return nil, err
	}
	blockSize := block.BlockSize()
	origData = Padding(origData, blockSize)
	blockMode := cipher.NewCBCEncrypter(block, padded_key[:blockSize])
	crypted := make([]byte, len(origData))
	blockMode.CryptBlocks(crypted, origData)
	return crypted, nil
}

func Padding(ciphertext []byte, blockSize int) []byte {
	padding := blockSize - len(ciphertext)%blockSize
	padtext := bytes.Repeat([]byte{byte(padding)}, padding)
	return append(ciphertext, padtext...)
}

func Md5(s string) string {
	h := md5.New()
	h.Write([]byte(s))
	return hex.EncodeToString(h.Sum(nil))
}

func (this *BaseController) Prepare() { ##### ......................................... (1)
	controllerName, _ := this.GetControllerAndAction()
	session := this.Ctx.GetCookie(Md5("sess")) ##### .......................... (1)-1

	if controllerName == "MainController" {
		if session == "" || session != Md5(admin_id + auth_key) { ##### ....... (1)-2
			this.Redirect("/login/login", 403)
			return
		}
	} else if controllerName == "LoginController" {
		if session != "" {
			this.Ctx.SetCookie(Md5("sess"), "")
		}
	} else if controllerName == "AdminController" {
		domain := this.Ctx.Input.Domain()

		if domain != "localhost" {
			this.Abort("Not Local")
			return
		}
	}
}

func (this *MainController) Index() { ##### ...................................... (2)
	this.TplName = "index.html"
	this.Data["app_name"] = app_name
	this.Data["flag"] = flag
	this.Render()
}

func (this *LoginController) Login() { ##### ..................................... (3)
	this.TplName = "login.html"
	this.Data["app_name"] = app_name
	this.Render()
}

func (this *LoginController) Auth() { ##### ...................................... (4)
	id := this.GetString("id")
	password := this.GetString("password")

	if id == admin_id && password == admin_pw {
		this.Ctx.SetCookie(Md5("sess"), Md5(admin_id + auth_key), 300)

		this.Ctx.WriteString("<script>alert('Login Success');location.href='/main/index';</script>")
		return
	}
	this.Ctx.WriteString("<script>alert('Login Fail');location.href='/login/login';</script>")
}

func (this *AdminController) AuthKey() { ##### ................................... (5)
	encrypted_auth_key, _ := AesEncrypt([]byte(auth_key), []byte(auth_crypt_key))
    ##### .................................................................... (5)-1
	this.Ctx.WriteString(hex.EncodeToString(encrypted_auth_key)) ##### ....... (5)-2
}

func main() { ##### .............................................................. (6)
	app_name, _ = web.AppConfig.String("app_name")
	auth_key, _ = web.AppConfig.String("auth_key")
	auth_crypt_key, _ = web.AppConfig.String("auth_crypt_key") ##### ......... (6)-1
	admin_id, _ = web.AppConfig.String("id")
	admin_pw, _ = web.AppConfig.String("password")
	flag, _ = web.AppConfig.String("flag")

	web.AutoRouter(&MainController{})
	web.AutoRouter(&LoginController{})
	web.AutoRouter(&AdminController{})
	web.Run()
}

(1)의 prepare() 부분을 보면, 이 서비스는 크게 3가지 controller로 구성되어있음을 확인 할 수 있다.

MainController : 특정 세션 관련 조건이 맞지 않으면(권한이 없으면) /login/login으로 튕겨낸다.

(1)-1, (1)-2 를 참고하면 쿠키의 파라미터명이 "sess" 문자열을 MD5 한 값이고 value가 "admin"+auth_key 값을 MD5 한 값이면 플래그를 획득 할 수 있음을 알 수 있다. 

LoginController : 세션값이 비어있으면 특정 조건의 세션을 세팅한다.

AdminController : domain이 localhost가 아니면 Not local 을 출력해주며 Abort 해버린다.

 

그리고 (2)~(5) 의 선언부를 보면 각 기능이 특정 Controller로 선언되어있음을 알 수 있다.

MainController : Index()

LoginController : Login(), Auth()

AdminController : Authkey()

 

(6)의 main을 보면 각각 파라미터에 config 파일의 값을 불러옴을 알 수 있다.

그런데 이상한점이 있다. 

(6)-1 을 보면 auth_crypt_key 를 app.conf파일에서 불러오는데 다시 살펴보면 해당 값은 선언되어있지 않다.

### app.conf

app_name = superbee
auth_key = [----------REDEACTED------------]
id = admin
password = [----------REDEACTED------------]
flag = [----------REDEACTED------------]

 

그 후 각 컨트롤러에 대해 AutoRouter가 실행되는데 document를 살펴보면 아래와 같이 작동함을 알 수 있다.

https://github.com/beego/beedoc/blob/master/en-US/mvc/controller/router.md#auto-matching

 

GitHub - beego/beedoc: An open source project for beego documentation.

An open source project for beego documentation. Contribute to beego/beedoc development by creating an account on GitHub.

github.com

실제로 (5)-2를 통해 encrypted_auth_key 를 획득하기 위해서는 /admin/authkey 에 접근해야 한다.

(1)에서 AdminController의 조건을 맞춰주기 위해 Burp Suite를 활용하여 조건을 맞춰주면 정상적으로 encrypted_auth_key를 획득 할 수 있다.

(5)-1을 보면 아래 추출 된 값은 auth_key와 auth_crypt_key 로 aes 암호화가 된 값임을 알 수 있다.

00fb3dcf5ecaad607aeb0c91e9b194d9f9f9e263cebd55cdf1ec2a327d033be657c2582de2ef1ba6d77fd22784011607

따라서, 이를 decrypt하면 auth_key를 획득 할 수 있다. 

aes.py

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

key = b"\x10"*16
iv = b"\x10"*16
#raw = pad(input_value)
encrypted = bytes.fromhex("00fb3dcf5ecaad607aeb0c91e9b194d9f9f9e263cebd55cdf1ec2a327d033be657c2582de2ef1ba6d77fd22784011607")
cipher = AES.new(key, AES.MODE_CBC, iv)
#output_value = cipher.encrypt(raw).hex()
decrypted_value = unpad(cipher.decrypt(encrypted),16)
print(decrypted_value)

필요한 조건을 모두 갖추었으므로 admin의 cookie key/value를 구해보자.

 

"sess" 를 md5화 하면 

f5b338d6bca36d47ee04d93d08c57861

"adminTh15_sup3r_s3cr3t_K3y_N3v3r_B3_L34k3d" 를 md5화 하면

e52f118374179d24fa20ebcceb95c2af

위 두 값을 활용하여 /main/index.html에 접근하면 플래그를 획득 할 수 있다.

 

FLAG = codegate2022{d9adbe86f4ecc93944e77183e1dc6342}

반응형