Lỗ hổng client-side template injection (CSTI) phát sinh khi các framework template phía client nhúng động đầu vào của người dùng vào các trang web. Trong quá trình render, các framework này đánh giá các biểu thức template, có thể bị kẻ tấn công khai thác để chèn mã độc hại, có khả năng dẫn đến thực thi mã tùy ý và các cuộc tấn công cross-site scripting (XSS).
Mặc dù client-side template injection là một vấn đề chung, chúng ta sẽ tập trung vào các ví dụ từ framework AngularJS vì đây là framework phổ biến nhất.
AngularJS
AngularJS là một framework JavaScript được sử dụng để xây dựng các ứng dụng web động. Các khái niệm cốt lõi chính cho người mới bắt đầu bao gồm:
Two-way Data Binding: Tự động đồng bộ hóa dữ liệu giữa model (dữ liệu) và view (giao diện người dùng), do đó những thay đổi ở một bên sẽ được phản ánh ở bên kia.
Directives: Các dấu hiệu đặc biệt (thuộc tính, phần tử hoặc lớp) mở rộng khả năng của HTML, cho phép các nhà phát triển tạo ra hành vi tùy chỉnh trong DOM.
Controllers: Các hàm JavaScript quản lý logic đằng sau view. Chúng kiểm soát dữ liệu và hành vi của ứng dụng.
Services: Các thành phần có thể tái sử dụng để xử lý logic nghiệp vụ, chẳng hạn như tìm nạp dữ liệu hoặc xử lý đầu vào. Các service có thể được chèn vào các controller hoặc các service khác.
Modules: Các container để tổ chức một ứng dụng AngularJS, nhóm các thành phần liên quan như controller, service và directive.
Dependency Injection: Một mẫu thiết kế cho phép AngularJS quản lý và chèn các phụ thuộc (như các service) vào các thành phần một cách tự động.
Filters: Được sử dụng để định dạng dữ liệu trước khi hiển thị nó trong view, chẳng hạn như định dạng ngày hoặc số.
Templates: Các view HTML với các biểu thức và directive AngularJS được nhúng, xác định cấu trúc của giao diện người dùng.
Expressions
Trong Angular, một biểu thức là một đoạn mã được viết bên trong dấu ngoặc nhọn kép ({{ }}) trong template HTML. Các biểu thức AngularJS rất giống với các biểu thức JavaScript: chúng có thể chứa các giá trị cố định, toán tử và biến.
Nếu không có directive ng-app, các biểu thức AngularJS được coi là văn bản thuần túy và sẽ không được đánh giá. Khi có ng-app, AngularJS khởi tạo ứng dụng và xử lý các biểu thức. Angular thay thế biểu thức bằng kết quả đã được đánh giá trong DOM.
Note
Directive ng-app phải có mặt trong hệ thống phân cấp DOM phía trên biểu thức. Thông thường, các ứng dụng Angular đặt ng-app trong thẻ <html> hoặc <body> gốc.
Đầu ra cuối cùng được hiển thị trên trang là My first expression: 2.
Ví dụ sau đây minh họa cách nhập framework AngularJS có thể gây ra các lỗ hổng. Nếu đầu vào của người dùng được nhúng mà không có sự làm sạch phù hợp, bất kỳ ai chèn dấu ngoặc nhọn kép ({{ }}) đều có thể thực thi các biểu thức Angular:
Mặc dù các biểu thức Angular một mình có tác động hạn chế, việc kết hợp chúng với một sandbox escape có thể cho phép thực thi JavaScript tùy ý, dẫn đến thiệt hại đáng kể.
Sandbox
AngularJS sandbox là một cơ chế bảo mật được thiết kế để hạn chế quyền truy cập vào các đối tượng có khả năng gây nguy hiểm, như window hoặc document, và các thuộc tính nhạy cảm, chẳng hạn như __proto__, trong các biểu thức template AngularJS.
Sandbox hoạt động bằng cách phân tích một biểu thức, viết lại mã JavaScript, và sau đó sử dụng các hàm khác nhau để kiểm tra các đối tượng hoặc hoạt động nguy hiểm.
Với ví dụ trên sử dụng biểu thức {{ 1 + 1 }}, nếu chúng ta đặt một điểm dừng tại dòng 13275 của tệp angular.js, chúng ta có thể thấy mã được viết lại của biểu thức này:
Khi cố gắng lấy hàm tạo Function bằng biểu thức {{constructor.constructor('alert(1)')()}}, console sẽ ném ra một lỗi như sau:
<a class='gotoLine' href='#"Error: [$parse:isecfn'>"Error: [$parse:isecfn</a> Referencing Function in Angular expressions is disallowed! Expression: constructor.constructor('alert(1)')()http://errors.angularjs.org/1.4.6/$parse/isecfn?p0=constructor.constructor('alert(1)')() at https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.js:68:12 at ensureSafeObject (https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.js:12524:13) at fn (eval at compile (https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.js:13275:15), <anonymous>:4:172) at Object.expressionInputWatch <a class='gotoLine' href='#as get'>as get</a> (https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.js:14235:31) at Scope.$digest (https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.js:15751:40) at Scope.$apply (https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.js:16030:24) at bootstrapApply (https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.js:1660:15) at Object.invoke (https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.js:4476:17) at doBootstrap (https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.js:1658:14) at bootstrap (https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.js:1678:12)"]
Info
Giải thích biểu thức
constructor.constructor sẽ trả về hàm tạo Function:
Hàm ensureSafeObject() kiểm tra xem đối tượng có phải là hàm tạo Function, đối tượng window, một phần tử DOM hay hàm tạo Object không. Nếu bất kỳ kiểm tra nào là đúng, nó sẽ ném ra một ngoại lệ và ngừng thực thi biểu thức. Nó cũng ngăn chặn quyền truy cập vào các biến toàn cục bằng cách làm cho tất cả các tham chiếu đến các biến toàn cục trông giống như một thuộc tính đối tượng.
ensureSafeMemberName(): đảm bảo rằng các thuộc tính JavaScript đang được truy cập là an toàn bằng cách chặn các thuộc tính nguy hiểm như __proto__.
ensureSafeFunction(): ngăn chặn các lệnh gọi hàm không an toàn, chẳng hạn như gọi hàm tạo Function hoặc sử dụng các phương thức như call(), apply(), hoặc bind().
Escaping the Sandbox
Sandbox escape xảy ra khi kẻ tấn công thao túng một môi trường sandbox để coi một biểu thức độc hại là vô hại. Một phương pháp phổ biến khai thác một hàm charAt() toàn cục đã được sửa đổi, như được minh họa ở đây:
'a'.constructor.prototype.charAt = [].join
Trong cuộc tấn công này, hàm charAt(), thường trả về một ký tự duy nhất từ một chuỗi, bị ghi đè bằng [].join. Điều này khiến charAt() trả về toàn bộ chuỗi thay vì chỉ một ký tự.
Ví dụ, hãy xem xét đoạn mã sau. Nếu this.index là 9, biến ch sẽ chứa chuỗi 'x9=9a9l9e9r9t9(919)' thay vì một ký tự duy nhất:
while (this.index < this.text.length) { var ch = this.text.charAt(this.index); if (ch === '"' || ch === "'") { this.readString(ch); } else if (this.isNumber(ch) || (ch === '.' && this.isNumber(this.peek()))) { this.readNumber(); } else if (this.isIdent(ch)) { this.readIdent(); } // ...}
Trong đoạn mã này, charAt() được dự định để trích xuất các ký tự riêng lẻ từ this.text để kiểm tra kiểu. Tuy nhiên, do bị ghi đè, charAt() trả về một chuỗi dài thay vì một ký tự duy nhất.
Sự sửa đổi này có thể khiến Angular hiểu sai các biểu thức như alert(1) là các định danh, không phải là các lệnh gọi hàm, do kiểm tra isIdent():
Hàm isIdent() kiểm tra xem ch có phải là một ký tự định danh hợp lệ không (ví dụ: một chữ cái hoặc dấu gạch dưới). Vì charAt() bây giờ trả về 'x9=9a9l9e9r9t9(919)', việc so sánh dựa trên các giá trị Unicode của các ký tự trong chuỗi. Kết quả là:
'a' (với giá trị Unicode thấp hơn) được coi là nhỏ hơn 'x9=9a9l9e9r9t9(919)'.
'z' (với giá trị Unicode cao hơn) được coi là lớn hơn 'x9=9a9l9e9r9t9(919)'.
Điều này khiến kiểm tra isIdent() luôn trả về true, coi bất kỳ chuỗi nào cũng là một định danh hợp lệ và cho phép các biểu thức có khả năng độc hại vượt qua sandbox.
Note
Lưu ý rằng chúng ta cần sử dụng hàm $eval() của AngularJS vì việc ghi đè hàm charAt() sẽ chỉ có hiệu lực khi mã trong sandbox được thực thi.
Constructing an Advanced AngularJS Sandbox Escape
Một trang web có thể chặn việc sử dụng dấu ngoặc đơn hoặc kép. Trong trường hợp này, chúng ta có thể sử dụng các hàm như String.fromCharCode() để tạo các ký tự. Mặc dù AngularJS hạn chế quyền truy cập vào hàm tạo String trong các biểu thức, chúng ta có thể vượt qua điều này bằng cách sử dụng thuộc tính constructor của một chuỗi.
Trong một sandbox escape thông thường, chúng ta sẽ sử dụng $eval() để thực thi một payload JavaScript, nhưng trong một số tình huống, $eval() không được định nghĩa. May mắn thay, chúng ta có thể sử dụng bộ lọc orderBy thay thế. Cú pháp thông thường cho bộ lọc orderBy là:
[123] | orderBy: 'Some string'
Lưu ý rằng toán tử | ở đây là một bộ lọc trong AngularJS, không phải là một phép OR bit như trong JavaScript. Trong ví dụ này, mảng [123] được truyền cho bộ lọc orderBy, với chuỗi 'Some string' làm đối số của nó. Mặc dù orderBy thường được sử dụng để sắp xếp các đối tượng, nó cũng có thể đánh giá các biểu thức, cho phép chúng ta sử dụng nó để truyền một payload.
Lab: Reflected XSS with AngularJS Sandbox Escape without Strings
Ứng dụng có script phía client sau đây sử dụng khái niệm controller và service $parse:
Kiểm soát dữ liệu của ứng dụng AngularJS và chúng chỉ là các đối tượng JavaScript thông thường.
Đây là một ví dụ đơn giản:
<!DOCTYPE html><html><head> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script></head><body><div ng-app="myApp" ng-controller="myCtrl"> <!-- Binding a simple expression --> <p>{{ greeting }} {{ name }}!</p></div><script> // Define an AngularJS module and controller var app = angular.module('myApp', []); app.controller('myCtrl', function($scope) { $scope.greeting = "Hello"; $scope.name = "World"; });</script></body></html>
Giải thích:
{{ greeting }} {{ name }} là biểu thức template AngularJS. Nó tự động đánh giá các giá trị của greeting và name từ $scope và hiển thị chúng.
Khi được render trong trình duyệt, điều này sẽ hiển thị Hello World!.
$parse Service
Service $parse trong AngularJS là một hàm nhận một chuỗi biểu thức và trả về một hàm có thể được sử dụng để phân tích và đánh giá biểu thức. Chuỗi biểu thức có thể chứa các biến, toán tử và các lệnh gọi hàm.
Để sử dụng hàm $parse, trước tiên nó phải được chèn dưới dạng một phụ thuộc vào một thành phần AngularJS, chẳng hạn như một controller. Đây là cách chúng ta có thể chèn nó vào một controller:
angular.module('myModule', []).controller('MyController', function($scope, $parse) { // Use the $parse service here});
Cú pháp của $parse:
var fn = $parse(expression);var result = fn(context, locals);
Ví dụ, khi truyền biểu thức 1+1 cho hàm $parse, nó sẽ tạo ra một hàm như sau:
Và biểu thức nằm dưới script sẽ render thuộc tính $scope.value, sẽ thực thi payload thông qua lệnh gọi được thực hiện trên hàm $parse():
<h1 ng-controller="vulnCtrl" class="ng-scope ng-binding">10 search results for {{value}}</h1>
AngularJS CSP Bypass
Tùy thuộc vào chính sách, CSP có thể chặn các sự kiện JavaScript. Tuy nhiên, AngularJS định nghĩa các sự kiện riêng của nó có thể được sử dụng thay thế. Khi ở trong một trình xử lý sự kiện, AngularJS cung cấp một đối tượng $event đặc biệt, tham chiếu đến đối tượng sự kiện của trình duyệt.
Trong Chrome, đối tượng $event có một thuộc tính path, là một mảng các đối tượng kích hoạt sự kiện. Mục cuối cùng trong mảng này luôn là đối tượng window, có thể được sử dụng để thoát khỏi sandbox. Bằng cách truyền mảng này cho bộ lọc orderBy, chúng ta có thể truy cập đối tượng window và sử dụng nó để thực thi các hàm toàn cục như alert(). Đoạn mã sau đây minh họa điều này:
Hàm from() được sử dụng để chuyển đổi một đối tượng thành một mảng và sau đó áp dụng một hàm (được chỉ định trong đối số thứ hai) cho mỗi phần tử. Ở đây, chúng ta đang sử dụng nó để gọi hàm alert(). Chúng ta không thể gọi trực tiếp alert() vì sandbox AngularJS sẽ phát hiện việc sử dụng đối tượng window. Bằng cách sử dụng from(), chúng ta ẩn đối tượng window khỏi sandbox, cho phép chúng ta chèn mã độc hại.
Có một số cách để ẩn đối tượng window khỏi sandbox AngularJS. Một phương pháp là sử dụng hàm array.map() như sau:
[1].map(alert);
Hàm map() gọi alert() cho mỗi mục trong mảng, bỏ qua sandbox vì nó sử dụng hàm alert() mà không tham chiếu trực tiếp đến window.
Lab: Reflected XSS with AngularJS Sandbox Escape and CSP
Chúng ta không thể gọi trực tiếp hàm alert() nên chúng ta cần gán nó cho một biến (z) và sau đó gọi biến đó.
Cuối cùng, id=x kết hợp với fragment #x là một cách thay thế để tập trung vào phần tử mà không cần sử dụng thuộc tính autofocus. Bằng cách này, chúng ta có thể giảm số lượng ký tự trong payload.
Info
Phiên bản hiện tại của lab có thuộc tính HttpOnly trên cookie nên chúng ta không thể cảnh báo cookie cục bộ. Tuy nhiên, nó có thể hoạt động ở phía nạn nhân.
How to Prevent Client-side Template Injection Vulnerabilities
Để ngăn chặn các lỗ hổng client-side template injection, hãy tránh sử dụng đầu vào không đáng tin cậy của người dùng để tạo các template hoặc biểu thức. Nếu không thể, hãy lọc cú pháp biểu thức template khỏi đầu vào của người dùng trước khi nhúng nó.
Chỉ mã hóa HTML không đủ để ngăn chặn các cuộc tấn công này, vì các framework giải mã nội dung trước khi xử lý các biểu thức template.