본문 바로가기

CTF

[LINE CTF 2023] baby simple gocurl

<문제>

 

<풀이>

 

1. 시작

링크로 들어가면 뜨는 첫 화면이다. 3개의 입력창이 있는데, URL을 입력하는 창, 헤더 Key를 입력하는 창, 헤더 Value를 입력하는 창이다. 입력값을 입력한 뒤 CURL이라고 쓰여 있는 버튼을 클릭하면 값이 전송된다.

 

cURL의 뜻을 몰라 검색해봤다. curl은 client url의 약자로, 프로토콜을 이용해 URL로 데이터를 전송하여 서버에 데이터를 보내거나 가져올 때 사용하는 명령어라고 한다.

 

페이지가 어떻게 동작하는지 확인하기 위해 URL입력창에 플레이스홀더로 들어있는 값 https://line.me/en/을 그대로 입력해서 제출해보았다. 해당 URL이 가리키는 페이지 소스가 아래 박스에 출력되었다.

 

2. 코드 분석

코드를 살펴보면 엔드포인트 3개를 확인할 수 있다.

	r.GET("/", func(c *gin.Context) {
		c.HTML(http.StatusOK, "index.html", gin.H{
			"a": c.ClientIP(),
		})
	})

인덱스 페이지이다. index.html 파일을 보여준다.

 

	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]
		fmt.Println("[+] " + reqUrl + ", " + reqIP + ", " + reqHeaderKey + ", " + reqHeaderValue)

		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,
		})
	})

엔드포인트 /curl/ 이다. index 페이지의 입력창에 적은 url, header key, header value값을 요청받아 처리한다.

눈여겨볼 코드는 52~55행이다. ClientIP가 127.0.0.1이 아니면 something wrong 메시지가 출력되고, 요청된 url에 "flag" 또는 "curl" 또는 "%"이 들어가면 마찬가지로 something wrong이 출력된다. ClientIP가 127.0.0.1 이어야 함을 알 수 있다.

 

	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",
		})
	})

엔드포인트 /flag/이다. reqIP가 127.0.0.1이면 flag를 출력하는 것을 확인할 수 있다. reqIP가 127.0.0.1인지 확인하는 if문으로 들어가지 못하면 "You are a Guest, This is only for Host"라는 메시지를 받게 된다.

 

3. ClientIP를 127.0.0.1로 설정하기

엔드페이지 /curl/에 대한 코드에서 확인한 바와 같이 ClientIP는 127.0.0.1이어야 한다. ClientIP를 식별하기 위한 HTTP 헤더를 검색해보았다. https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/X-Forwarded-For

X-Forwarded-For 라는 헤더 키를 찾았다. 클라이언트의 원IP주소를 보기 위해 사용되는 요청 헤더라고 한다.

 

헤더 Key 입력창에 X-Forwarded-For를 입력하고, 헤더 Value 입력창에 127.0.0.1을 입력하면 ClientIP를 127.0.0.1로 설정할 수 있을 것이라고 추측했다.

 

4. request IP를 127.0.0.1로 설정하기

엔드페이지 /flag/에 대한 코드에서 확인한 바와 같이 reqIP 또한 127.0.0.1이어야 한다. 그래야 서버에 host로 인식될 수 있다.

 

CTF 대회 시간동안에 이 부분은 해결하지 못했다. 하지만 대회가 끝나고 나서 해답을 알게 되었다.

 

버프스위트를 이용해 URL 입력창에 http://localhost:8080/flag를 입력하면 FLAG가 들어있는 메시지를 응답하는 것을 확인할 수 있다. 그런데 엔드페이지 /curl/에 대한 코드에서 확인한 바에 따르면, URL 입력값에 "flag"가 들어있으면 something wrong이 출력되는데 이 해답의 케이스에서는 왜 something wrong이 안 나오고 FLAG가 출력되었는지 잘 모르겠다.