前言
企业介绍:nVisium是一家位于弗吉尼亚州赫恩登的私营应用安全公司。自2009年以来,nVisium一直在识别自定义应用程序中的安全问题,隐私问题和合规性差距。我们已经发展壮大,为财富500强企业和家庭品牌提供应用程序和云安全解决方案,以便在企业或其用户受到损害之前解决漏洞。
网络是一个聚集想法、技术、bug、以及很多各式各样JavaScript框架的游乐场。当今最受欢迎的Web框架之一是Angular,这是一个通过一系列规范模式简化前端Web应用程序开发的平台。作为应用程序安全专业人员,了解此框架的基础知识可以在应用程序安全评估的初始阶段为你提供优势。
这是两篇博客文章中的第一篇,旨在帮助渗透测试人员和应用程序安全研究人员对基于应用程序的角度进行更深入的分析。第一部分将向您介绍AngularJS和Angular的构建模块。我们将重点介绍每个概念,以帮助指导您的安全评估。然后,在第二部分中,我们将展示一些可以从浏览器控制台动态调试Angular代码的方法。有了这些知识,我们就可以开始进行多角度深入分析。
请注意,这不是一篇关于“如何在Angular中利用XSS或XYZ漏洞”的文章。有很多专门讨论Angular特定漏洞的文章以及快速找到它们的方法。相反,本博文的目的是了解Angular应用程序的工作原理、常见模式以及从应用程序安全角度动态调试Angular的方法。
在我们继续之前,请注意在示例代码中使用了Typescript for Angular 2+,因为大多数Angular应用程序都是用该语言编写的。但是,当代码在浏览器中呈现时,你将看到已编译的JavaScript。当应用程序从Typescript转换为JavaScript时,生成的代码不是最可读或最美观的,因此为了可读性,我们将坚持使用Typescript。
什么是Angular Anyways?
Angular是现代JavaScript框架之一,允许开发人员创建具有许多特色功能的快速响应网站。Angular还使开发人员能够分离前端和后端逻辑。Angular的核心是能够自动地在UI元素中反映应用程序状态的变化。
例如,假设应用程序应该每60秒从服务器中提取一次比特币价格。如果应用程序使用jQuery,则每次价格更改时,前端代码都必须操纵DOM以反映页面上的价格更新。相比之下,使用Angular的应用程序可以更新用于显示当前价格的变量,比如$('#price-field').text(newPrice);
,新价格将直接显示在页面中,而不必通过执行类似操作来手动设置它。
Angular版本
当你作为开发人员,测试人员或应用程序安全研究人员开始研究Angular时,您可能会注意到的第一件事就是Angular的许多版本已在相对较短的时间内发布。但是,我们可以说Angular只有两个主要版本。Angular 1.x(或AngularJS)和Angular 2+(或Angular)。使用AngularJS的应用程序大部分依赖于ECMAScript 5(ES5); 所以在某种程度上,它是有限制的,并且通常不利于自ES6以来的JavaScript功能,例如类,arrow函数等。
通常Angular版本1.x被引用为AngularJS,版本2及更高版本被引用为Angular。我们将尝试指定概念或示例仅适用于一个版本,但是当我们说“Angular”时,我们指的是Angular 1.X(AngularJS)和Angular 2+(Angular)。
Angular 1.X应用程序基于4种不同的结构构建:模块,控制器,服务和指令。
- 模块可以被认为是命名空间。
- 控制器是附加到DOM对象的函数,它们通过更新作用于这些元素的变量的函数来控制UI元素的行为。
- 服务在某种程度上是Angular应用程序的业务层,因为它们拥有直接处理数据的功能。
-
指令可用于创建自定义HTML元素和属性,这些元素和属性可以添加到附加了特定功能的任何视图中。例如,指令可以创建元素
<price-console />
以在页面上显示比特币的价格。
在较高的层面上,这些概念之间的关系可能如下所示:
当Angular 2问世时,模式发生了重大变化,以利用ES6的面向对象特性。在某种程度上,控制器和指令融合到Angular调用的组件中。
- 与控制器一样,组件控制页面上DOM元素的行为; 但是,组件通常使用模板来创建具有自己范围的自定义可重用HTML元素。此外,虽然AngularJS指令和控制器被定义为具有视图可用变量和闭包的函数,但组件被构建为JavaScript类。
在较高的层次上,Angular 2+应用程序看起来像这样:
但是版本4,5和6呢?版本3发生了什么?我们可以说这些版本在很大程度上是对Angular 2的改进,但模式大致相同。有趣的是,Angular 3从未存在过。
在这里,官方的Angular指南中包含一个很好的说明,解释Angular和AngularJS之间的主要差别。
现在让我们更详细地了解一些Angular核心概念,这些概念将使我们能够更好地理解Angular应用程序的高级结构。在我们完成每个概念时,我们将重点介绍每种方法如何帮助我们进行评估工作。
深入到Angular概念
作用域
Angular 1.x的核心是作用域的概念。每个应用程序都是作为一系列控制器构建的,每个组件都通过其作用域在页面上传递更改。每个控制器都可以通过自己的$scope
变量添加、删除和更新视图可用的变量。回到比特币价格示例,控制器可以像这样更新价格变量:
$scope.vm.btcPrice = data.btcPrice;
请注意,Angular 2+出于同样的目的使用this
而不是$scope
,因为组件是作为类而不是函数构建的。以上示例在Angular 2+应用程序中可能如下所示:
this.price = data.price;
Angular监视作用域中的变量,监听作用域更改并触发函数以响应这些更改。例如,只有在$scope.vm.isAdmin
等变量设置为true
时,才能显示页面中的元素。作为应用安全研究人员,最好将isAdmin
改为true
,以查看你是否可以访问任何感兴趣的页面元素。例如,您可以在页面上看到仅限管理员的菜单和功能。
Angular使用HTML属性ng-if
(Angular 1.X)和ngIf
(Angular 2+)来决定在页面中显示或不显示的内容(此属性显示或移除页面元素,而不是隐藏或取消隐藏它们)。更改isAdmin
为true
可能允许您查看页面上的元素,否则在渲染页面时,这些元素会被角度移除。此时,您可以开始点击查看是否触发了服务器中未得到充分保护的任何管理功能。
作为安全研究人员,您可以通过检查作用域来检查不同变量的状态。此外,作用域变量的更改可以级联一系列其他更改并触发各种功能。这可以通过几种不同的方式完成。要检查和操作UI元素的作用域,可以使用Angular浏览器检查器应用程序,例如Augary或ng-inspector。也可以使用浏览器控制台操作作用域变量,我们将在本文的第二部分中看到。
路由
路由的概念适用于Angular的所有版本。Angular决定使用路由器模块在页面上呈现什么视图和局部视图。例如,当您导航到www.example.com/#/users时,Angular会渲染/views/users.html,它由一个叫做UsersControllers
被的控制器调用或Angular 2+中的UserComponent
组件控制。
路由通常在设置应用程序模块的同一文件中声明,通常称为app.ts
或app.js
。根据用户权限,查找路由会给出一个目录和页面列表,您可以访问这些目录和页面,也可以不访问这些目录和页面。
在Angular 2+中,路由通常如下所示:
RouterModule.forRoot([
{ path: '', component: LoginComponent, pathMatch: 'full' },
{ path: '', component: HomeComponent, canActivate: [AuthGuard] },
{ path: 'login', component: LoginComponent, pathMatch: 'full' },
{ path: 'events', component: EventsComponent },
]),
在Angular 1.x中:
$routeProvider.
when("/dashboard", {
templateUrl: 'views/home.html',
controller: 'DashboardController',
controllerAs: 'vm'
}).
when("/login", {
templateUrl: 'views/login.html',
controller: 'LoginController',
controllerAs: 'vm'
}).
when('/admin', {
templateUrl: 'views/adminpanel.html',
controller: 'AdminPanelCtrl',
css: 'styles/css/adminpanel.css',
resolve: {
"currentAuth": [
"Auth", "AdminAuth", function (Auth, AdminAuth) {
var token = Auth.$getAuth().token;
return AdminAuth.isAdmin(token);
}
]
}
}).
otherwise({ redirectTo: '/' });
在最后一个实例中,我们声明了每个页面的控制器以及从视图中引用该控制器的别名。上面的代码还告诉我们,*路由管理面板是根据AdminAuth.isAdmin(token);
来解析的,返回true
还是false
。那么,我们可以用这些信息做些什么呢?我们的下一步可能是寻找并修改AdminAuth.isAdmin(token);
函数以便始终返回true
,以防我们可以访问仅供管理员查看的视图元素。
服务
服务通常在UI和DOM逻辑之外完成大部分繁重工作。服务作为单例注入组件和控制器,因此它们通常作为控制器之间共享数据的一种方式。你将看到应用程序的大部分REST逻辑由Angular服务处理,因此它们可以作为应用程序使用的任何API接口的一种文档形式。
服务不仅包含您可能无法通过其他方法(例如,蜘蛛或自动目录和文件枚举)找到的有趣网址的引用,它们还将向你展示如何正确格式化API请求。实际上,对Angular应用程序进行深入评估的最有力的部分之一是对服务进行审查,因为它们可以让您轻松地测试缺失的服务器端访问控制漏洞,而不需要服务器端源代码(或其他一些敏感/非公开的信息集)。
我们来看看Angular 2+服务:
export class UserService extends BaseService {
constructor(private http: HttpClient, private router: Router) {
super();
}
addUser(firstName: string, lastName: string, email: string) {
return this.http.post<any>('api/admin/adduser', {
first: firstName,
last: lastName,
email: email
})
.pipe(map((res: any) => {
// login successful if there's a jwt token in the response
if (res && res.code == 200) {
Notifications.show("User account created successfully.")
}
}));
}
}
从上面我们知道,要创建一个新用户,我们需要发送一个POST请求到/api/admin/adduser
其正文格式如下:{ "first":"George", "last":"Orwell", "email":"1984@example.com"}
。
在Angular 1中,服务看起来略有不同。让我们看一下连接到Firebase数据库的服务,以获得用户列表:
angular
.module('myApp.admin.services')
.factory('AdminService', ['$firebaseArray', '$firebaseObject', '$http', '$q',
function ($firebaseArray, $firebaseObject, $http, $q) {
var AdminService = {
allUsers: {},
getAllUsers: getAllUsers,
}
// Get a reference to the Firebase
var userProfilesRef = new Firebase("https://example.firebaseio.com/userprofiles/");
function getAllUsers() {
var deferred = $q.defer();
AdminService.allUsers = $firebaseObject(userProfilesRef);
Admin.allUsers.$loaded().then(function () {
deferred.resolve(AdminService.allUsers);
}, function (error) {
deferred.reject(error);
});
return deferred.promise;
}
上述服务逻辑告诉我们在example.firebaseio.com/userprofiles/上有一个Firebase端点,我们可以调用它来获取应用程序的用户列表。
服务还可以保存有趣的逻辑,用于与无服务器服务(如Firebase )通信。在某些情况下,您可能会发现整个数据层逻辑都在客户端代码中,在这种情况下,您可以通过从浏览器控制台操作服务功能来枚举NoSQL数据库。我们将在本文的第二部分看到如何做到这一点。
控制器
如前所述,控制器是Angular 1.x概念。AngularJS应用程序使用控制器来操作DOM元素,并通过Angular的 $scope
作用域设置侦听器和变量。例如,控制器可以保存页面中用户界面元素使用的变量,例如表单值和动态URLs,并且它们可以使用服务将这些表单提交给运行后端代码的服务器。此外,控制器可以保存作用域的变量,当这些变量发生变化时,会改变应用程序的行为。如果你需要找出是什么触发了页面上的特定更改,请寻找控制器逻辑。
组件
自从Angular 2发布后,Angular不再使用控制器,而是使用组件。组件控制视图或视图的一部分。组件不是作为带有闭包的函数来构建控制器,而是作为JavaScript类来构建,这些类可以保存自定义Web元素的标记模板、仅限于某个组件的CSS样式、该组件使用的变量以及控制该组件行为的逻辑。和AngularJS控制器一样,当你试图确定哪些变量可用于页面的特定部分时,你需要检查组件,以及当这些变量发生变化时会发生什么变化。
认证模式
大多数身份验证逻辑通常位于专用于这些目的的服务中。例如,以下是Angular2 +身份验证服务的常见做法:
...
export class AuthenticationService extends BaseService {
constructor(private http: HttpClient, private router: Router) {
super();
}
...
login(username: string, password: string) {
return this.http.post<any>('/api/auth/login', { username: username, password: password, email: email })
.pipe(map((res: any) => {
// successful if there's a jwt token in the response
if (res && res.auth_token) {
// store username and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('currentUser', JSON.stringify({ username, auth_token: res.auth_token }));
}
}));
}
logout() {
// remove user from local storage to log user out
localStorage.removeItem('currentUser');
this.router.navigateByUrl('/login');
}
//...
}
上面告诉我们,在成功进行身份验证尝试时,currentUser
会保存一个对象localStorage
。注销函数简单地删除了该对象,因此迫使客户端向应用编程接口发送另一个请求来获取新令牌。这也告诉我们,注销后会话在服务器中可能不会失效。
请注意注销功能是如何强制重定向到登录页面的。如果我们在本地存储中手动添加currentUser
会发生什么?应用程序是否允许您查看经过身份验证的路由?这些只是您在检查上述认证服务后可以查看的一些内容。
Angular 1.X中存在类似的模式,其在路由更改时检查身份验证。
run(["$rootScope", "$location", "$templateCache", '$http', function ($rootScope, $location, $templateCache,$http) {
$.material.init();
$rootScope.$on("$routeChangeError", function (event, next, previous, error) {
if (error === "AUTH_REQUIRED" || error.status === 401) {
$location.path("/login");
}
});
}]);
另一种常见模式是使用Angular 拦截器进行授权。HTTP拦截器是每次提交请求和/或收到服务器响应时运行的函数。两种常见的拦截器是JWT拦截器和错误拦截器。错误拦截器可用于检测代码为401 (未授权)的响应,以通知客户端用户必须注销并重定向到登录页面:
export class ErrorInterceptor implements HttpInterceptor {
constructor(private authenticationService: AuthenticationService) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(catchError(err => {
if (err.status === 401) {
// if any response has a code of 401, logout the user and reload the page, forcing a redirect to the login page
this.authenticationService.logout();
location.reload(true);
}
const error = err.error.message || err.statusText;
return throwError(error);
}))
}
}
虽然上面的拦截器看的是HTTP响应,但是JWT拦截器的工作是在所有的HTTP请求中包含一个JWT头令牌:
export class JwtInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// add authorization header with jwt token if available
let currentUser = JSON.parse(localStorage.getItem('currentUser'));
if (currentUser && currentUser.token) {
request = request.clone({
setHeaders: {
Authorization: `Bearer ${currentUser.token}`
}
});
}
return next.handle(request);
}
}
同样,你可以使用这些信息来了解应用程序如何处理身份验证,如果可能的话,尝试更改功能,看看你是否能注意到对你的评估有价值的东西。
最后
希望这些信息能为你提供一些见解,有助于你将来对Angular应用的安全性评估。当你在网络应用程序中寻找安全漏洞时,理解Angular可以增加你的直觉。在这篇文章的第二部分,我们将探索一些方法,使用Angular提供的调试功能从浏览器控制台动态测试应用程序。