티스토리 뷰
문제 개요
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
더 정확히는 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를 획득한 것을 볼 수 있습니다.
소스코드가 비교적 간단한 구조여서 분석하기 수월했고, 해당 문제는 사람들이 제일 많이 푼 문제였습니다.
- 끝 -
'보안 > CTF' 카테고리의 다른 글
[Hackthebox] Amidst Us Writeup(문제풀이) (0) | 2022.05.23 |
---|---|
[Hackthebox] BlinkerFluids Writeup(문제풀이) (0) | 2022.05.22 |
[LINECTF2022] [WEB] Memo Drive 문제풀이(writeup) (0) | 2022.03.28 |
[LINECTF2022] [WEB] bb 문제풀이(writeup) (0) | 2022.03.28 |
[Codegate2022예선] (BLOCKCHAIN) NFT 문제풀이 (0) | 2022.03.03 |