Cotonti CMS PHP 对象注入漏洞CVE-2026-71294
Cotonti 是基于 PHP+MySQL、BSD 协议开源轻量 CMS/CMF 内容管理框架,主打轻量化、模块化,体积小巧、服务器资源占用极低
一、基本情况
Cotonti CMS自带论坛社区能力兼顾建站与二次开发,适合论坛社群、游戏公会站点、小型社区门户、个人博客、企业简易官网等站点。
Cotonti CMS 不仅代码简洁易懂,适合新手学习 CMS 开发,且原生社区能力强劲,搭建论坛站点效率极高;模板改造灵活,自由度高。

栋科技漏洞库关注到 Cotonti CMS 不受信任数据反序列化(PHP 对象注入漏洞),现已追踪为CVE-2026-71294,CVSS 3.X评分 7.6。
二、漏洞分析
CVE-2026-71294漏洞是 Cotonti CMS 相关版本中存在的,注释插件 PHP 对象通过创建/编辑操作中的无限制 unserialize() 注入 漏洞。
漏洞位于Cotonti 评论插件,发表评论、编辑评论两处接口直接接收用户可控参数,解码后直接执行 unserialize(),未限制可实例化类。
普通注册会员即可发起攻击,构造恶意序列化对象,借助 PHP 魔术方法执行数据库写入、构造链路实现任意代码执行获取服务器权限。
具体来说,该漏洞源于 Cotonti CMS 的 Comments 插件反序列化用户提供的数据,而不限制可能实例化的类。
在plugins/comments/controllers/actions/CreateAction.php中,
通过cot_import(“ci”、“P”、“TXT”)(仅限修剪的净化)获得的“ci”POST参数被传递给“unserialize(base64_decode($ci))”,
没有allowed_classes限制,任何具有注释写权限成员都可访问(plugins/comments/setup.php中的默认Auth_members=>RW设置)。
在plugins/comments/controllers/actions/EditAction.php中,
`cb`参数在prepareComeBack()中通过`unserialize(base64_decode($this->revolution))`进行反序列化,
任何编辑自己注释的成员都可以访问。因为unserialize()是在没有allowed_classes的情况下调用的,
所以攻击者可以构造Cotonti(PHP对象注入原语)加载的任何类的序列化PHP对象。
使用Cotonti自己的MySQL_cache类在实践中证明了这一点:
一个精心制作的序列化MySQL_cach对象,一旦反序列化并稍后进行垃圾收集,就会触发其__destruct()->flush()链,
导致攻击者使用攻击者选择的行值控制INSERT INTO cot_cache,这证实了真正的POP链利用,
进一步的影响(包括潜在的RCE)取决于给定Cotonti安装的加载类中可用的其他小工具链。
DeleteAction.php中的第三个接收器包含相同的unserialize()模式,但在仅限管理员的授权检查后被门控,普通成员无法访问。
plugins/comments/controllers/actions/CreateAction.php
<?php
/**
* Comments system for Cotonti
* Add new comment action
* @package Comments
* @copyright (c) Cotonti Team
* @license https://github.com/Cotonti/Cotonti/blob/master/License.txt
*/
declare(strict_types=1);
namespace cot\plugins\comments\controllers\actions;
use Cot;
use cot\controllers\BaseAction;
use cot\exceptions\ForbiddenHttpException;
use cot\exceptions\NotFoundHttpException;
use cot\plugins\comments\controllers\IndexController;
use cot\plugins\comments\inc\CommentsControlService;
use cot\plugins\comments\inc\CommentsDictionary;
use cot\plugins\comments\inc\CommentsNotificationService;
use cot\plugins\comments\inc\CommentsService;
use cot\users\UsersHelper;
defined('COT_CODE') or die('Wrong URL');
/**
* @property-read IndexController $controller
*/
class CreateAction extends BaseAction
{
public function run(): string
{
global $cot_captcha;
[$auth['read'], $auth['write'], $auth['admin']] = cot_auth('plug', 'comments');
if (!$auth['write']) {
throw new ForbiddenHttpException();
}
cot_shield_protect();
$source = cot_import('source', 'P', 'ALP');
$sourceId = cot_import('source-id', 'P', 'TXT');
$extensionCode = cot_import('extension', 'P', 'ALP');
$categoryCode = cot_import('category', 'P', 'TXT');
$ci = cot_import('ci', 'P', 'TXT');
// Commented Item url params
$ciExtensionCode = $ciUrlParams = null;
if (!empty($ci)) {
$ci = unserialize(base64_decode($ci));
$ciExtensionCode = $ci[0];
$ciUrlParams = $ci[1];
if (empty($extensionCode)) {
$extensionCode = $ciExtensionCode;
}
}
if (empty($source) || empty($sourceId)) {
throw new NotFoundHttpException();
}
// Check if comments are enabled for specific category/item
if (!empty($extensionCode)) {
cot_block(
CommentsService::getInstance()->isEnabled($extensionCode, $categoryCode)
);
}
$comment = [
'com_area' => $source,
'com_code' => $sourceId,
'com_author' => Cot::$usr['id'] === 0
? cot_import('comment_author', 'P', 'TXT')
: UsersHelper::getInstance()->getFullName(Cot::$usr['profile']),
'com_authorid' => Cot::$usr['id'],
'com_text' => cot_import('comment_text', 'P', 'HTM'),
];
// Extra fields
if (!empty(Cot::$extrafields[Cot::$db->com])) {
foreach (Cot::$extrafields[Cot::$db->com] as $extraField) {
$comment['com_' . $extraField['field_name']] = cot_import_extrafields(
'comment_' . $extraField['field_name'],
$extraField,
'P',
'',
'comments_'
);
}
}
/* == Hook == */
foreach (cot_getextplugins('comments.add.first') as $pl) {
include $pl;
}
/* ===== */
if (isset(Cot::$cfg['legacyMode']) && Cot::$cfg['legacyMode']) {
// @deprecated in 0.9.26
/* == Hook == */
foreach (cot_getextplugins('comments.send.first') as $pl) {
include $pl;
}
/* ===== */
}
$service = CommentsService::getInstance();
$service->validateWithMessages($comment);
if (empty($comment['com_author']) && Cot::$usr['id'] === 0) {
cot_error(Cot::$L['comments_authorTooShort'], 'comment_author');
}
if (Cot::$usr['id'] === 0 && !empty($cot_captcha)) {
$rverify = cot_import('rverify', 'P', 'TXT');
if (!cot_captcha_validate($rverify)) {
cot_error(Cot::$L['captcha_verification_failed'], 'rverify');
}
}
/* == Hook == */
foreach (cot_getextplugins('comments.add.validate') as $pl) {
include $pl;
}
/* ===== */
$errors = $this->controller->getErrors();
if ($errors !== []) {
return $this->controller->errorResult($errors);
}
$id = CommentsControlService::getInstance()->save(null, $comment, $ciExtensionCode, $ciUrlParams);
$comment['com_id'] = $id;
$_SESSION['cot_comments_edit'][$id] = Cot::$sys['now'];
/* == Hook == */
foreach (cot_getextplugins('comments.add.done') as $pl) {
include $pl;
}
/* ===== */
if (Cot::$cfg['plugin']['comments']['mail']) {
CommentsNotificationService::getInstance()->notifyAdmins(
$comment,
CommentsDictionary::EVENT_CREATE,
$ciExtensionCode,
$ciUrlParams
);
}
cot_message(Cot::$L['comments_added']);
cot_shield_update(20, 'New comment');
return $this->controller->successResult(Cot::$L['comments_added']);
}
}
三、影响范围
未知
四、修复建议
未知
五、参考链接
管理员已设置登录后刷新可查看