ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Clear] LINE CTF 2022 gotm Writeup
    CTF/LINE CTF 2022 2022. 3. 28. 06:56

    주어진 URL에 접근해보면 당장에는 빈 페이지로 보이지만, 주어진 소스를 보면 go언어로 만들어져있는 사이트이며, 유효한 경로와 해당 기능이 어떤 핸들러에 매핑되어 동작하는지 확인 할 수 있다.

    // (3/3) of full source
    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))
    }

     

    각각 기능을 살펴보면,

    // (2/3) of full source
    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) ##### ...... (1)
    		if err != nil {
    		}
    		tpl.Execute(w, &acc)
    	} else {
    
    		return
    	}
    }

    1. / <--> root_handler

     - X-Token 이라는 헤더에서 토큰을 읽어와 jwt디코딩을 하여 유효한 경우 해당 계정의 id를 출력해준다.

    2. /regist <--> regist_handler

     - id,pw 파라미터로부터 계정 정보를 읽어서 Account 구조체 형식에 맞게 계정정보 리스트에 등록한다.

    3. /auth <--> auth_handler

     - 사용자가 입력한 id,pw 값이 유효하다면 id와 is_admin 정보가 포함된 jwt토큰을 발급한다.

       (해당 토큰이 root, flag 핸들러에 활용된다.)

    4. /flag <--> flag_handler

     - X-Token 헤더에서 읽어온 jwt 토큰 값을 디코딩하여 is_admin의 값이 True라면 플래그를 출력해준다.

     

     

    그 외 소스는 아래와 같다. (전체 소스코드는 아래 소스코드를 시작으로 역순이다.)

    // (1/3) of full source
    package main
    
    import (
    	"encoding/json"
    	"fmt"
    	"log"
    	"net/http"
    	"os"
    	"text/template"
    
    	"github.com/golang-jwt/jwt"
    )
    
    type Account struct { 
    	id         string
    	pw         string
    	is_admin   bool
    	secret_key string ##### .................................................. (2)
    }
    
    type AccountClaims struct {
    	Id       string `json:"id"`
    	Is_admin bool   `json:"is_admin"`
    	jwt.StandardClaims
    }
    
    type Resp struct {
    	Status bool   `json:"status"`
    	Msg    string `json:"msg"`
    }
    
    type TokenResp struct {
    	Status bool   `json:"status"`
    	Token  string `json:"token"`
    }
    
    var acc []Account
    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")
    
    func clear_account() {
    	acc = acc[:1]
    }
    
    func get_account(uid string) Account {
    	for i := range acc {
    		if acc[i].id == uid {
    			return acc[i]
    		}
    	}
    	return Account{}
    }
    
    func jwt_encode(id string, is_admin bool) (string, error) {
    	claims := AccountClaims{
    		id, is_admin, jwt.StandardClaims{},
    	}
    	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) ##### ........... (3)
    	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
    }

    기타 소스에서 참고할만한 점은

    (2)에서 account 구조체에 jwt 토큰을 encode/decode 하는데 필요한 secret_key 값이 굳이 포함되어있다는 점과

    (3)에서 jwt토큰에 HS256 알고리즘을 사용하여 하나의 키(secret_key)만 얻으면 토큰의 정보를 조작 할 수 있다는 점이다.(HS256 알고리즘을 사용하는 경우 이론상 BruteForce 공격이 가능하긴 하지만 키의 길이가 매우 짧지 않은 경우가 아니면 오랜 시간이 필요하므로 풀이 방법에서 제외시켰다.)

     

    소스코드를 분석하여 얻은 위 정보들을 미루어보았을 때,

    플래그를 획득할 유일한 방법은 

     1. secret_key leak -> 2. 획득한 secret_key를 통해 is_admin 값이 True 인 jwt 토큰 생성 -> 생성한 토큰을 통해 플래그 획득

    이라고 생각했다.

     

    그래서 secret_key를 어떻게 얻을 수 있을까?

    확실한건 발급되는 토큰 외 우리가 조작 가능한 값 혹은 서버에서 우리에게 정보를 주는 유일한 내용은 root_handler의 id 값이었다. 그래서 해당 부분을 다시 한번 천천히 분석해보았고,

    (1) 과 같이 페이지를 출력해주는 과정에서 "Template" 을 사용 한다는것이 의심스러워졌다.

    go언어가 아직 나에게 익숙한 언어는 아니지만 go 언에에도 SSTI가 가능할지도 모른다는 생각을 했고 이에 대해 공부해보았다.

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

     

    [SSTI] Exploiting Go's template engine to get XSS

    This vulnerability is an edge case of user data being passed directly into the template, and the exploit is a result of not using the int...

    blog.takemyhand.xyz

    우리가 평소에 SSTI 공격의 유효성을 판단할때 예시로 {{7*7}}을 사용한다. 하지만 go 언어에서 SSTI 유효성을 확인하려면 {{*}} 구문을 활용해야 하고, 유효할 경우  data struct  가 포함된 정보를 출력해준다고 한다.

     

    즉, 이 문제의 케이스에서 SSTI가 유효할 경우 injection 포인트가 계정의 id이기 때문에 해당 구조체의 정보가 노출될 것이고 거기에는 소스코드의 (2) 에서 확인 했듯 secret_key가 포함되어 있을 것이다.

     

    해당 시나리오가 유효한지 확인해보자.

     

    1. id 값을 {{.}}로 하여 regist

    2. 해당 계정의 jwt 토큰 확인

    3. 로그인(root_hander 접근)

    우리의 추측대로 해당 계정의 구조체 정보가 노출되어 secret_key 값을 획득 할 수 있었다.

     

    아래와 같이 획득한 secret_key를 활용하여 is_admin 값이 True인 토큰을 발급 할 수 있었고

    import jwt
    
    encoded_jwt = jwt.encode({"id": "admin","is_admin":True}, "fasdf972u1031xu90zm10Av", algorithm="HS256")
    print(encoded_jwt)

    이를 활용하여 플래그를 획득 할 수 있었다.

     

    FLAG = LINECTF{country_roads_takes_me_home}

    반응형

    'CTF > LINE CTF 2022' 카테고리의 다른 글

    [Study] LINE CTF 2022 bb Writeup  (0) 2022.03.29
    [Study] LINE CTF 2022 Memo Drive Writeup  (0) 2022.03.28

    댓글

Designed by Tistory.