AngularJS DOM XSS漏洞实战:从表达式注入到沙箱逃逸

📅 2026/7/4 15:54:44
AngularJS DOM XSS漏洞实战:从表达式注入到沙箱逃逸
1. 项目概述与核心价值最近在复盘一些经典的Web安全漏洞案例发现很多朋友对DOM型XSS特别是结合了AngularJS框架的案例理解上总是隔着一层纱。大家可能对反射型、存储型XSS的利用点比较熟悉但一到DOM型尤其是涉及到前端框架的沙箱逃逸和表达式执行就容易犯迷糊。正好PortSwigger Web Security Academy也就是大家常说的PortSwigger Labs里有一个关于AngularJS DOM XSS的靶场它非常典型完美地展示了当开发者不当地信任了来自URL的输入并将其直接拼接进使用了ng-app指令的HTML中时会引发多么严重的安全问题。这个靶场不仅仅是找到一个弹窗alert(1)就结束了它更深入地考察了对AngularJS框架特性、JavaScript执行上下文以及浏览器DOM解析顺序的理解。通过详细拆解这个靶场我们不仅能掌握一种特定漏洞的利用方法更能举一反三理解现代单页面应用SPA中类似漏洞的审计思路。无论你是刚开始接触Web渗透测试的新手还是想深化对客户端漏洞理解的老兵这个案例都值得花时间彻底搞懂。2. 靶场环境与漏洞原理深度解析2.1 PortSwigger Labs与AngularJS简介PortSwigger Labs是Burp Suite创始人创建的免费、在线的Web安全学习平台。它的靶场环境模拟了真实世界中的各种漏洞从基础的SQL注入到复杂的OAuth逻辑缺陷应有尽有。其最大优点是“实战化”你需要像攻击真实网站一样去探测、分析、构造和利用漏洞最终达成明确的目标如弹出警告框、窃取Cookie等这比单纯看理论要深刻得多。AngularJS通常指1.x版本是一个由Google维护的前端MVC框架。它通过“指令”扩展HTML实现了数据双向绑定、依赖注入等强大功能。其核心机制之一是“表达式”双花括号{{ }}内的内容会被AngularJS解析并执行。例如{{ 11 }}会在页面中渲染为2。更关键的是AngularJS有一个名为$scope的对象它是视图和控制器之间的桥梁表达式正是在$scope的上下文中求值的。漏洞的根源在于AngularJS 1.x版本的设计中表达式的能力非常强大。如果攻击者能够控制一个被AngularJS解析的表达式就可以执行任意的JavaScript代码。常见的危险模式是开发者将用户输入如URL参数search未经任何处理就直接放入一个被ng-app指令包裹的DOM元素内部。一旦这个用户输入包含了AngularJS表达式框架就会忠实地执行它。2.2 DOM XSS与反射型XSS的本质区别这是很多人的困惑点。我们简单对比一下反射型XSS攻击载荷Payload由服务器“反射”回响应页面。服务器端脚本如PHP、JSP获取了用户输入如URL参数未经净化就直接拼接进HTML然后返回给浏览器。浏览器接收到的是一个“已经”包含了恶意脚本的完整HTML文档然后解析执行。数据流浏览器 - 网络请求 -服务器- 网络响应 - 浏览器解析执行。关键漏洞发生在服务器端的代码逻辑里。DOM型XSS攻击载荷的“执行”完全发生在客户端的浏览器中不经过服务器处理。服务器返回的是一个“干净”的、静态的或带有通用逻辑的HTML/JS文件。是客户端JavaScript可能是框架也可能是原生JS从DOM中提取数据如document.location.hash并以不安全的方式如innerHTML、eval()处理这些数据导致了代码执行。数据流浏览器 - 客户端JS读取DOM如URL- 不安全处理 - 浏览器执行。关键漏洞发生在客户端的JavaScript逻辑里。服务器可能完全不知情。本靶场属于DOM型XSS因为触发漏洞的AngularJS表达式解析是浏览器加载了AngularJS库之后在客户端完成的。服务器只是返回了一个包含ng-app的页面并将URL中的参数值原样放在了页面的某个位置。2.3 靶场漏洞场景模拟假设靶场应用有一个搜索功能URL结构类似https://0aXX00xx03f0xxxx.web-security-academy.net/?searchtest服务器端的响应可能如下html ng-app head titleAngularJS Search/title script src/resources/js/angular.js/script /head body div您搜索的关键词是{{ query }}/div !-- 其他内容 -- /body /html其中{{ query }}的值是由前端JavaScript从URL参数中提取并赋值的。漏洞代码可能看起来是这样// 模拟漏洞代码从URL获取search参数直接放入$scope var searchTerm new URLSearchParams(window.location.search).get(search); angular.module(myApp, []).controller(SearchController, function($scope) { $scope.query searchTerm; // 危险用户可控数据直接进入$scope });这样一来如果用户访问https://.../?search{{ 11 }}页面上显示的就将是2证明了表达式被执行。这就为执行任意JavaScript代码打开了大门。3. 实战利用步骤拆解与Payload构造3.1 信息收集与漏洞探测首先访问靶场地址。使用浏览器开发者工具F12是第一步。查看页面源码右键点击页面选择“查看页面源代码”。注意寻找html或body标签是否包含ng-app、ng-cspContent Security Policy可能会限制利用等AngularJS指令。是否引入了angular.js库及其路径。寻找可能反射用户输入的位置注意{{、}}的迹象。在真实靶场中源码里可能看不到直接的{{query}}因为它是通过JS动态绑定的但查看源码能确认AngularJS环境。分析网络请求在“网络”(Network)标签页中查看页面加载的资源和初始的文档响应。确认服务器返回的HTML是“静态”的没有将我们的参数直接嵌入到脚本标签或危险属性中。这有助于确认为DOM型漏洞。初步测试在搜索框或URL的search参数中输入一些基本的测试载荷观察行为输入{{1}}或{{11}}。如果页面显示1或2而不是字符串本身那么强烈表明存在AngularJS表达式注入。输入。观察是否被HTML编码。DOM XSS通常不依赖这些字符但可以了解服务端的过滤情况。3.2 AngularJS Payload构造原理在确认存在表达式注入后我们需要构造一个能执行JavaScript代码的Payload。在AngularJS 1.x中表达式并不直接支持script标签或alert()这样的全局函数调用。我们需要利用AngularJS提供的特殊对象和方法。核心对象$eval与$constructor$scope.$eval(): 用于在当前作用域内执行一个AngularJS表达式字符串。constructor JavaScript中函数的constructor属性指向创建该对象的函数。对于$scope对象它的constructor指向创建它的构造函数。而这个构造函数的constructor就是JavaScript顶层的Function构造函数。利用链思路 我们的目标是执行alert(document.domain)或alert(1)。我们不能直接写{{alert(1)}}因为alert不在$scope上。但我们可以尝试访问全局对象window。然而在AngularJS沙箱特别是较新版本中直接访问window可能被禁止。一种经典的沙箱逃逸方法涉及以下步骤找到一个可以访问的对象如$scope。通过它的constructor属性找到Function构造函数。使用Function构造函数创建一个新的函数该函数体是我们的恶意代码。执行这个新函数。一个经典的Payload结构如下{{$eval.constructor(alert(1))()}}$eval是$scope上的一个方法函数。$eval.constructor就是这个函数的构造函数即Function。$eval.constructor(alert(1))相当于new Function(alert(1))创建了一个执行alert(1)的函数。最后的()立即调用这个新创建的函数。但是在PortSwigger的这个特定靶场中由于AngularJS版本或上下文限制直接使用$eval可能无法成功。我们需要使用更通用的constructor链。3.3 靶场专用Payload分析与构造根据实战经验PortSwigger这个AngularJS DOM XSS靶场常用的有效Payload是{{constructor.constructor(alert(1))()}}我们来拆解这个Payloadconstructor 在AngularJS表达式中constructor指向当前作用域对象的constructor属性。这里它指向创建当前$scope的控制器构造函数。.constructor 再次访问这个构造函数本身的constructor属性。由于控制器构造函数本身也是一个函数它的constructor就是JavaScript内置的Function构造函数。(alert(1)) 调用Function构造函数并传入一个字符串参数alert(1)。Function构造函数会把这个字符串编译成一个函数体。() 立即执行上一步创建的新函数。为什么这个能工作因为这个Payload巧妙地避开了对$scope上特定属性如$eval的依赖直接通过JavaScript原型链的通用属性constructor一步步走到了最底层的Function构造函数。只要AngularJS没有彻底禁掉对constructor属性的访问这条链就是通的。编码与绕过 有时服务器端或WAF可能会检测{{、}}或constructor等关键字。我们可以考虑使用HTML实体编码或JavaScript Unicode编码进行绕过。例如将constructor编码为#99;#111;#110;#115;#116;#114;#117;#99;#116;#111;#114;。当浏览器解析HTML时它会将这些实体解码为constructor然后AngularJS再将其作为表达式的一部分进行解析。在URL中我们也可以直接使用这些编码。最终的攻击URL可能看起来像https://0aXX00xx03f0xxxx.web-security-academy.net/?search{{#99;#111;#110;#115;#116;#114;#117;#99;#116;#111;#114;.#99;#111;#110;#115;#116;#114;#117;#99;#116;#111;#114;(alert(1))()}}注意在实际测试中务必使用靶场要求的目标如alert(document.domain)来验证漏洞。很多CTF或靶场要求弹出包含特定域名的警告框来证明利用成功。3.4 分步实操验证输入探测在搜索框输入{{7*7}}。如果页面显示49确认漏洞存在。构造Payload将搜索词替换为{{constructor.constructor(alert(document.domain))()}}。注意这里使用document.domain是为了获取当前页面的域名这是PortSwigger Labs验证漏洞利用成功的标准方式。URL编码由于()和空格等字符在URL中有特殊含义我们需要对完整的Payload进行URL编码。你可以使用浏览器的控制台encodeURIComponent({{constructor.constructor(\alert(document.domain)\)()}})。发起攻击将编码后的Payload拼接到URL的search参数后面然后回车访问。观察结果如果页面成功弹出一个警告框内容为该靶场的域名如0aXX00xx03f0xxxx.web-security-academy.net则漏洞利用成功。4. 漏洞挖掘与利用的进阶技巧4.1 如何寻找类似的AngularJS DOM XSS漏洞在实际的渗透测试或代码审计中你不能指望每个应用都有一个明显的search参数。你需要系统地寻找识别AngularJS应用查看页面源码寻找ng-app、>