Web,Mobile/Tech

JSFuck과 Function 객체 생성자를 이용한 XSS Bypass

LimeLee 2023. 7. 19. 18:16

0. 서론 

스크립트 태그 영역 안에서 별도의 태그나 이벤트 핸들러를 사용하지 않고 XSS 취약점을 터트리는 경우 서버에서 XSS 스크립트 실행에 사용해야 할 함수(여기서는 'alert' 를 예시로 든다.) 값 자체를 필터링하고 있다면 이를 우회하기가 쉽지가 않다. 필터링되어 있지 않은 비슷한 함수 confirm, prompt 등으로 대체하거나 서비스 자체의 필터링을 이용한 방법*을 이용하는게 아니라면 alert 자체를 우회하긴 어렵다.

 

* 페이지를 불러오기 전 백엔드에서 "abc"를 공백으로 치환하는 서비스 자체의 시큐어코딩이 되어있을 경우 "alabcert"를 입력하여 우회 등등



그러나 해당 스크립트를 문자열을 실행할 수 있다면 이런 제한에서 비교적 자유로워 질 수 있다.

우회 예시
=> 문자열 합치기
var a="al";var b="ert(1)";
"a".concat("lert(1)");
"al"+"ert(1)";

=> Unicode escape sequence
"ale\x72t(1)"

문자열을 스크립트로 실행하는데 eval이라는 함수를 자주 사용한다.
그러나 eval 함수도 필터링 되어 있는 경우엔 어떻게 우회할 수 있을까.

 

상황 예시 

<script>
var a = "사용자의 입력 값";
</script>

필터링되는 문자열
String, fromCharcode, console.log, eval, alert,', prompt, confirm, location, document


JSFuck 이라는 프로그래밍 스타일을 통해 우회가 가능하다.
대략적인 원리는 [, ], (, ), +, ! 6개의 문자로 문자열로 갖가지 트릭을 이용해 모든 문자을 만들어내는 일종의 식을 짠다. 

(![]+[[]])[+!+[]] = "a"

"a"+"l" = "al" = (![]+[[]])[+!+[]]+(![]+[[]])[!+[]+!+[]]
(![]+[[]])[+!+[]]+(![]+[[]])[!+[]+!+[]]+(!![]+[[]])[!+[]+!+[]+!+[]]+(!![]+[[]])[+!+[]]+(!![]+[[]])[+[]] = "alert"


이런식으로 문자열 "alert(1)" 을 실행하는 함수를 생성한 것이다.

 

JSFuck 사이트에서 원하는 문자열을 스크립트로 실행하는 페이로드를 자동으로 생성해주며 해당 페이로드를 삽입할 수 있다면 실행이 가능하다.

간단한 "alert(1)"을 실행시키는 페이로드를 생성하여 콘솔에 입력해보니 스크립트가 실행된다.

 

하지만 한계점이라면 있다. 간단한 "alert(1)"을 실행시키는 페이로드조차 이정도의 길이를 가진다. "alert("xss")"정도만 되어도 엄청난 길이의 페이로드를 자랑하며, 이정도의 길이의 get 파라미터를 수용할 수 없는 서비스에서는 이런 공격이 성공하지 않을 것이다. 

페이로드를 보면 eval을 사용하지 않고 문자열을 스크립트로 실행하였다.

JSFuck 이 동작하는 원리를 이해한다면 JSFuck의 실제로 사용하기 힘든 페이로드 길이를 해결하면서 eval이 필터링 되어있는 환경에서 페이로드를 짜는데 도움이 될 것이다.


JSFuck github 페이지에 가면 이렇게 되어있다 Run => []["filter"]["constructor"]( CODE )()
"alert(1)"을 실행하는 페이로드를 보기 편하게 치환하면 []["flat"]["constructor"]("return eval")()("alert(1)") 이다.

 

두 코드를 분석하여 어떻게 문자열을 실행시킬 수 있었는지 확인해보도록 한다.

 

1. 프로토타입

 

Javascript는 객체지향 프로토타입 기반 언어이다.

객체지향 언어의 특징 중 하나로는 '상속'이 있다. 기존 클래스의 프로퍼티, 메소드를 사용할 수 있는 것을 의미하는데 Javascript 같은 프로토타입 기반 언어는 이 상속의 개념을 원형 객체로 부터 복제하여 새로운 객체를 만들어내는 것으로 구현했다. 이 원형 객체를 "프로토타입" 이라고 한다.

 

JSFuck 페이지에서 만든 페이로드 "[]["flat"]["constructor"]("return eval")()("alert(1)")"의 가장 앞부분 "[]" 는 빈 배열이다.

Javascript에서 별도의 정의 없이 배열을 생성할 수 있는 건 Javascript 에 내장되어 있는 Built in Object인 Array 객체를 프로토타입 삼아 새로운 배열 객체를 생성했기 때문이다.

 

Built in Object는 ECMAScript 명세에 정의된 객체를 말하며, Linux 환경의 서버에서는 당연하게 'cd' 명령어를 사용하듯 Javascript를 사용하는 환경에서는 언제나 사용할 수 있는 것이 특징이다.

 

Array라고 하는 전역 객체를 기반으로 만들어진 빈 배열 "[]" 프로토타입 기반 언어의 특징에 따라 원형 객체 Array의 프로퍼티나 메소드에 접근할 수 있다. 

빈 배열 뒤에 나오는 ["flat"]이나 ["filter"]나 전부 Array 전역 객체 안에 있는 메소드에 접근한 것이다.

 

자세한 것은 Array 객체의 문서에서 확인할 수 있다.

 

2. 생성자

[]["flat"] 까지 Array 객체의 flat 메소드에 접근한 것까지 이해를 했다면 그 뒤의 ["constructor"] 부분도 확인을 해보자. 위의 문서를 확인해보면 메소드 외 Constructor 항목이 존재하는 것을 확인할 수 있다.

 

Constructor는 생성자로 새로운 객체를 생성하는 함수로 빈 배열의 경우에도 Array 객체 안에 있는 constructor 생성자를 통해 만들어진 것이다.

 

[]["flat"]["constructor"] 는 어느 객체의 생성자에 접근한 것일까?

 

모든 Javascript 의 함수는 Function 객체라고 한다. 이는 Function 객체의 문서에서 확인 가능하다. 

즉, Array 안에 있는 flat(), filter() 메소드 또한 Function 객체를 원형으로 하여 만들어진 객체라는 것을 알 수 있다.

 

[]["flat"]을 통해 flat 메소드의 원형이 되는 Function 객체에 접근할 수 있고 그 객체 안에 있는 constructor 생성자에 접근하여 새로운 Function 객체를 생성할 수 있다.

 

[]["flat"]["constructor"]("alert(1)")() alert(1)이라는 함수를 생성하여 실행한 것이고

[]["flat"]["constructor"]("return eval")()("alert(1)") eval을 반환하는 함수를 생성하여 실행한 후 eval("alert(1)") 를 또 실행한 것이다.

 

Built in Object의 생성자와 Javascript 언어의 특징인 프로토타입을 활용한 방법이므로 Javascript를 지원하는 환경이면 사용가능한 우회 방법이다.

 

3. 활용

프로토타입과 constructor 개념을 알았다면 여러가지로 활용 가능하다.

 

- 표기법을 이용한 우회

Javascript에서 객체 내 데이터에 접근하는 방법으로 점 표기법(Dot notation) 또는 대괄호 표기법(Bracket notation)을 사용하여 호출할 수 있다.

 

[]["flat"]["constructor"]("alert(1)")() 같은 경우는 대괄호 표기법을 이용했다고 볼 수 있다.

만약에 [, ] 대괄호를 필터링하는 서비스가 있다면 점 표기법을 이용하여 "a".concat 등으로 우회할 수 있다.

이미 정의되어진 객체에 접근해도 좋다.

 

"a".concat.constructor(...)()
Array.fill.constructor(...)()
Function.constructor(...)()

 

- 생성자의 문법을 이용한 우회

대괄호와 점(.) 모두 제한당했다면 생성자의 다른 문법을 사용해도 된다.

생성자 문서를 Syntax 부분을 확인하면

new Function(functionBody)
new Function(arg0, functionBody)
new Function(arg0, arg1, functionBody)
new Function(arg0, arg1, /* … ,*/ argN, functionBody)

Function(functionBody)
Function(arg0, functionBody)
Function(arg0, arg1, functionBody)
Function(arg0, arg1, /* … ,*/ argN, functionBody)

익숙한 형태의 예시가 보인다.

그리고 "Function() can be called with orwithout new. Both create a new Function instance." 라는 문구를 찾을 수 있다.

var test = new Function(1);
var test = Function(1);

 

뭔 말이냐 하면, 위의 두 코드는 동일한 동작을 한다. new를 생략해서 쓸 수 있다.

 

이 형태를 사용하면 굳이 빈 배열을 생성하고 객체 프로토타입의 메소드에 접근해서 생성자에 접근할 필요없이 Function 객체의 생성자를 호출하여 함수를 실행할 수 있다.

 

new Function("alert(1)")()
Function("alert(1)")()