ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Clear] LINE CTF 2023 Baby Simple GoCurl
    CTF/LINE CTF 2023 2023. 3. 26. 09:00

    주어진 URL에 접근해보자

    URL, Header Key, Header Value를 입력받아서 요청을 날리고, 응답 값을 json형식으로 출력해준다.

    응답이 단순히 json 데이터로 반환되기때문에 클라이언트 영역에서 리다이렉트시키거나 스크립트를 실행하는것은 불가능했다.

     

     

    대략적인 기능을 파악했으니, 주어진 Go언어의 소스코드를 분석해보자.

    (디버깅을 위해 출력부분 관련하여 추가/수정된 부분이 있다.)

    package main
    
    import (
    	"errors"
    	"io/ioutil"
    	"log"
    	"net/http"
    	"os"
    	"strings"
    
    	"github.com/gin-gonic/gin"
    )
    
    func redirectChecker(req *http.Request, via []*http.Request) error {
    	log.Println("[+] redirectChecker started!!")
    	log.Println("via : ")
    	log.Println(via)
    	for i := 0; i <= len(via)-1; i++ {
    		log.Println("via Hosts! : ")
    		log.Println(via[i].Host)
    		log.Println("******************************")
    
    	}
    
    	reqIp := strings.Split(via[len(via)-1].Host, ":")[0]
    	log.Println("reqIp : ")
    	log.Println(reqIp)
    	log.Println("******************************")
    
    	if len(via) >= 2 || reqIp != "127.0.0.1" {
    		log.Println("Error at redirectChecker")
    		return errors.New("Something wrong")
    	}
    
    	return nil
    }
    
    func main() {
    	flag := os.Getenv("FLAG")
    
    	r := gin.Default()
    
    	r.LoadHTMLGlob("view/*.html")
    	r.Static("/static", "./static")
    
    	r.GET("/", func(c *gin.Context) {
    		c.HTML(http.StatusOK, "index.html", gin.H{
    			"a": c.ClientIP(),
    		})
    	})
    
    	r.GET("/curl/", func(c *gin.Context) {
    		client := &http.Client{
    			CheckRedirect: func(req *http.Request, via []*http.Request) error {
    				return redirectChecker(req, via)
    			},
    		}
    
    		reqUrl := strings.ToLower(c.Query("url"))
    		reqHeaderKey := c.Query("header_key")
    		reqHeaderValue := c.Query("header_value")
    		reqIP := strings.Split(c.Request.RemoteAddr, ":")[0]
    		log.Println("[+] " + reqUrl + ", " + reqIP + ", " + reqHeaderKey + ", " + reqHeaderValue)
    		log.Println("[*] c.ClientIp() : ")
    		log.Println(c.ClientIP())
            
            // ############################################################ (1)
    		if c.ClientIP() != "127.0.0.1" && (strings.Contains(reqUrl, "flag") || strings.Contains(reqUrl, "curl") || strings.Contains(reqUrl, "%")) {
    			c.JSON(http.StatusBadRequest, gin.H{"message": "Something wrong"})
    			return
    		}
    
    		req, err := http.NewRequest("GET", reqUrl, nil)
    		if err != nil {
    			c.JSON(http.StatusBadRequest, gin.H{"message": "Something wrong"})
    			return
    		}
    
    		if reqHeaderKey != "" || reqHeaderValue != "" {
    			req.Header.Set(reqHeaderKey, reqHeaderValue)
    		}
    
    		resp, err := client.Do(req)
    		if err != nil {
    			c.JSON(http.StatusBadRequest, gin.H{"message": "Something wrong"})
    			return
    		}
    
    		defer resp.Body.Close()
    
    		bodyText, err := ioutil.ReadAll(resp.Body)
    		if err != nil {
    			c.JSON(http.StatusBadRequest, gin.H{"message": "Something wrong"})
    			return
    		}
    		statusText := resp.Status
    
    		c.JSON(http.StatusOK, gin.H{
    			"body":   string(bodyText),
    			"status": statusText,
    		})
    	})
    
    	r.GET("/flag/", func(c *gin.Context) {
    		reqIP := strings.Split(c.Request.RemoteAddr, ":")[0]
    
    		log.Println("[+] IP : " + reqIP)
    		if reqIP == "127.0.0.1" {
    			c.JSON(http.StatusOK, gin.H{
    				"message": flag,
    			})
    			return
    		}
    
    		c.JSON(http.StatusBadRequest, gin.H{
    			"message": "You are a Guest, This is only for Host",
    		})
    	})
    
    	r.Run()
    }

    (1)의 조건문이 문제의 핵심이다.

    c.ClientIP() 를 통해 획득한 클라이언트의 IP가 127.0.0.1이 아니고 (And)

    입력받은 URL 에 flag, curl, % 이 포함되어있으면 

    조건이 참이되어 Something wrong을 반환한다. 이 필터링 로직을 통해 서버가 /flag에 접근하는것을 제한한다.

     

    플래그의 획득조건인 /flag 부분을 보면 요청자의 IP가 127.0.0.1 이어야 한다.

    즉, 어떻게든 서버가 http://127.0.0.1:8080/flag 에 접근하도록 해야한다.

     

    동시에,

    1. 필터링 조건문을 false로 만드려면 c.ClientIP() 의 값이 127.0.0.1 이거나

    2. 사용자 입력 url에 flag라는 값이 없어야한다.(%도 사용 불가하므로 url필터링을 통한 우회도 제한하였다.)

     

    처음에는 두 조건이 다 달성하기 어렵다고 판단하여 접근 시 서버에서 직접 302 상태 코드를 반환하면서 http://127.0.0.1:8080/flag 로 리다이렉트 시키는 서버를 만들어서 활용하는 방법을 생각해봤다.

    하지만 /curl/접근 시 우선적으로 redirectChecker()에 의해 해당 시나리오는 불가했다.

     

    어쨌든 url에 path에 flag 키워드 없이 접근이 절대 불가하다고 생각하여 c.ClientIP()를 제어 할 수 있는 방법을 조사해보았다. 문제에서 헤더 키와 헤더 밸류를 입력받는다는 점에서 Http Request Header를 중점적으로 조사했고,

    c.ClientIP()의 값이 X-Forwarded-For 혹은 X-Real-IP 헤더의 값에 영향을 받는 케이스가 있다는 것을 알게되었다.

    https://github.com/gin-gonic/gin/issues/2697

     

    c.ClientIP() · Issue #2697 · gin-gonic/gin

    Friends, the update to 1.7 broke the work with IP addresses of clients. I am proxying Go through Nginx location /backend/ { rewrite /backend/(.*) /$1 break; proxy_set_header X-Forwarded-For $remote...

    github.com

     

    나중에 알고보니 해당 부분 관련 공식 자료의 주석에 해당 내용이 있었다.

    https://github.com/gin-gonic/gin/blob/457fabd7e14f36ca1b5f302f7247efeb4690e49c/context.go#L768

     

    GitHub - gin-gonic/gin: Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better perf

    Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance -- up to 40 times faster. If you need smashing performance, get yourself some Gin. - ...

    github.com

     

    그리고 실제로 유효함을 확인했다.

     

    우리는 /curl/이 호출되어 실행되는 과정에서의 c.ClientIP() 값을 변조해야하므로 입력란에 저렇게 기입해서는 안되고, 최초에 /curl/을 호출하는 패킷에 헤더 정보를 직접 추가해줘야 한다.

     

    그러면 1. 조건이 false가 되어 url에 flag 키워드 존재 여부와 상관 없이 전체 필터링 조건문이 false가 되어 우회가 되고, 플래그를 획득 할 수 있다.

    FLAG = LINECTF{6a22ff56112a69f9ba1bfb4e20da5587}

    반응형

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

    [Study] LINE CTF 2023 Adult Simple GoCurl  (0) 2023.03.27

    댓글

Designed by Tistory.