转载

Joomla反序列化漏洞的查漏补缺

Joomla反序列化漏洞的查漏补缺

阅读: 3

2015年12月15日国内各大安全厂商都从国外站点上关注到一条关于Joomla远程代码执行漏洞的内容,原文可以看 这里 。之后开启了一轮漏洞分析大战,比快,比准,比嘲讽。

而作为对于PHP SESSION序列化机制不怎么了解的我,就兴奋的阅读着各家的分析来学习这个漏洞的原理。虽然这些分析文章帮助我重现了这个漏洞的利用,并且貌似解释清楚了原理,但是,有一个问题我还是没有在这些文章中找出。

0x01 被忽略的角落

我先在这里把被忽略的问题写出来,让我们带着问题来看这几篇文章都迷失在哪里。被忽略的问题就是:

Joomla改变了PHP默认的SESSION处理方式了吗?

如果你跟我一样,看过各家对这个漏洞的分析文章,肯定会对下面这段话有印象:

键名 + 竖线 + 经过 serialize() 函数反序列处理的值。

在分析文章中这句话的出现有像这样的解释:

Joomla反序列化漏洞的查漏补缺

或者这样:

Joomla反序列化漏洞的查漏补缺

还有这样:

Joomla反序列化漏洞的查漏补缺

看上去都将矛头指向了Joomla自己实现SESSION机制的处理,而没有使用PHP默认的SESSION机制导致的对象注入问题。作为不懂PHP SESSION机制的我只好先去找了几篇关于这方面介绍的文章去学习。

0x02 PHP SESSION的自定义

如果想在PHP中自己定义处理SESSION过程,需要使用session_set_save_handler函数来将相关的自定义处理方法进行注册,注册后再使用session_start启动SESSION机制就可以使用自己定义的函数处理SESSION了。代码示例如下:

<a href="http://blog.nsfocus.net/tag/php/" title="php" target="_blank">php</a> function sess_open($sess_path, $sess_name) {print "Session opened.print "Sess_path: $sess_pathprint "Sess_name: $sess_namereturn true; }function sess_close() {print "Session closed.&lt;br&gt;";return true;}function sess_read($sess_id) {print "Session read.&lt;br&gt;";print "Sess_ID: $sess_id&lt;br&gt;";return '';}function sess_write($sess_id, $data) {print "Session value written.&lt;br&gt;";print "Sess_ID: $sess_id&lt;br&gt;";print "Data: $data&lt;br&gt;&lt;br&gt;";$fp = fopen('C:/wamp/www/999.txt','w');fwrite($fp, $data);fclose($fp);return true;}function sess_destroy($sess_id) {print "Session destroy called.&lt;br&gt;";return true;}function sess_gc($sess_maxlifetime) {print "Session garbage collection called.&lt;br&gt;";print "Sess_maxlifetime: $sess_maxlifetime&lt;br&gt;";return true;}session_set_save_handler("sess_open", "sess_close", "sess_read", "sess_write", "sess_destroy", "sess_gc");session_start();
phpfunction sess_open($sess_path, $sess_name) {  print "Session opened.   print "Sess_path: $sess_path   print "Sess_name: $sess_name   return true; }   function sess_close() {     print "Sessionclosed.<br>";     return true; }   function sess_read($sess_id) {     print "Sessionread.<br>";     print "Sess_ID: $sess_id<br>";     return ''; }   function sess_write($sess_id, $data) {     print "Sessionvaluewritten.<br>";     print "Sess_ID: $sess_id<br>";     print "Data: $data<br><br>";     $fp = fopen('C:/wamp/www/999.txt','w');     fwrite($fp, $data);     fclose($fp);     return true; }   function sess_destroy($sess_id) {     print "Sessiondestroycalled.<br>";     return true; }   function sess_gc($sess_maxlifetime) {     print "Sessiongarbagecollectioncalled.<br>";     print "Sess_maxlifetime: $sess_maxlifetime<br>";     return true; } session_set_save_handler   ("sess_open", "sess_close", "sess_read", "sess_write", "sess_destroy", "sess_gc"); session_start();   

在对SESSION相关变量进行赋值时会调用注册的sess_write方法进行处理,为了方便了解传入的参数内容,我print出了data变量。我们来看一下赋值的这个过程:

Joomla反序列化漏洞的查漏补缺

可以看出data变量传入的就是PHP SESSION机制序列化好的内容,也就是说即使我们自定义了处理SESSION的函数,但是如果没有对数据进行处理的话,SESSION还是那个PHP自己序列化的SESSION。了解到这里后,我们再回来看Joomla的SESSION的处理部分:

<a href="http://blog.nsfocus.net/tag/php/" title="php" target="_blank">php</a> public function register() { // Use this object as the session handlersession_set_save_handler( array($this, 'open'), array($this, 'close'), array($this, 'read'),array($this, 'write'), array($this, 'destroy'), array($this, 'gc') ); }public function write($id, $data) {// Get the database connection object and verify its connected. $db = JFactory::getDbo();$data = str_replace(chr(0) . '*' . chr(0), '/0/0/0', $data);try{$query = $db-&gt;getQuery(true)-&gt;update($db-&gt;quoteName('#__session'))-&gt;set($db-&gt;quoteName('data') . ' = ' . $db-&gt;quote($data))-&gt;set($db-&gt;quoteName('time') . ' = ' . $db-&gt;quote((int) time()))-&gt;where($db-&gt;quoteName('session_id') . ' = ' . $db-&gt;quote($id));// Try to update the session data in the database table.$db-&gt;setQuery($query);if (!$db-&gt;execute()){return false;}/* Since $db-&gt;execute did not throw an exception, so the query was successful.Either the data changed, or the data was identical.In either case we are done.*/return true;}catch (Exception $e){return false;}}
phppublic function register() { // Use this object as the session handler   session_set_save_handler( array($this, 'open'), array($this, 'close'), array($this, 'read'),  array($this, 'write'), array($this, 'destroy'), array($this, 'gc') ); }   public function write($id, $data) {  // Get the database connection object and verify its connected. $db = JFactory::getDbo();       $data = str_replace(chr(0) . '*' . chr(0), '/0/0/0', $data);       try     {         $query = $db->getQuery(true)             ->update($db->quoteName('#__session'))             ->set($db->quoteName('data') . ' = ' . $db->quote($data))             ->set($db->quoteName('time') . ' = ' . $db->quote((int) time()))             ->where($db->quoteName('session_id') . ' = ' . $db->quote($id));           // Try to update the session data in the database table.         $db->setQuery($query);           if (!$db->execute())         {             return false;         }         /* Since $db->execute did not throw an exception, so the query was successful.         Either the data changed, or the data was identical.         In either case we are done.         */         return true;     }     catch (Exception $e)     {         return false;     } }   

从上面的代码我们可以看出,Joomla注册了write函数作为写入SESSION内容时的处理函数。但是write函数除了对data变量做了一次字符替换,没有再做任何操作就存入了数据库,而这个替换是不会影响到SESSION序列化和反序列化的。

而这几篇分析文章中都提到的一句话:

键名 + 竖线 + 经过 serialize() 函数反序列处理的值。

其实就是PHP处理SESSION的默认序列化格式。

所以到这里我能得出的结论就是:

Joomla的自定义SESSION处理并不是导致对象注入的元凶,SESSION仍然是按照默认的机制进行序列化的。

到这里我们前面提出来的问题得到了答案,但是我们貌似更看不懂这个漏洞了,既然所有的SESSION都是使用PHP默认机制来完成序列化的,那么这个漏洞是怎么形成的呢?现在,我们带一个新问题来继续分析:

PHP默认的SESSION机制有问题吗?

0x03 老司机带我来跳坑

前面章节提到的几篇分析文章除了共同将矛头指向了Joomla自定义SESSION处理,还有一个共同点就是都引用ryat老司机的 《PHP Session 序列化及反序列化处理器设置使用不当带来的安全隐患》 这篇文章。

这篇文章指出,如果写入SESSION和读取SESSION使用的方式不一样,可能会造成对象注入的问题。这里引用ryat文章中的例子来说明:

<a href="http://blog.nsfocus.net/tag/php/" title="php" target="_blank">php</a> //foo1.<a href="http://blog.nsfocus.net/tag/php/" title="php" target="_blank">php</a>ini_set('session.serialize_handler', '<a href="http://blog.nsfocus.net/tag/php/" title="php" target="_blank">php</a>_serialize');session_start(); $/_SESSION['ryat'] = $/_GET['ryat'];//foo2.<a href="http://blog.nsfocus.net/tag/php/" title="php" target="_blank">php</a>ini_set('session.serialize_handler', '<a href="http://blog.nsfocus.net/tag/php/" title="php" target="_blank">php</a>');//or session.serialize_handler set to <a href="http://blog.nsfocus.net/tag/php/" title="php" target="_blank">php</a> in <a href="http://blog.nsfocus.net/tag/php/" title="php" target="_blank">php</a>.ini session_start();class ryat {var $hi;function __wakeup() {echo 'hi';}function __destruct() {echo $this-&gt;hi;}}
php //foo1.php ini_set('session.serialize_handler', 'php_serialize');          session_start(); $/_SESSION['ryat'] = $/_GET['ryat'];   //foo2.php   ini_set('session.serialize_handler', 'php');   //or session.serialize_handler set to php in php.ini session_start();   class ryat {      var $hi;     function __wakeup() {         echo 'hi';     }     function __destruct() {         echo $this->hi;     } }    

在存储SESSION使用的是php_serialize方式,而读取使用的是php方式。这两种方式对应的序列化格式是这样的:

| 处理器 | 对应的存储格式 | | —————————– | ———————————- | |php | 键名 + 竖线 + 经过 serialize() 函数反序列处理的值 | |php_serialize

(php>=5.5.4) | 经过 serialize() 函数反序列处理的数组 |

在这种存储和读取方式不同的情况,我们很容易理解对象注入的问题,通过foo1.php?ryat=|O:4:”ryat”:1:{s:2:”hi”;s:4:”ryat”;}来将竖线前面的字符作为键名,让serialize()函数反序列化竖线后面我们输入的内容。难道Joomla在SESSION也像实例代码那样修改了session.serialize_handler?但是并没有,我翻遍了Joomla的代码,也没有找到session.serialize_handler相关的任何代码,所以Joomla仍然使用统一的SESSION序列化和反序列化方式。

我卡在了这个地方好久,为了找到原因,我决定脱离Joomla代码,使用我前面给出的PHP SESSION自定义函数来复现这个反序列化漏洞,因为这样整个序列化和反序列过程很简单,并且我还可以print序列化内容。就在我第一次尝试的时候,我发现了一个问题: Joomla反序列化漏洞的查漏补缺

注入的竖线居然原封不动的print的出来了!天啊!老司机在他的文章里留了一个 扣子 。PHP的SESSION使用php方式进行序列化时,是不会对输入内容检查、过滤或者转义竖线的,那么这里我们可以得到第二个问题的答案:

PHP的SESSION机制存在潜在的对象注入隐患

PHP序列化SESSION内容的源码内容如下:

c#define PS_ENCODE_LOOP(code) do { /HashTable *_ht = Z_ARRVAL_P(PS(http_session_vars)); /int key_type; //for (zend_hash_internal_pointer_reset(_ht); /(key_type = zend_hash_get_current_key_ex(_ht, &amp;key, &amp;key_length, &amp;num_key, 0, NULL)) != HASH_KEY_NON_EXISTANT; /zend_hash_move_forward(_ht)) { /if (key_type == HASH_KEY_IS_LONG) { /<a href="http://blog.nsfocus.net/tag/php/" title="php" target="_blank">php</a>_error_docref(NULL TSRMLS_CC, E_NOTICE, "Skipping numeric key %ld", num_key); /continue; /} /key_length--; /if (<a href="http://blog.nsfocus.net/tag/php/" title="php" target="_blank">php</a>_get_session_var(key, key_length, &amp;struc TSRMLS_CC) == SUCCESS) { /smart_str_appendl(&amp;buf, key, key_length);if (memchr(key, PS_DELIMITER, key_length) || memchr(key, PS_UNDEF_MARKER, key_length)) {<a href="http://blog.nsfocus.net/tag/php/" title="php" target="_blank">PHP</a>_VAR_SERIALIZE_DESTROY(var_hash);smart_str_free(&amp;buf);return FAILURE;}smart_str_appendc(&amp;buf, PS_DELIMITER);<a href="http://blog.nsfocus.net/tag/php/" title="php" target="_blank">php</a>_var_serialize(&amp;buf, struc, &amp;var_hash TSRMLS_CC);} else {smart_str_appendc(&amp;buf, PS_UNDEF_MARKER);smart_str_appendl(&amp;buf, key, key_length);smart_str_appendc(&amp;buf, PS_DELIMITER); /} /} /} while(0)
c #define PS_ENCODE_LOOP(code) do { /       HashTable *_ht = Z_ARRVAL_P(PS(http_session_vars));        /     int key_type;                                              /                                                                 /     for (zend_hash_internal_pointer_reset(_ht);                /             (key_type = zend_hash_get_current_key_ex(_ht, &key, &key_length, #_key, 0, NULL)) != HASH_KEY_NON_EXISTANT; /                 zend_hash_move_forward(_ht)) {                  /         if (key_type == HASH_KEY_IS_LONG) {                    /             php_error_docref(NULL TSRMLS_CC, E_NOTICE, "Skipping numeric key %ld", num_key);    /             continue;                                          /         }                                                      /         key_length--;                                          /         if (php_get_session_var(key, key_length, &strucTSRMLS_CC) == SUCCESS) {    /             smart_str_appendl(&buf, key, key_length);         if (memchr(key, PS_DELIMITER, key_length) || memchr(key, PS_UNDEF_MARKER, key_length)) {             PHP_VAR_SERIALIZE_DESTROY(var_hash);             smart_str_free(&buf);             return FAILURE;         }         smart_str_appendc(&buf, PS_DELIMITER);           php_var_serialize(&buf, struc, &var_hashTSRMLS_CC);     } else {         smart_str_appendc(&buf, PS_UNDEF_MARKER);         smart_str_appendl(&buf, key, key_length);         smart_str_appendc(&buf, PS_DELIMITER);                                              /         }                                                      /     }                                                          / } while(0)   

0x04 Joomla漏洞原理

通过user-agent注入序列化对象代码到SESSION内容中,利用SESSION内容会存入数据库,通过使用utf-8的畸形字符截断部分内容,使注入对象后的序列化字符串仍保证正确结构。利用PHP默认SESSION序列化的php方式,通过传入带竖线的字符串,来使前面的序列化内容作为键值,保证发序列化过程不会在解析注入对象内容前停止,从而实现用户自定义对象解析。

对于SESSION反序列化部分的内容,感兴趣的各位可以看一下LN的 这篇文章 ,从PHP源码的层面分析了这个过程。

0x05 总结

这个漏洞最有意义的地方,就是告诉了我们在PHP下使用默认的SESSION机制会存在对象注入的风险。如果你对于SESSION的序列化内容进行了存储(文件或者数据库),那么请注意这些存储对象的一些截断特性,否则和SESSION序列化特性配合起来,威力不容小觑啊。

正文到此结束
Loading...