티스토리 뷰

보안/CTF

[LINECTF2022] [WEB] gotm 문제풀이(writeup)

돔돔이부하 2022. 3. 28. 14:41
728x90
반응형

문제 개요

Golang template injection and jwt token's secret_key exposure

코드 분석

Dockerfile 을 봤을 때 KEY, FLAG, ADMIN_ID, ADMIN_PW 값이 있다는 것을 확인할 수 있습니다.

ENV KEY this_is_fake_key
ENV FLAG LINECTF{this_is_fake_flag}
ENV AMDIN_ID admin
ENV AMDIN_PW this_is_fake_pw

그리고 이를 main.go 파일에서는 바로 전역 변수로 아래와 같이 추가하고 있는 것을 볼 수 있습니다.

var secret_key = os.Getenv("KEY")
var flag = os.Getenv("FLAG")
var admin_id = os.Getenv("ADMIN_ID")
var admin_pw = os.Getenv("ADMIN_PW")

 

main.go 의 최상단에서 import 부분을 보면 text/template 이라는 템플릿 엔진과 jwt 토큰을 사용한다는 것을 알 수 있습니다.

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"text/template"

	"github.com/golang-jwt/jwt"
)

 

main 함수에서는 아래와 같이 서버 실행 직후 바로 admin 계정을 생성해서 acc 배열에 넣고 있고, 아래로는 등록된 라우터와 컨트롤러들이 보입니다.

func main() {
	admin := Account{admin_id, admin_pw, true, secret_key}
	acc = append(acc, admin)

	http.HandleFunc("/", root_handler)
	http.HandleFunc("/auth", auth_handler)
	http.HandleFunc("/flag", flag_handler)
	http.HandleFunc("/regist", regist_handler)
	log.Fatal(http.ListenAndServe("0.0.0.0:11000", nil))
}

 

flag_handler 를 보면 is_admin == true 즉 admin 계정일 경우에는 Flag가 출력되서 보인다고 합니다.

func flag_handler(w http.ResponseWriter, r *http.Request) {
	token := r.Header.Get("X-Token")
	fmt.Println(token)
	if token != "" {
		id, is_admin := jwt_decode(token)
		fmt.Println(id)
		fmt.Println(is_admin)
		if is_admin == true {
			p := Resp{true, "Hi " + id + ", flag is " + flag}
			res, err := json.Marshal(p)
			if err != nil {
			}
			w.Write(res)
			return
		} else {
			w.WriteHeader(http.StatusForbidden)
			return
		}
	}
}

 

jwt token 을 다루는 것에 있어서는 사용하는 알고리즘 방식이나 로직 자체에 결함은 없어 보였기 때문에 secret_key를 얻거나 다른 취약점으로 admin 계정을 획득해야한다는 것을 알 수 있습니다.

 

그러다가 root_handler 에서 golang text/template 모듈의 template injection 취약점이 있어 보였습니다.

func root_handler(w http.ResponseWriter, r *http.Request) {
	token := r.Header.Get("X-Token")
	if token != "" {
		id, _ := jwt_decode(token)
		acc := get_account(id)
		tpl, err := template.New("").Parse("Logged in as " + acc.id)
		if err != nil {
		}
		tpl.Execute(w, &acc)
	} else {

		return
	}
}

template.New("").Parse() 에서 acc.id 를 기준으로 템플릿을 렌더링하는 것을 볼 수 있습니다. acc.id 는 회원가입할 때 사용한 사용자 ID 를 뜻하는데요.

 

func regist_handler(w http.ResponseWriter, r *http.Request) {
	uid := r.FormValue("id")
	upw := r.FormValue("pw")
	fmt.Println(uid)
	fmt.Println(upw)

	if uid == "" || upw == "" {
		return
	}

	if get_account(uid).id != "" {
		w.WriteHeader(http.StatusForbidden)
		return
	}
	if len(acc) > 4 {
		clear_account()
	}
	new_acc := Account{uid, upw, false, secret_key}
	acc = append(acc, new_acc)

	p := Resp{true, ""}
	res, err := json.Marshal(p)
	if err != nil {
	}
	w.Write(res)
	return
}

회원가입 할 때 uid 에 대해 별도의 필터링은 없어 보였습니다.

 

즉 사용자 ID 를 통한 Template Injection 되는 것을 알 수 있습니다. text/template 모듈에 관한 설명은 아래 링크에서 확인할 수 있습니다.

https://pkg.go.dev/text/template

 

template package - text/template - pkg.go.dev

This example demonstrates one way to share some templates and use them in different contexts. In this variant we add multiple driver templates by hand to an existing bundle of templates. package main import ( "io" "log" "os" "path/filepath" "text/template"

pkg.go.dev

더 정확히는 struct 구조체 부분을 어떻게 템플릿에 출력하는 지에 대해서 보면 이 문제에 대한 해답을 알 수 있게 됩니다.

 

문제 풀이

회원가입에 사용되는 Account 구조체는 아래와 같은 구조를 가지고 있습니다.

type Account struct {
	id         string
	pw         string
	is_admin   bool
	secret_key string
}

 

그렇기 때문에 아래와 같은 코드에서

tpl, err := template.New("").Parse("Logged in as " + acc.id)
tpl.Execute(w, &acc)

 

acc.id 에 { . } 가 들어가게 되면 Account 라는 구조체의 구조 전체를 출력하게 됩니다. (acc 는 전역 변수로써 Account 구조체의 배열을 의미합니다.)

마침 Account 구조체의 제일 마지막 속성은 secret_key 였습니다. 이로써 jwt token 에 사용될 secret_key 를 획득할 수 있게 됩니다.

 

이제 X-Token 값에 등록된 일반 사용자의 토큰 값을 secret_key 를 이용해서 decrypt 하고 data 부분에서 is_admin 부분을 true 값으로 만든 다음에 secret_key 를 이용해서 encrypt 를 한 결과를 X-Token 값에 넣어준 다음에 /flag 경로로 이동해봅니다.

위와 같이 Flag를 획득한 것을 볼 수 있습니다.

 

 

소스코드가 비교적 간단한 구조여서 분석하기 수월했고, 해당 문제는 사람들이 제일 많이 푼 문제였습니다.

 

- 끝 -

728x90
반응형
댓글