풀이일 : 2023/07/10
먼저 문제 링크에 접속해 본다.
Image Viewer를 클릭해보면 다음과 같은 화면이 나온다.
Not Found X 가 화면에 출력된다.
문제 소스코드를 살펴보며 힌트를 찾아보자.
@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
)
"/img_viewer" 페이지의 POST 메소드 부분을 보면, 일단 사용자로부터 입력받은 url을 가져온다.
url이 "/"로 시작된다면, url은 "http://localhost:8000"에 입력받은 url을 합친 것이 된다.
url에 "localhost"나 "127.0.0.1"이 있다면, 에러 이미지를 화면에 띄우게 된다.
위에서 보았던 에러 이미지가 바로 이 이미지인 것 같다.
-> 따라서 공격 시에는 이를 우회해 주어야 하므로 127.0.0.1의 hex 값인 0x7f000001을 사용하도록 하자.
에러가 나지 않는다면 위에서 만든 url(localhost + 사용자의 입력) 경로대로 localhost 안의 사용자 입력 경로 값을 가져온다.
localhost의 주소는 "127.0.0.1"이고, 포트 번호는 1500~1800 중 랜덤한 값이다.
사용되고 있는 포트 번호를 알아내 보자.
burp suite를 사용해보자.
intercept를 통해 패킷을 복사하고, url을 수정하여 공격 payload를 만들었다.
위의 이미지에서 초록으로 표시된 8000을 1500부터 1800까지 1씩 올려가며 결과를 관찰할 수 있도록 설정 해준다.
Request가 0, 즉 8000 그대로일 때와 Response의 결과가 같은 포트 번호를 알아냈다!
1739번이 랜덤으로 배정되어 사용되고 있었다.
다른 Request의 경우 길이가 다른데, Response를 Render해보면 에러 이미지가 떠있는 것을 볼 수 있다.
-> Response의 img 소스로 Base64 인코딩된 flag 값이 왔다.
flag는 DH{43dd2189056475a7f3bd11456a17ad71}