[WEB] web-ssrf 문제풀이

SSRF(Server Side Request Forgery)는 CSRF와 다르게 클라이언트 측의 요청을 변조시키는 것이 아닌 서버 측 자체의 요청을 변조하여 공격자가 원하는 형태의 악성 행위를 서버에 던져주면 서버가 검증 없이 그대로 받아 그에 따른 행위/응답을 해주는 공격을 말한다. 이 공격은 주로 사용자 입력을 받아 서버가 직접 다른 웹이나 포트에 접근해서 데이터를 가져오는 기능에서 발생하며, 외부가 아닌 내부에서 공격을 수행하게 되므로 접근제어 정책을 우회할 수 있는 공격이다.

 

문제를 확인해보자.

 

 

Flask로 작성된 Image Viewer 서비스에서 SSRF 취약점을 이용해 /app/flag.txt에 위치한 플래그를 획득하는 문제이다.

접속해보자.

 

 

Image Viewer를 클릭하면 해당 페이지가 나온다. 기본으로 입력된 경로 url로 View 버튼을 클릭해보자.

 

 

url 파라미터 값을 변조하여 상위 디렉토리로 올라가 /flag.txt를 접근해보자.

 

택도 없다. 소스코드를 확인해보자.

 

#!/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)

 

url 파라미터 값으로 전달된 값의 첫번째 값이 "/"이면 url = "http://localhost:8000" + url 가 된다.

url 값에 "localhost" 또는 "127.0.0.1"이 포함되어 있으면 error.png를 반환하고 함수를 종료한다.

 

함수가 종료되지 않으면, data 값에 localhost에 위치한 url 파라미터에 해당하는 경로의 값을 가져오고

해당 값을 base64로 디코딩하여 이미지를 출력한다.

 

localhost의 IP는 127.0.0.1 이며, local port는 1500~1800의 랜덤 값이다.

 

flag.txt는 http://127.0.0.1:{1500~1800}/flag.txt로 접근해야 하는 것 같다.

하지만 127.0.0.1이 필터링되기 때문에 HEX 인코딩을 사용해보자.

해당 인코딩을 사용하면, 127.0.0.1은 0x7f000001로 표기할 수 있다.

 

hex 인코딩이 먹히는지 확인하기 위해 8000포트로 접근하여 기존 /static/dream.png 이미지를 읽어보자.

http://0x7f000001:8000/static/dream.png

 

 

정상적으로 /static/dream.png 이미지가 읽어진다.

그렇다면 로컬 통신하는 port로 접근하여 다시 해당 이미지를 읽어보자.

 

마찬가지로 알고 있는 이미지 경로인 /static/dream.png 를 사용해서 1500~1800 값을 무작위 대입 해보자.

http://0x7f000001:{1500~1800}/static/dream.png

 

인트루더로 port 무작위 대입 결과 8000번 port와 동일한 응답 값을 출력하는 1558 port를 확인했다.

1558 port로 접근해서 flag.txt 파일을 읽어보자.

http://0x7f000001:1558/static/dream.png

 

 

이미지 data 값으로 전달된 base64 인코딩된 값을 디코딩해보자.

 

 

플래그가 나왔다.

 

문제풀이 끗

복사했습니다!