本周是开学周,整个工作日都在开会,学习时间并不多。周四晚上参加字节跳动的宣讲会,简单的介绍了目前字节跳动的技术栈。其中字节跳动的 http 框架使用的是 Gin,因此周六周日就捣鼓起来了,其中就遇到了一个奇怪的功能——SecureJSON。

  根据文档描述 SecureJSON 可以防止 json 劫持,如果给定的结构是数组值,则默认预置 "while(1);" 到响应体。

1
2
3
4
5
6
7
8
func securejson() {
r := gin.Default()
names := []string{"lena", "austin", "foo"}
r.GET("securejson", func(c *gin.Context) {
c.SecureJSON(http.StatusOK, names)
})
r.Run()
}

  运行示例代码,响应体被插入了"while(1);"

1
2
λ curl http://127.0.0.1:8080/securejson
while(1);["lena","austin","foo"]

  根据文档的描述将 Array 修改成 Map。

1
2
3
4
5
6
7
8
9
10
func securejson() {
r := gin.Default()
r.GET("securejson1", func(c *gin.Context) {
c.SecureJSON(http.StatusOK, gin.H{
"names": "Solyn",
})
})

r.Run()
}

  此时响应体前并未添加"while(1);"

1
2
λ curl http://127.0.0.1:8080/securejson1
{"names":"Solyn"}

  难道[]是造成的所谓 JSON 劫持的原因吗,JSON 劫持到底是什么,通过查阅资料找到了一段示例代码。
  某银行可以通过某个 Get 方法的 API 获取已登录用户的账户余额。

1
2
3
4
5
6
7
8
9
GET https://mybank.com/users/balance
[
{
"userName": "jayden",
"balance": 1200 },
{ "userName": "oscar",
"balance": 1200000 }
]
Cookie: XXXXXOOOXXXXX

  正常情况下 Cookie 和同源策略可以阻止 JS 跨域获取数据,那攻击者该如何获取到数据呢。在浏览器控制台实验发现以[]开头的 Javascript 代码可以直接执行。

1
2
eval('[1,2,3]');
Array(3) [ 1, 2, 3 ]

  也就是说如果将[]格式的数据放入<script>标签下可以直接被执行,即便如此又该如何获取到数据呢?这时候 Javascript 覆盖和重写特性就被利用了-如果有多个重名的函数与方法只有最后一个定义的有效。攻击者通过重写默认的Array()函数,并将返回[]格式的 API 放入<script>标签下,一旦运行到[就会调用覆写的Array()来窃取数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>function Array(){
var that = this;
var index = 0;
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++;}

<script>
<script src="https://mybank.com/users/balance"></script>

  主流浏览器通过删除__defineSetter__阻止覆写修复了该漏洞。但随着 ES6 Proxy 的发布,Array()又可以被覆写利用了。尽管浏览器很快修复了这些漏洞,Google、Facebook 等厂商通过在自己的 API 前添死循环阻止执行到[,也是为了避免今后出现其它利用方法。客户端则需要先将附加的安全头去除再进行处理。
  这也是为什么 Gin 的 SecureJSON 不对{}格式的 JSON 进行处理,JS 会首先检查语法,导致{}格式的 API 无法被执行。

1
2
eval('{"key":"value"}');
SyntaxError: unexpected token: ':'

  虽然只是一个小问题,但深入研究探究其中的缘由还是很有趣的。