原型链污染

继承与原型链 - JavaScript | MDN (mozilla.org)

JavaScript 原型链污染 | Drunkbaby’s Blog (drun1baby.top)

简单来说就是:

  • prototypenewClass 类的一个属性
  • newClass 类实例化的对象 newObj 不能访问 prototype,但可以通过.__proto__ 来访问 newClass 类的 prototype
  • newClass 实例化的对象 newObj.__proto__ 指向 newClass 类的 prototype

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

找能够控制数组(对象)的“键名”的操作即可:

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

test:

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)

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

这里__proto__已经代表o2的原型,并没有被当作key,故无法污染链子

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

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

JSON 解析的情况下,__proto__ 会被认为是一个真正的“键名”,而不代表“原型”,此时可以造成污染。

CatCTF 2022 wife

app.post('/register', (req, res) => {
    let user = JSON.parse(req.body)
    if (!user.username || !user.password) {
        return res.json({ msg: 'empty username or password', err: true })
    }
    if (users.filter(u => u.username == user.username).length) {
        return res.json({ msg: 'username already exists', err: true })
    }
    if (user.isAdmin && user.inviteCode != INVITE_CODE) {
        user.isAdmin = false
        return res.json({ msg: 'invalid invite code', err: true })
    }
    let newUser = Object.assign({}, baseUser, user) //就是这里,原型链污染
    users.push(newUser)
    res.json({ msg: 'user created successfully', err: false })
})

Object.assign() - JavaScript | MDN (mozilla.org)

Object.assign() 静态方法将一个或者多个源对象中所有可枚举自有属性复制到目标对象,并返回修改后的目标对象。

正常情况下是无法污染的:

baseUser = {
    a:1
}
user = {
    a:2,
    b:1,
    __proto__:{
        c:3
    }
}
 
// 这个函数的作用:浅复制一个对象,第一个参数位是对象的内容,后面的参数位是多个对象内容叠加进去,进行复制出一个全新的对象
let newUser = Object.assign({}, baseUser, user)  
// 无污染,结果正常
console.log(newUser)  // {a: 2, b: 1}  
// 无污染,结果正常
console.log(newUser.__proto__)  // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}

但是好在后端服务器是JavaScript,我们通过post发送过去的 json是字符串,JavaScript需要通过JSON.parse()函数才能把 json字符串转成对象,如之前所说,json解析时__proto__能被当成真正的key

过滤

如果遇到过滤的话:

字符串 stringObject 的 replace() 方法执行的是查找并替换的操作。它将在 stringObject 中查找与 regexp 相匹配的子字符串,然后用 replacement 来替换这些子串。如果 regexp 具有全局标志 g,那么 replace() 方法将替换所有匹配的子串。否则,它只替换第一个匹配子串。

replacement 可以是字符串,也可以是函数。如果它是字符串,那么每个匹配都将由字符串替换。但是 replacement 中的 $ 字符具有特定的含义。如下表所示,它说明从模式匹配得到的字符串将用于替换。

img

'123'.replace("2",'$`');
"113"
'123'.replace("2","$'");
"133"

这里先把匹配到的字符串替换为$`,然后由于$特殊含义,此时 $`又被替换为原位置的左侧文本,另一个同理