ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Catfish CMS越权漏洞源码实战:水平垂直越权原理与修复

2026/10/1 7:11:34 拓冰建站 浏览量
Catfish CMS越权漏洞源码实战:水平垂直越权原理与修复 简介Catfish CMS鲶鱼内容管理系统源码压缩包专门呈现越权漏洞的真实代码场景适合PHP开发者、安全测试人员及CMS二次开发学习者用于漏洞复现与加固演练。包内共715个文件以346个PHP核心逻辑文件为主辅以80个HTML页面、56个JS脚本、13个CSS样式及多类静态资源完整覆盖前台展示、后台管理、模板渲染与前端交互模块另有SQL数据库脚本与配置文件便于本地搭建环境复现问题。压缩包整体8.78MB目录结构清晰可快速定位权限校验相关代码段。资源重点展示了越权漏洞的典型成因包括认证授权不完善、URL参数篡改、权限检查缺失、会话管理缺陷及后台接口暴露等并通过可运行源码帮助读者理解漏洞触发机制。目前已有940人学习下载适合希望通过代码审计与漏洞修复实践提升CMS安全能力的初中级开发者参照真实缺陷学习身份验证、参数过滤、安全会话及后台接口防护的正确实现方法。1. Catfish CMS 越权漏洞源码一份能让你把越权漏洞看穿的实战样本我拿到这份“Catfish鲶鱼CMS 带越权漏洞源码.zip”的时候第一反应是这玩意儿不能直接拿来上线。花了半小时部署起来用两个浏览器登录两个普通账号改一个 URL 里的 id 参数就能把另一账号的文章删除。这不是环境问题是代码里权限校验缺失。这份源码的价值就在于它能让你在完全可控的本地环境里把水平越权、垂直越权两种漏洞从根上看明白。适合三类人做代码审计的安全测试、刚接触 Web 安全的学员、以及维护 ThinkPHP CMS 的 PHP 开发者。后面所有操作都基于这份 zip 解压后的源码不涉及任何真实系统。2. 把漏洞跑起来环境搭建、目录结构与权限基线拿到 zip 先别急着扫漏洞先把环境跑通。越权漏洞的复现非常依赖会话Session和权限状态部署不正确后面所有操作都会跑偏。2.1 源码包里到底是什么静态资源与 ThinkPHP 逻辑的分工解压后你会看到一串前端文件test.bmp、bootstrap.min.css、jquery-ui.min.css、umeditor.css、jquery-confirm.css、bootstrap-datetimepicker.min.css、mathquill.css、image.css、video.css 还有 umeditor 相关文件。这些只是 UI 层真正决定越权漏洞的是 application 或 App 目录下的 PHP 控制器以及 route 路由文件。前端静态资源占了压缩包体积的大头但安全审计的目光要穿过它们直接落到 index.php 入口和控制器方法上。常见做法是先把目录结构列出来确认入口与框架。Catfish CMS 基于 ThinkPHP入口文件通常是根目录的 index.php应用目录是 application模块按 admin、home、api 等划分。越权漏洞往往出现在 admin 模块的控制器里但也有部分接口写在公共模块导致未授权访问。unzip catfish_cms_vuln.zip -d /var/www/html/catfish cd /var/www/html/catfish find . -maxdepth 2 -type f | head -50第一行把压缩包解压到 Web 根目录第二行进到项目目录第三行列出两层以内的文件。你会发现 application 目录、public 目录和 install 目录都在。参数说明-d 指定解压目标-maxdepth 2 只找两层避免把 vendor 里的第三方库全部打出来head -50 是为了先看骨架不用把所有文件都罗列。真正的漏洞代码集中在 application/admin/controller 下文件权限一般为 644目录为 755。太宽松的权限本身也是风险但这份资源里我更关注逻辑层。你还可以用 grep 快速搜索危险函数模式比如直接在代码里搜M(member)-where和I(get.id)这些往往是越权链路的入口。2.2 本地部署 Catfish CMSPHPStudy 与 Docker 两条路部署我推荐两条路。如果本机已经装了 PHPStudy直接把解压后的目录放到 Wnmp 的站点根目录创建数据库后访问 install 即可。注意 PHP 版本最好选 5.6 或 7.xThinkPHP 老版本对 PHP 8 的兼容性很玄学越权复现过程里因为这些兼容报错不值得。另一条路是 Docker好处是环境隔离复现完了直接删掉容器不留垃圾docker run -d --name catfish-lab \ -p 8080:80 \ -v /var/www/html/catfish:/var/www/html \ -e MYSQL_ROOT_PASSWORDroot \ php:7.4-apache这个命令把宿主机解压好的源码目录挂载到容器 Web 目录容器内的 Apache 直接提供服务。参数说明-p 8080:80 把宿主 8080 端口映射到容器 80 端口-v 是目录挂载改代码即时生效最后用 php:7.4-apache 镜像保证 ThinkPHP 跑得起来。启动后访问 http://localhost:8080/ 会进入安装引导。数据库初始化我一般用命令行做干净可控CREATE DATABASE catfish DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER catfishlocalhost IDENTIFIED BY catfish_pass; GRANT ALL PRIVILEGES ON catfish.* TO catfishlocalhost; FLUSH PRIVILEGES;第一句建库并指定 utf8mb4避免中文乱码第二句建一个专用账号第三句授权最后刷新权限。安装向导里填这个数据库名、用户和密码前缀保持默认 cat_后面复现时用到表名就是 cat_article、cat_member 之类。如果你改成了其他前缀后面所有 SQL 和代码里的表名都要跟着变容易出现 5.3 节里说的表不存在问题。2.3 账号权限分级用两个账号建立测试基线越权复现最少需要两个账号。一个普通作者一个管理员。普通账号负责操作自己的内容管理员账号负责验证高权限接口是否被别人够到。安装完成后我习惯先在后台创建一个“编辑”角色和一个“作者”角色再创建两个普通用户分配给不同角色。这里要注意角色表里有一个 role_id 字段后面垂直越权测试靠的是低角色账号去请求高角色的接口。# 用 curl 保持会话登录第一个账号 curl -c cookies_user1.txt -d usernameuser1passwordtest123login_submit1 \ http://localhost:8080/index.php?mmemberalogin # 第二个账号 curl -c cookies_user2.txt -d usernameuser2passwordtest123login_submit1 \ http://localhost:8080/index.php?mmemberalogin-c 保存 Cookie 到文件-d 提交表单。两次登录分别保存到不同文件后续请求带上各自 Cookie就能区分两个用户身份。登录接口路径根据实际安装路由调整ThinkPHP 的 classic 模式下常见是 index.php?mmemberalogin。注意这些命令不要直接复制就跑先看安装后生成的 URL 格式路由模式不同参数名会有差异。建立基线后在 user1 下创建一篇测试文章记录它的 id例如 101。user2 下也创建一篇id 102。后面越权测试就在这两个 id 之间切换。到此环境算是跑通了下一步是理解漏洞代码到底错在哪里。提示测试过程中我会把所有请求和响应保存到文本文件里方便最后对比修复前后的差分结果。越权复现的结论必须基于可回放的请求记录而不是“我刚才好像删成功了”。3. 越权漏洞的根因从权限模型到控制器里的危险写法环境跑通后进入正题。越权漏洞说起来就一句话服务端没有在每次请求里确认“当前用户是否有权做这件事”。但放到 Catfish 这种 CMS 里它的具体形态有好几层得一层层拆。3.1 水平越权和垂直越权先分清你在找什么越权分为水平越权和垂直越权两者目标不同。水平越权是“相同角色用户之间互相操作”比如作者 A 修改作者 B 的文章垂直越权是“低角色操作高角色功能”比如普通作者调用管理员删除用户的接口。在 Catfish CMS 里水平越权常见于文章管理、会员资料编辑、评论删除。这类接口通常带一个 id 参数id 是数据库自增主键攻击者把它改成别人的 id就可能操作别人的数据。垂直越权常见于后台管理接口典型的漏洞是控制器方法没有做角色权限判断或者只判断了“是否登录”而没有判断“角色等级”。拿到一份源码我一般直接看控制器基类。ThinkPHP 里后台控制器会继承一个 AdminBaseController里面应有权限校验逻辑。如果某些控制器直接继承 Controller且方法里没有 $this-checkRole()说明这里大概率存在垂直越权。Catfish 的权限模型基于 RBAC角色表、节点表、角色-节点关联表三张表维护权限但实现经常出现不能严格执行的情况。3.2 定位越权点的四个线索找越权点不能靠肉眼扫全文件我有四个固定线索。线索一是 URL 中的数字参数。凡是路由里出现 id、cid、aid、uid 这类参数的接口先标记出来。线索二是控制器方法名。delete、update、edit 这类方法往往直接按 ID 操作权限校验容易缺失。线索三是继承了错误基类的控制器。后台接口不继承公共鉴权基类等于开门迎客。线索四是 ajax 请求接口。很多 CMS 为了前端异步单独写了 ajax 目录或 method开发者容易在这里漏掉权限判断。举个例子Catfish 里文章删除的典型路由是public function delete() { $id I(get.id); // 直接删除 M(article)-where(id, $id)-delete(); }这段代码没有从 session 里取当前用户 ID也没有校验文章的作者字段是否等于当前用户。这就是一条标准的水平越权链路。I(get.id) 是 ThinkPHP 的输入获取函数等价于 $_GET[id]它只做格式过滤不做权限判断。删除操作的影响行数直接由传入的 id 决定攻击者遍历 id 就能删掉任意文章。3.3 三处典型越权写法注释、绕过与默认不校验为了把原理说透我总结了 Catfish 源码里最常见的三种越权写法。请将它们作为审计对照表而不是直接认为这份源码里百分百有这三段。第一种方法内注释掉权限检查public function editUser() { // $this-check_auth(); // 暂时注释测试完忘开 $uid I(post.uid); $data[email] I(post.email); M(member)-where(uid, $uid)-save($data); }注释掉的 check_auth 原本会校验当前会话用户是否有编辑目标用户的权限。注释后任何登录用户都能通过传入任意 uid 修改他人邮箱。这种“临时注释忘记恢复”是越权漏洞最常见的来源之一。代码审查时盯着注释行看往往有惊喜。第二种只校验登录不校验角色public function delUser() { if (!isset($_SESSION[admin_id])) { $this-error(未登录); } $uid I(post.uid); M(member)-where(uid, $uid)-delete(); }这里判断了是否登录但没有判断管理员等级。普通编辑账号登录后只要把 uid 改成其他人的 ID就能删除会员。这是垂直越权因为删会员通常需要更高权限参数说明$_SESSION[admin_id] 只表示存在后台会话不代表会话里有高角色标识。正确的做法是同时校验 $_SESSION[role_id] 的值。第三种参数绑定绕过了服务端校验public function saveArticle() { $article M(article); $data $article-create(); // 没有将 uid 强制设为当前用户 $article-save($data); }$data 来自表单如果表单里有隐藏字段 uid且用户能篡改它那么保存后文章作者就变成了别人的 uid。这种情况属于水平越权叠加数据篡改。create() 方法默认接受 POST 全字段除非使用 field 白名单限制。参数说明create() 返回的数据数组直接映射到数据库字段必须用 field(title,content,cat_id) 之类白名单收紧可更新字段。这三种写法在真实 CMS 里都很常见。审计时先看控制器方法开头有没有统一的权限调用再看操作数据时使用的主键是不是完全由客户端传入最后看查询有没有带上“当前用户”这个条件。Catfish 的后台权限一般通过 role 表和 node 表做 RBAC管理员登录后 session 里写入 admin_role_id然后每个控制器方法里调用 check_auth 去比对角色节点。这套模型在理论上没问题但实现时常见两个坑一是某些接口没接入 RBAC二是 RBAC 只在菜单显示层生效后端接口没有同步做强制校验。这就导致前端“看不到”某按钮但构造请求照样能执行。所以在审计时不要以菜单展示判断权限要以接口实际执行是否校验为准。注意第三点里说的“隐藏字段”篡改用浏览器 DevTools 或者直接构造 POST 请求就能做到不依赖漏洞扫描器。4. 完整复现一次越权从水平修改到垂直提权原理讲完动手。复现越权分三步准备环境、执行越权请求、对比响应结果。每一步都留痕方便最后写报告或做修复验证。4.1 水平越权复现改一个 id 删掉别人的草稿假设你已经用 user1 登录并且知道 user2 有一篇草稿 id102。作为普通作者user1 的权限只能删自己的文章但接口没有校验作者所以用 user1 的 Cookie 请求删除 id102看返回是否成功。curl -b cookies_user1.txt -X POST \ -d id102 \ http://localhost:8080/index.php?madminaarticlemethoddelete-b 是读取 Cookie-d 提交文章 ID-X 指定 POST。如果返回 JSON 里 status1文章 102 消失说明水平越权复现成功。关键点在于Cookie 是 user1 的操作对象却是 user2 的文章这就是“水平”跨越。如果返回 status0 也不要急先确认三件事id 字段名是不是 article_id路由 method 参数是不是必须拼接在 URL 里数据库里文章表前缀有没有写对这些属于环境配置问题不是漏洞不存在。复现成功后我会再验证一次正常操作用 user2 的 Cookie 删除 id102确认接口本身能用。这样对比更严谨同一条删除接口自己删成功别人删也成功就能排除“接口本来就允许所有人删”的误解。4.2 垂直越权复现低权限账号直接调后台接口垂直越权的复现思路是用普通编辑账号去请求管理员专属接口。比如备份数据库是管理员权限URL 是 index.php?madminabackupmethodexport普通编辑登录后直接访问这个 URL。curl -b cookies_user2.txt \ http://localhost:8080/index.php?madminabackupmethodexport如果接口仅判断登录没判断角色响应里会出现数据库备份文件下载或者返回“备份成功”。这里要观察响应头 Content-Disposition 或 Location 字段。如果被跳转到登录页或权限提示页说明接口权限校验存在。另一个更典型的垂直越权是后台会员列表curl -b cookies_user2.txt \ http://localhost:8080/index.php?madminamembermethodlistspage1普通编辑能读出所有会员信息包括手机号和密码哈希这就是严重越权。响应长度和 page 分页信息能直接证明数据泄露。运行这条命令时注意 URL 里的 符号在 bash 中需要用引号包裹避免被解释为后台任务。4.3 利用链分析为什么越权经常和未授权叠加实践中越权和未授权常常一起出现。因为同一套代码里开发者可能对公共控制器忘记加任何登录检查。未授权是“不登录也能访问”越权是“登录了但权限不够”。这份源码里的部分公共模块接口比如附件上传、搜索建议就是未授权而 admin 模块下的接口则是越权。把两者串联攻击链路会变成未授权拿到上传 token 或后台路径越权删除数据或修改管理员密码。复现到这一步你就会发现 CMS 的安全不是单个函数的问题而是整个控制器职责划分的问题。修复时不能只给某个方法加一行判断要在基类层面做统一拦截。这也是为什么很多扫描器扫不出越权因为它们无法理解业务角色只能靠登录后逐个请求接口再人工判断返回是否不应该被访问。curl -b cookies_user1.txt \ -H Referer: http://localhost:8080/admin/article \ -H X-Requested-With: XMLHttpRequest \ -X POST -d id102 \ http://localhost:8080/index.php?madminaarticlemethoddelete这里加上了 Referer 和 X-Requested-With 头模拟浏览器 ajax 请求。某些接口会校验这两个头缺了会直接拒绝。如果你用 curl 复现得到 403先加这两行再试比盲目怀疑漏洞是否存在更高效。提示复现时建议在浏览器开发者工具里复制为 curl 命令保留所有请求头。某些接口做了 Referer 校验缺了 Referer 会 403那会干扰你对越权是否成功的判断。5. 越权复现常见问题排查五个翻车现场与解决办法我在拆这份源码时也踩了不少坑下面五条按出现频率排序每一条都是现象、原因、解决的结构。5.1 伪静态没开启所有路由 404现象访问 index.php?madminalogin 正常但访问美化后的 URL 比如 /admin/login 直接 404。原因ThinkPHP 默认使用 PATHINFO 模式要求 Apache/Nginx 配置 rewrite 规则。这份源码的 .htaccess 在 Apache 下有效但 Nginx 下必须手动配置。解决location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s$1 last; } }放到 server 块里。注意 s 参数名要和源码路由配置一致。配置后重启 Nginx伪静态 URL 就能访问。如果后端接口路径写的是 index.php?mxxxaxxx不受这个影响后面复现尽量用原始 URL 格式少一层烦恼。5.2 登录态 Cookie 没带全越权变未授权现象登录成功后单独用 PHPSESSID 请求接口返回 401但带上全部 Cookie 却能成功。原因ThinkPHP 的会话依赖 PHPSESSID 和 ThinkPHP 自身的 cookie 标识某些登录接口还会写入记忆登录的 cookie。如果你用 curl 只读取了 PHPSESSID丢了 auth 类的 cookie服务端就认为没登录。解决用 curl 的 -c 完整保存再 -b 完整读取。或者先访问一次首页拿到会话 Cookie再登录避免多次 set-cookie 覆盖。curl -c all_cookies.txt http://localhost:8080/index.php sleep 1 curl -b all_cookies.txt -c all_cookies.txt -d usernameuser1passwordtest123login_submit1 \ http://localhost:8080/index.php?mmemberalogin先 GET 首页初始化 Session再 POST 登录并追加 Cookie最后所有请求都 -b all_cookies.txt。顺序很重要sleep 1 是为了等 Set-Cookie 写入文件生效。这样处理后后续越权请求不会因为缺 Cookie 而变成未授权。5.3 数据库连接配置错误导致表名带前缀找不到现象安装完成后进入后台提示数据库表不存在比如 cat_article。原因config/database.php 里面的 prefix 参数和实际安装时填的前缀不一致。比如安装时填了 cat_但配置文件里写的是 think_查询就指向了不存在的 think_article。解决// application/database.php 或 config.php db_prefix cat_, prefix cat_,两个参数名都检查一遍ThinkPHP 版本不同键名可能是 db_prefix 也可能是 prefix。修改后清空 Runtime 缓存再访问后台。清缓存操作rm -rf application/Runtime/*.php如果目录路径不同先 find 到 Runtime 所在位置再删除。不清缓存的话旧配置可能还驻留在内存里。5.4 调试模式关闭错误信息被吞现象请求越权接口返回空白页或 500但没有具体报错无法定位是 SQL 错误还是权限判断中断。原因ThinkPHP 默认关闭调试模式错误不显示到页面。解决把入口文件里的 APP_DEBUG 临时改为 true// index.php define(APP_DEBUG, true);改完刷新接口页面会显示具体 SQL 和调用栈。复现结束后务必改回 false否则本来是想审计漏洞结果自己的环境也暴露了调试信息。调查错误时重点看 SQL 语句里的表名和 where 条件能直接看出查询是否带上了当前用户 ID。5.5 WAF 或安全组件拦截了带危险方法的请求现象curl 里直接 POST 删除接口返回 403 或 “请求被拦截”但浏览器里操作正常。原因本地环境装了安全软件或者 PHP 扩展对 DELETE 方法做了限制。有些环境变量把 curl 的 User-Agent 识别成扫描器。解决在 curl 命令里伪造一个正常浏览器的 User-Agent并加上 Referercurl -b cookies_user1.txt \ -H User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) \ -H Referer: http://localhost:8080/admin/article \ -d id102 \ http://localhost:8080/index.php?madminaarticlemethoddelete如果还拦截检查 PHP 的 disable_functions 里有没有 shell_exec、proc_open 等以及是否装了 open_basedir 限制了读写范围。这些常见但容易被忽略。另一个思路是先在浏览器里用开发者工具手动操作一次把请求复制成 curl再替换 Cookie这样请求头完整且自然极少被拦。6. 修复后的验证用差分对比确认越权漏洞已堵上找到漏洞只是第一步能修掉并验证才完整。修复的思路是“最小权限校验”不改业务逻辑只加权限判断。6.1 修复前后代码对比水平越权修复的关键是在查询条件中加入当前用户 IDpublic function delete() { $uid $_SESSION[user_id]; $id I(get.id); M(article)-where(id, $id)-where(author_uid, $uid)-delete(); }这样即使传入别人的 idwhere 条件里的 author_uid 也会让删除影响行数为 0返回失败。垂直越权修复则是在方法开头强制校验角色if ($_SESSION[role_id] ! 1) { $this-error(权限不足); }更规范的是在后台基类的构造函数里统一调用权限节点检查而不是每个方法各写各的。修复后不要只测刚才那条路径把同角色下其他类似接口也过一遍防止同类问题批量存在。6.2 用自动化脚本验证修复有效性验证我用一段 Python 脚本两个账号分别发起越权请求对比状态import requests s1 requests.Session() s2 requests.Session() url_login http://localhost:8080/index.php?mmemberalogin url_delete http://localhost:8080/index.php?madminaarticlemethoddelete s1.post(url_login, data{username: user1, password: test123}) s2.post(url_login, data{username: user2, password: test123}) # user1 尝试删 user2 的文章 r1 s1.post(url_delete, data{id: 102}) # 修复后续 r1 应包含“无权限”或影响行数 0 print(user1 delete user2 article:, r1.json()) # user2 调用管理员备份接口 r2 s2.get(http://localhost:8080/index.php?madminabackupmethodexport) print(user2 access backup:, r2.status_code, len(r2.content))脚本用两个独立 Session 模拟两个用户分别调用越权接口。修复前 r1 状态成功r2 返回备份数据修复后 r1 报权限错误r2 返回 302 跳转。对比这两个输出就能确认漏洞是否堵上。这段脚本在本地可复现也适合放到安全测试的回归用例里。从那以后我每次修完 CMS 越权漏洞都会强制跑一遍这套双账号差分验证防止下次改动把漏洞带回来。希望这份拆解和脚本能帮到正在跟 Catfish CMS 较劲的你。本文还有配套的精品资源点击获取