Post

CVE-2023-27163: Request Baskets의 Forward URL 처리 과정에서 발생하는 SSRF 취약점

CVE-2023-27163: Request Baskets의 Forward URL 처리 과정에서 발생하는 SSRF 취약점

본 포스팅은 HTB의 ‘Sau’ 실습을 기반으로 작성되었습니다.

HTB - Sau


1. Request Baskets

Request Baskets는 HTTP 요청을 수집하거나 특정 주소로 전달(forwarding)하기 위해 사용되는 오픈소스 애플리케이션이다. Webhook 테스트, API 요청 디버깅, 요청 로깅 및 확인, 요청 프록시 처리 등을 목적으로 사용된다.

Request Basket의 핵심 기능 중 하나는 사용자가 생성한 basket에 대한 요청을 다른 URL로 forwarding 할 수 있는 기능이다.

예를 들어, ‘/test’라는 basket을 생성한 뒤 다음과 같이 설정할 수 있다.

1
2
3
4
5
{
  "forward_url":"http://example.com",
  "proxy_response":true,
  "expand_path":true
}

이 경우, 사용자가 http://target:55555/test/login에 접근하면,

Request Basket은 내부적으로 http://example.com/login에 접근하는 요청을 수행한다.

즉, Requests Baskets는 요청 수집 도구를 넘어, 사용자가 지정한 주소로 서버 측 HTTP 요청을 대신 수행할 수 있다.

해당 기능에서 취약점이 발생한다.



2. CVE-2023-27163

2.1 취약점 개요

CVE-2023-27163은 Request Baskets <= 1.2.1 버전에 존재하는 SSRF(Server-Side Request Forgery) 취약점이다.

공격자는 basket의 포워딩 기능을 악용하여 서버가 임의의 주소로 HTTP 요청을 수행하도록 만들 수 있다. 예를 들어, 포워딩 주소를 127.0.0.1과 같은 내부 주소로 설정하면 외부에서는 접근할 수 없는 내부 서비스에 대해 Request Baskets가 대신 요청을 수행하게 만들 수 있다.


2.2 코드 분석

Request Baskets에서 포워딩 함수는 아래와 같다.

Request Baskets는 사용자가 basket에 요청을 보내면 해당 요청 정보를 저장한 뒤 설정된 ForwardURL로 동일한 요청을 다시 전달(forwarding)하는 구조이다.

전체 흐름은 다음과 같다.

  1. 사용자 요청 수신
  2. RequestData 생성
  3. ForwardURL 확인
  4. 새로운 HTTP Request 생성
  5. ForwardURL로 요청 전달

실제 포워딩 처리(3~5)는 Forward 함수 내부에서 수행된다.

[Baskets.go/Forward()]

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
49
50
51
52
func (req *RequestData) Forward(client *http.Client, config BasketConfig, basket string) (*http.Response, error) {
	forwardURL, err := url.ParseRequestURI(config.ForwardURL)
	if err != nil {
		return nil, fmt.Errorf("invalid forward URL: %s - %s", config.ForwardURL, err)
	}

	// expand path
	if config.ExpandPath && len(req.Path) > len(basket)+1 {
		forwardURL.Path = expandURL(forwardURL.Path, req.Path, basket)
	}

	// append query
	if len(req.Query) > 0 {
		if len(forwardURL.RawQuery) > 0 {
			forwardURL.RawQuery += "&" + req.Query
		} else {
			forwardURL.RawQuery = req.Query
		}
	}

	forwardReq, err := http.NewRequest(req.Method, forwardURL.String(), strings.NewReader(req.Body))
	if err != nil {
		return nil, fmt.Errorf("failed to create forward request: %s", err)
	}

	// copy headers
	for header, vals := range req.Header {
		for _, val := range vals {
			forwardReq.Header.Add(header, val)
		}
	}
	// headers cleanup
	forwardHeadersCleanup(forwardReq)
	// set do not forward header
	forwardReq.Header.Set(DoNotForwardHeader, "1")

	// forward request
	response, err := client.Do(forwardReq)
	if err != nil {
		// HTTP issue during forwarding - HTTP 502 Bad Gateway
		log.Printf("[warn] failed to forward request for basket: %s - %s", basket, err)
		badGatewayResp := &http.Response{
			StatusCode: http.StatusBadGateway,
			Header:     http.Header{},
			Body:       ioutil.NopCloser(strings.NewReader(fmt.Sprintf("Failed to forward request: %s", err)))}
		badGatewayResp.Header.Set("Content-Type", "text/plain")

		return badGatewayResp, nil
	}

	return response, nil
}

코드를 보면 사용자가 설정한 ForwardURL 값은 url.ParseRequestURI()를 통해 형식만 검증된다.

1
forwardURL, err := url.ParseRequestURI(config.ForwardURL)

url.ParseRequsetURI()은 Go의 net/url 패키지에서 제공하는 함수로, 전달된 문자열이 RFC 기반 URL 형식에 맞는지만 검사한다. 즉 단순 URL 문법만 검사할 뿐, 해당 주소가 내부망인지 여부는 검사하지 않으므로 http://127.0.0.1과 같은 주소들도 모두 정상 URL로 처리된다.

이후 Forward() 함수는 사용자가 입력한 URL을 그대로 사용하여 새로운 HTTP 요청을 생성하고, Client.Do()를 통해 Request Baskets 서버가 직접 해당 주소로 요청을 수행한다.

1
2
3
4
5
6
7
8
forwardReq, err := http.NewRequest(
    req.Method, 
    forwardURL.String(), 
    strings.NewReader(req.Body))

...

response, err := client.Do(forwardReq)

결과적으로 공격자는 Request Baskets 서버를 프록시처럼 이용하여 내부 네트워크나 로컬 서비스에 접근할 수 있게 된다.

추가로, 본 포스팅의 실습에서는 사용되지 않았지만 Go의 기본 http.Client는 별도의 ‘CheckRedirect’ 설정이 없는 경우 HTTP Redirect를 자동으로 따라간다. 따라서 환경에 따라 Redirect를 이용한 SSRF 우회 가능성도 존재한다.

즉, CVE-2023-27163의 원인은 사용자 입력 URL이 별도의 내부망 검증 없이 서버 측 HTTP 요청 대상으로 사용되었다는 점이다.


2.3 실습

실습 환경은 다음과 같다.

  • basket을 생성 및 사용할 수 있는 55555번 포트는 외부에 공개되어 있다.
  • 80번 포트는 내부 포트로서 외부에서는 접근할 수 없다.

실습 과정은 다음과 같다.

  1. 접근 가능한 55555번 포트에서 basket을 생성한다.
  2. 생성한 basket의 Forwarding URL을 request-basket 내부 주소(http://127.0.0.1:80)로 설정한다.
  3. basket 경로로 요청을 전송하여 Request Baskets 서버가 내부 주소로 HTTP 요청을 수행하도록 만든다.
  4. 응답 결과를 통해 외부에서는 접근할 수 없는 내부 웹 서비스에 접근할 수 있음을 확인한다.

[1] basket 생성

웹 소스코드에서 얻은 API로 요청을 보내 토큰을 얻는다.

[2] Forwarding URL 설정

아래 옵션들을 이용하여 Forwarding URL을 설정한다. 실제로 브라우저에 설정 창이 있다. 포워딩 URL은 닫혀있던 80번 포트로 설정한다. 앞으로 ~:55555/exploit으로의 요청은 ~:80으로 포워딩된다.

[3] 결과 확인

nmap이나 curl로 80번 포트에 접근하면 사진과 같이 필터링되거나 무한 대기 상태에 걸린다. 하지만 55555번 포트에서 설정해둔 /exploit 페이지로 접속하면 80번 포트에 해당하는 페이지의 html 코드를 얻을 수 있다.

3. 패치

현재 공식 오픈소스에서는 해당 취약점에 대한 근본적인 SSRF 차단 패치보다는, 위험을 완화(mitigate)하는 방향으로 대응하고 있다. 개발자 역시 해당 기능의 특성상 단순 URL 필터링만으로는 SSRF를 완전히 차단하기 어렵다고 언급하였다.

Request Baskets는 사용자가 지정한 URL로 요청을 전달하는 forwarding 기능 자체가 핵심 기능이기 때문에, localhost나 내부망 주소를 차단하더라도 Redirect 등을 이용한 우회 가능성이 존재한다.

대신 개발자는 다음과 같은 방향으로 위험 완화 방안을 제안하였다.

  • forwarding 기능 기본 비활성화
  • 관리자만 forwarding 기능 사용 가능하도록 제한
  • Docker/LXC 등을 이용한 네트워크 격리
  • 방화벽을 통한 내부망 접근 차단

실제로 이후 커밋에서는 forwarding 기능을 기본적으로 비활성화하였다.



Reference


request-baskets

request-baskets/issues

This post is licensed under CC BY 4.0 by the author.