WebSocket 很容易被跨站劫持攻击,因为跨域资源共享(CORS)不适应于 WebSocket。

来源

正浏览着网页,一个显眼的弹窗广告从窗口右下角抖动而出:GET BITCOIN EVERYDAY FOR FREE!

看到 BITCOIN 的字样便引起了我的好奇心,所以就点开看看了。

还蛮有科技感的,一番浏览,注册登录后,发现这是一个玩游戏的网站(黑人问号?)

玩法

仔细体验后,大概了解了这个网站的体验流程:

  • 用户注册后,通过玩游戏赚取算力
  • 赚取的算力可以用来挖掘比特币
  • 每 5 分钟系统将送出 2000 SAT(1 SAT = 10e-8 BitCoin)
  • 用户具体获得 SAT 的数量 = (用户算力/全网算力)* 2000

虽然一看就知道所谓的算力只不过是网站提供的一个数字,并没有真正在挖矿(废话!),但不得不说,这种模式套上数字货币后真的显得很高大上。

也就是说,要尽可能分多点比特币就要提高自己的算力,那么玩游戏就是获得算力的最好方式。

Canvas 游戏

这里的游戏嵌在页面内的 Canvas 游戏,于是便想到:一切的游戏数据结果都是在本地产生的。

基于这点,我便开始分析游戏数据是怎么被提交到服务器的。

分析

首先打开 NetWork,然后进行开始游戏——提交成绩这一过程,发现并没有 XHR 请求产生,这不科学!

然后在点开 All,对所有项目逐个分析,终于,发现了一个可疑的 WebSocket 连接:

点开 Message,发现了以下内容:

破案了,原来数据(包括游戏数据)都是通过 WebSocket 与服务器进行交互的。

让我们再开始一局游戏,发现会发送下列数据:

同样,在结束游戏时,也会发送相应数据。

数据分析

获取数据如下:

1
2
3
4
5
// 发送开始游戏请求
{
"cmd": "game_start_request",
"cmdval": "{\"game_number\":1}"
}
1
2
3
4
5
// 服务器接收到请求,返回游戏游戏编号
{
"cmd": "game_start_response",
"cmdval": "{\"user_game_id\":\"5c9060c0ca01ad0001d71c00\",\"game_number\":1,\"level\":{\"level\":1,\"progress\":1,\"size\":3},\"cool_down\":0}"
}
1
2
3
4
5
6
// 游戏结束时,客户端向服务器发送相应游戏数据
{
"cmd": "game_end_request",
"cmdval": "{\"power\":600,\"id\":1,\"time\":1550982156266,\"user_game_id\":\"5c906356ca01ad0001d71d66\",\"win_status\":3}"
}
// 其中 power 为得分,id 为游戏的序号,time 为提交时间,user_game_id 为用户 id,win_status 为 3 代表完成游戏

至此,数据整个提交流程分析完啦。

那么,既然是通过 WebSocket 提交数据,是不是也意味这我也可以人工提交数据咧?

发起“攻击”

抱着试一试的心态,打开了WebSocket 在线测试工具 ,填入上面的 wss 地址:

点击连接:

成功建立起连接。

然后尝试发送游戏开始命令:{"cmd":"game_start_request","cmdval":"{\"game_number\":1}"}

成功得到服务器的响应!

那么下一步,就是构造一个可以提交的game_end_request了!

根据上面分析得到的game_end_request格式,可以构造出以下数据:{"cmd":"game_end_request","cmdval":"{\"power\":600,\"id\":1,\"time\":1550982556266,\"user_game_id\":\"5c90c1d5ca01ad0001d749a5\",\"win_status\":3}"}

然后再次发送以上构造出来的数据,惊喜的发现,提交成功了!

可以看到账号里的算力增加了:

WOW!那是不是意味着….

定时器请求脚本

于是写出以下脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 建立 WebSocket 连接并添加消息监听
const client = new WebSocket('wss://ws.rollercoin.com/cmd');

client.addEventListener('message', data => {
let responseData = JSON.parse(data.data);
let newGameData;
if (responseData.cmd == 'game_start_response') {
newGameData = JSON.parse(responseData.cmdval);
if (responseData.cmd == 'game_start_response' && newGameData.user_game_id) {
console.log(newGameData.user_game_id);
self.successData(newGameData.user_game_id, newGameData.game_number);
}
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 通过以下函数构造游戏数据
function sendData(gameId) {
let cmdval = {
game_number: gameId
};
let sendingData = {
cmd: 'game_start_request',
cmdval: JSON.stringify(cmdval)
};
client.send(JSON.stringify(sendingData));
}

function successData(gameId, gameNumber) {
let self = this;
let oldGameId;
let gamePower;
self.gamePower[gameNumber - 1];
let cmdval = {
power: gamePower,
id: gameNumber,
time: new Date().getTime(),
user_game_id: gameId,
win_status: 3
};
let sendingData = {
cmd: 'game_end_request',
cmdval: JSON.stringify(cmdval)
};
if (gameId != oldGameId) {
client.send(JSON.stringify(sendingData));
}
oldGameId = gameId;
}
1
2
3
4
// 最后,添加定时器,定时发送
let intervalTimer = setInterval(() => {
this.allsend();
}, 35000);

然后就去吃饭了…

回来后,再看看自己的账户信息发现: 0.00378368 ฿ 按照今天的价格 大概价值 15\$ 了呢!

然后算力排名也从一开始的:

飞升至 Top 1 !

当然,也不指望最终能从这些网站提款。。。

总结

很明显,之所以能成功提交到数据,是因为服务器没有对 WSS 连接请求头中的 Origin 字段进行验证。

(可以看到上图的Origin字段不同域(rollercoin.com)却也能成功建立连接)

WebSocket 跨站劫持攻击

我是在最近浏览的技术文章里,看到一个关键词:WebSocket 跨站劫持攻击,心里想怎么感觉跟上次的那个 WebSocket 攻击那么像。

结果 Google 后发现,还真的就是同一类型的攻击。

关于WebSocket 跨站劫持攻击

其实在这次攻击之前,我都不知道这类攻击还有个专有名词:WebSocket 跨站劫持攻击。

也就是说我竟然是在“无意间”完成的一次 WebSocket 跨站劫持攻击,还挺骄傲的哈哈哈哈哈。

关于 WebSocket

WebSocket 是一种新协议,也是近年来比较热门的一种双向通信(实时推送)的解决方案,随着 HTML5 草案不断完善,越来越多现代浏览器开始支持这个特性,这也意味着以后会有越来越多的 WebApp 会使用到这项技术了。

因为它具有实时性强,支持双向通讯,减少通信成本等特点,很快便代替了以前使用的轮询,长轮询,iframe 流等实时推送技术。

重要的一点是,跨域资源共享(CORS)并不适应于 WebSocket ,这意味着Access-Control-Allow-Origin在 WSS 连接中是不生效的。

所以便有了我这个“攻击”成功的故事。

最后

最后,大家也不要去尝试以上的攻击啦,因为。。。。。。我好人做到底,已经通过他们的 support 邮箱跟他们联系报告了这个缺陷啦,估计很快就会被修复了!

也算是做了一件好事,权当学习吧!

加油,共勉!