前言
赛后自学了nodejs原型链污染后来尝试做这个题,难度不算太大,但是绕过姿势非常奇怪没见过,写一篇总结记录一下做法
wp
首先打开环境发现是一个登录框,题目有附件我们下载查看附件
最关键的就是controller.js和main.js两个文件,index.html是前端代码用处不大
代码审计
controller.js
const fs = require("fs");//引入js模块,为flag1和flag2提供读文件操作
const SECRET_COOKIE = process.env.SECRET_COOKIE || "this_is_testing_cookie"
//设置SECRET_COOKIE的值,如果环境变量process.env中存在SECRET.COOKIE就把这个常量赋值为环境变量中的值,如果环境变量中没有则设置为"this_is_testing_cookie"
const flag1 = fs.readFileSync("/flag1")//读flag1中的值
const flag2 = fs.readFileSync("/flag2")//读flag2中的值
//merge函数,nodejs原型链污染的关键函数,文章后面会放我对这个函数在nodejs原型链污染作用上的理解
function merge(target, source) {
for (let key in source) {
if (key == "__proto__") {
continue;//过滤了__proto__键,本题不能用__proto__来进行原型链污染,但prototype仍然可以使用
}
if (key in target && key in source) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
//设置一个LoginController函数,描述登录逻辑
function LoginController(req, res) {
try {
let user = {}//设置一个空对象user
merge(user, req.body)//req.body是一个属性,通常用于存储与请求相关的主体内容,尤其是在POST请求中。当客户端向服务器发送数据(例如,通过表单提交或JSON数据),这些数据将被解析并存储在req.body中
if (user.username !== "admin" || user.password !== Math.random().toString()) {
res.status(401).type("text/html").send("Login Failed")//判断用户名或者密码的逻辑
} else {
//登录成功的逻辑,这里是本题nodejs原型链污染的重要利用点
res.cookie("user", SECRET_COOKIE)
req.user = "admin"
res.redirect("/flag1")
}
} catch (e) {
console.log(e)
res.status(401).type("text/html").send("What the heck")
}
}
//设置一个Flag1Controller函数,描述flag1的获得逻辑
function Flag1Controller(req, res) {
try {
if (req.cookies.user === SECRET_COOKIE || req.user == "admin") {
//这个判断使用逻辑或,只要两者有一个判断为true,整个判断为true,所以只要满足req.user == "admin"即可
res.setHeader("This_Is_The_Flag1", flag1.toString().trim())
//把flag1的内容放在相应包中
res.status(200).type("text/html").send("Login success. Welcome,admin!")
} else {
res.status(401).type("text/html").send("Unauthorized")
}
} catch (__) { }
}
//设置一个Flag2Controller函数,描述flag2的获得逻辑
function Flag2Controller(req, res) {
//三元运算符,如果req.body.checkcode的值存在则保留原有的值,如果不存在则复制为1234
let checkcode = req.body.checkcode ? req.body.checkcode : 1234;
console.log(req.body)
if (checkcode.length === 16) { //判断checkcode的长度要等于16
try {
checkcode = checkcode.toLowerCase() //把checkcode的值中的大写字母变成小写字母
if (checkcode !== "aGr5AtSp55dRacer") { //很明显经过前面的操作这里无法直接满足,所以可以通过触发 toLowerCase() 的错误来绕过条件判断,可以通过传递一个非字符串类型且长度为16的值(比如数组),这样执行checkcode时会抛出错误(因为非字符串无法调用该方法),错误被catch捕获后,代码会继续执行catch后面的内容
res.status(403).json({ "msg": "Invalid Checkcode1:" + checkcode })
}
} catch (__) { }
//得到flag2
res.status(200).type("text/html").json({ "msg": "You Got Another Part Of Flag: " + flag2.toString().trim() })
} else {
res.status(403).type("text/html").json({ "msg": "Invalid Checkcode2:" + checkcode })
}
}
module.exports = {
LoginController,
Flag1Controller,
Flag2Controller
}
main.js
const express = require("express")
const fs = require("fs")
const cookieParser = require("cookie-parser");
const controller = require("./controller")
const app = express();
const PORT = Number(process.env.PORT) || 80
const HOST = '0.0.0.0'
app.use(express.urlencoded({ extended: false }))
app.use(cookieParser())
app.use(express.json())
app.use(express.static('static'))
//登陆页面
app.get("/", (res) => {
res.sendFile(__dirname, "static/index.html")
})
//传值使用post方式
app.post("/", (req, res) => {
controller.LoginController(req, res)
})
//flag1要发送get请求
app.get("/flag1", (req, res) => {
controller.Flag1Controller(req, res)
})
//flag2要发送post请求,可以在登录成功页面抓包post方式在flag2路由下传值
app.post("/flag2", (req, res) => {
controller.Flag2Controller(req, res)
})
app.listen(PORT, HOST, () => {
console.log(`Server is listening on Host ${HOST} Port ${PORT}.`)
})
解题
代码审计后本题的思路就非常明显了,首先绕过登录判断成功登录后访问flag1拿到前半段flag,然后访问flag2绕过判断拿到后半段flag
登录
首先我们随便输入用户名密码,抓包,因为源代码中假如登录成功会执行
res.cookie("user", SECRET_COOKIE)
req.user = "admin"
res.redirect("/flag1")
所以我们要利用nodejs原型链污染把req.user设置为admin
payload
{
"username":"admin",
"password":"123",
"constructor":{
"user":"admin"
}
}
把content-type改成application/json后发送请求包
flag1
执行了catch(e)中的内容,看起来登录好像已经绕过了,我们尝试手动访问flag1
成功访问,我们修改请求包为get访问flag1
成功拿到前半段flag1
flag2
接下来就是第二段flag
我做的时候竟然纳闷flag2这段代码是哪个页面的逻辑,想了半天才想到去main.js里面去看看,真服了我这脑子
flag2是post传值,这里需要绕过的关键判断是
if (checkcode.length === 16) { //判断checkcode的长度要等于16
try {
checkcode = checkcode.toLowerCase() //把checkcode的值中的大写字母变成小写字母
if (checkcode !== "aGr5AtSp55dRacer")
很明显如果我们的字符串大写字母转为小写字母后肯定不能满足这个判断的字符串,因为里面还有大写字母。代码审计中提到了这里的绕过方式,我们利用数组绕过
{
"checkcode":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
}
成功拿到第二段flag,拼起来就是完整的flag。