Tanktrouble WP
题目环境:docker.zip
启动环境:
cd src
npm install
node server.js
当初自己做完,看了wp还有很多疑惑,今天理一理
题目分析
是一个js写的网页小游戏:
提示要玩到555分
js与服务器端通过socketio技术通信,涉及到两个io对象的方法:
接收端:on()监听事件
发送端:emit()发送事件
接收端on监听时,接收到发送端emit发送的事件后,可以对事件的数据进行操作
审计前端代码,重点看GameState.js(因为里面有处理各种collide(碰撞)的方法)
在
方法中,首先创建了socketio对象,一个玩家对应一个socketio对象create()
然后让此对象向服务端发送事件"start",告诉服务器自己的name
对应地,服务器端(server.js)中监听"start"事件,在玩家组ALL_PLAYER中添加对应的玩家地址和name,并返回"serverState"和"newPlayer"事件,返回addr和ALL_PLAYERS来同步地址和其他玩家的信息
然后在前端启动对这些事件名的监听
比如前端监听的update事件,在后端的逻辑是:
可以做到时刻周期性地更新所有玩家的信息(位置、血量、分数)到前端,做到各个前端的同步。
前端接收到update事件,使用事件的数据进行对页面的修改等操作
我的做法
大致理解运作原理后,解题关键看前端的这个update函数:
update() {
this.socket.emit('position', {
x: this.tank.x,
y: this.tank.y,
r: this.tank.rotation,
id: this.id,
health: this.tank.health,
});
////////////////////////////////////// handle all collisions ///////////////////////////////////////////
// tank-with walls
this.game.physics.arcade.collide(this.tank, this.layer);
// otherTanks with walls
this.game.physics.arcade.collide(this.otherGroup, this.layer);
// tank bullets with walls
this.game.physics.arcade.collide(this.tank.weapon.bullets, this.layer);
// otherTanks bullets with walls
this.otherGroup.forEach(function (t) {
this.game.physics.arcade.collide(t.weapon.bullets, this.layer);
}, this);
// tank with it's own bullets
this.game.physics.arcade.collide(this.tank, this.tank.weapon.bullets, (tank, bullet) => {
bullet.kill();
tank.health -= 1;
// console.log("I shot myself.");
if (tank.health <= 0) {
updateContent(this.notice.children[3], "you have been slain by yourself");
// this.notice.children[3].textContent = "you have been slain by yourself";
// console.log("you have been slain by yourself");
}
});
// otherTanks with their own bullets
this.otherGroup.forEach(function (t) {
this.game.physics.arcade.collide(t, t.weapon.bullets, (tank, bullet) => {
bullet.kill();
});
}, this);
// tank bullets with otherTanks
this.otherGroup.forEach(function (t) {
this.game.physics.arcade.collide(t, this.tank.weapon.bullets, (tank, bullet) => {
bullet.kill();
});
}, this);
// otherTanks bullets with me
this.otherGroup.forEach(function (t) {
this.game.physics.arcade.collide(this.tank, t.weapon.bullets, (tank, bullet) => {
bullet.kill();
tank.health -= 1;
if (tank.health <= 0) {
tank.socket.emit("kill", {
killer: t.id
});
updateContent(this.notice.children[3], `you have been slain by ${t.name}`);
}
});
}, this);
}
前端没有加分的处理,说明算分机制一定是后端写的
在update函数中只有两处emit与后端产生了通信,一处是position向后端发送自己的位置,一处是kill发送玩家被击杀的信息。
很明显后端在接收到killer的id后会在ALL_PLAYERS中为此id加分
那么可以反复向后端发送kill事件,killer为自己的id,就能为自己反复加分。
id如何获得呢,position等函数都有玩家的id,用burp抓包可以获得id
复现时在后端可以看到,添加玩家时:
其实id就是玩家的地址,并且作为ALL_PLAYERS的index
用python写脚本向服务器后端发websocket包kill,指定killer为自己的ip
import socketio
import time
# 创建一个Socket.IO客户端
sio = socketio.Client()
@sio.event
def connect():
print("连接已建立")
# 无限循环,持续发送消息
sio.emit('start', {"name": "mak111"})
while True:
sio.emit('kill', {"killer": "::ffff:[玩家ip]"})
print("已发送消息")
time.sleep(0.1)
#断开连接
@sio.event
def disconnect():
print("连接已断开")
#连接
sio.connect('http://[服务器ip]:3000')
#保持脚本运行
sio.wait()
运行脚本时,可以看到后端调试输出的玩家信息,score已经开始增长了
那为什么前端页面展示的分数没有随之增长呢?并且播报mak111击杀了mak111。
先看后端处理kill事件的逻辑
socket.on('kill', function (data) {
console.log(`收到kill事件,${data}`);
if(data) {
try {
let id = data.killer;
if(id) {
ALL_PLAYERS[id].score += 1;
console.log(ALL_PLAYERS[id]);
}
socket.broadcast.emit('killBroadcast', {
killer: id? ALL_PLAYERS[id].name : ALL_PLAYERS[addr].name,
killed: id? ALL_PLAYERS[addr].name : "自己"
});
} catch(e) {
}
}
});
这会为ALL_PLAYERS["我的ip"]的score属性+=1,同时返回播报信息,killer为ALL_PLAYERS[id].name,即传来的id在ALL_PLAYERS中对应的name属性,killed为ALL_PLAYERS[addr],即服务器取当前socket对象的ip地址作为ALL_PLAYERS的索引查到的玩家name。
那么很明显我这里给的id和服务器取到的addr是同一个,就会播报mak111击杀了mak111。
然后,后端的update逻辑是:
var pack = {};
try{
for (var i in ALL_PLAYERS) {
var player;
player = ALL_PLAYERS[i];
pack[i] = {
x: player.x,
y: player.y,
r: player.r,
health: player.health,
score: player.score
};
}
for (var i in ALL_SOCKETS) {
var socket = ALL_SOCKETS[i];
socket.emit("update", pack);
}
} catch(e){}
前端接受update事件的逻辑是:
onUpdate(data) {
// console.log("Everyone else's info:");
this.notice.children[0].textContent = `当前房间玩家数量:${Object.keys(data).length}`;
for (var i in data) {
var x, y, r, id, health;
if (i != this.id) {
x = data[i].x;
y = data[i].y;
r = data[i].r;
id = data[i].id;
health = data[i].health;
if (this.otherPlayers[i] == null) {
console.log("Player is null.");
}
else {
this.otherPlayers[i].updateInfo(x, y, r, health);
}
}else {
this.notice.children[1].textContent = `当前得分:${data[i].score}`;
}
}
}
服务器端返回所有玩家的数据data,前端判断data的id是否和自己的id相等,如果相等就放到网页中,即自己的分数。
但是后端在处理新建socket连接时是这样做的:
io.on('connect', function (socket) {
let addr = socket.handshake.address;
if(ADDRESS.includes(addr)) {
ADDRESS.splice(ADDRESS.indexOf(addr),1);
delete ALL_SOCKETS[addr];
delete ALL_PLAYERS[addr];
}
console.log(addr);
ALL_SOCKETS[addr] = socket;
ADDRESS.push(addr);
即如果发现新socketio对象的地址在地址列表ADDRESS中已存在,那么就会覆盖数据,并删除ALL_SOCKETS、ALL_PLAYERS中的原玩家数据和sockeio对象,然后添加新的socketio对象来覆盖。
也就是说当我们运行py脚本时,原本服务器存储的socketio对象就被删除覆盖了,但是我们前端使用的仍然是原本的socketio对象,这样服务器发送的update事件就不会发送给我们前端的socketio对象,而是发送给我们在py脚本中新建的socketio对象。这样网页前端不会收到update事件,自然也不会更新。
这也是为什么运行脚本后,网页显示的玩家数量仍为1,这是因为玩家数量也是从ALL_PLAYER的长度取得的。
但是,由于后端getflag的逻辑:
socket.on("getFlag", function (data) {
try {
//console.log(ALL_PLAYERS);
if(ALL_PLAYERS[addr].score >= 555) {
ALL_PLAYERS[addr].score -= 555;
socket.emit("getFlag", FLAG);
} else {
socket.emit("getFlag", {flag: "玩够555分就能拿到flag"});
}
} catch(e) {
}
});
是直接取客户端ip作为索引,去找玩家的分数,而我们前端的玩家虽然已经被删除,但是getflag时后端是直接拿我们的ip去找玩家数据的,因此还是可以找到我们用脚本创建的玩家对象的数据,即刷出来的满足获取flag条件的玩家的数据。
后端判断满足条件,返回了flag。
(当时是没有意识到这点,但是跑脚本的时候正好和网页创建用户的时候网络环境不一样,那么靶机后端拿到的ip不一样,没有覆盖,于是看到前端页面的分数就一直在涨,自然而然getflag了)
当然也可以先跑脚本再创建用户,这样就是前端创建的玩家覆盖了脚本的socketio对象,但是脚本还在向后端发送kill事件,从而服务器数据更改,前端同步通过socketio获得了后端返回的数据,判断id即ip地址相同,并插入到了页面。
官方exp
官方exp算是比较正规的了,在js控制台直接创建两个io对象,一个作为击杀者,一个作为被击杀,最后随便用一个来getflag即可。由于都是在前端运行的socketio,这里的对象s1,s2在后端用的就是同一个socketio对象,
var s1 = io();
var s2 = io();
var killerId;
s1.emit("start",{"name":"tank_killed"});
s2.emit("start",{"name":"tank_killer"});
s2.on("serverState",data => killerId = data.id);
setTimeout(() => {
console.log(`获取到的killerId:${killerId}`)
for(let i=0;i<600;i++){
s1.emit("kill",{"killer":killerId});
}
}, 1000);
s2.on("getFlag",data => alert(data.flag));
setTimeout(() =>{
s2.emit("getFlag");
},2000)
控制台运行这个,获得flag。
当然,在明确了后端的逻辑以后,只需要一个socketio对象就可以完成加分并getflag,如下是简化后的exp.js:
var s1 = io();
s1.emit("start",{"name":"tank_killed"});
setTimeout(() => {
for(let i=0;i<600;i++){
s1.emit("kill",{"killer":"[ip]"});
}
}, 1000);
s1.on("getFlag",data => alert(data.flag));
setTimeout(() =>{
s1.emit("getFlag");
},2000)
Comments NOTHING