2015年12月15日国内各大安全厂商都从国外站点上关注到一条关于Joomla远程代码执行漏洞的内容,原文可以看这里。之后开启了一轮漏洞分析大战,比快,比准,比嘲讽。
而作为对于PHP SESSION序列化机制不怎么了解的我,就兴奋的阅读着各家的分析来学习这个漏洞的原理。虽然这些分析文章帮助我重现了这个漏洞的利用,并且貌似解释清楚了原理,但是,有一个问题我还是没有在这些文章中找出。
0x01 被忽略的角落
我先在这里把被忽略的问题写出来,让我们带着问题来看这几篇文章都迷失在哪里。被忽略的问题就是:
Joomla改变了PHP默认的SESSION处理方式了吗?
如果你跟我一样,看过各家对这个漏洞的分析文章,肯定会对下面这段话有印象:
键名 + 竖线 + 经过 serialize() 函数反序列处理的值。
在分析文章中这句话的出现有像这样的解释:
或者这样:
还有这样:
看上去都将矛头指向了Joomla自己实现SESSION机制的处理,而没有使用PHP默认的SESSION机制导致的对象注入问题。作为不懂PHP SESSION机制的我只好先去找了几篇关于这方面介绍的文章去学习。
0x02 PHP SESSION的自定义
如果想在PHP中自己定义处理SESSION过程,需要使用session_set_save_handler函数来将相关的自定义处理方法进行注册,注册后再使用session_start启动SESSION机制就可以使用自己定义的函数处理SESSION了。代码示例如下:
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
|
print
"Session opened.
print "
Sess_path
:
$
sess_path
print
"Sess_name: $sess_name
return true; }
function sess_close() {
print "
Session
closed
.
&
lt
;
br
&
gt
;
";
return true;
}
function sess_read($sess_id) {
print "
Session
read
.
&
lt
;
br
&
gt
;
";
print "
Sess_ID
:
$
sess_id
&
lt
;
br
&
gt
;
";
return '';
}
function sess_write($sess_id, $data) {
print "
Session
value
written
.
&
lt
;
br
&
gt
;
";
print "
Sess_ID
:
$
sess_id
&
lt
;
br
&
gt
;
";
print "
Data
:
$
data
&
lt
;
br
&
gt
;
&
lt
;
br
&
gt
;
";
$fp = fopen('C:/wamp/www/999.txt','w');
fwrite($fp, $data);
fclose($fp);
return true;
}
function sess_destroy($sess_id) {
print "
Session
destroy
called
.
&
lt
;
br
&
gt
;
";
return true;
}
function sess_gc($sess_maxlifetime) {
print "
Session
garbage
collection
called
.
&
lt
;
br
&
gt
;
";
print "
Sess_maxlifetime
:
$
sess_maxlifetime
&
lt
;
br
&
gt
;
";
return true;
}
session_set_save_handler
("
sess
_open
", "
sess
_close
", "
sess
_read
", "
sess
_write
", "
sess
_destroy
", "
sess
_gc"
)
;
session_start
(
)
;
|
在对SESSION相关变量进行赋值时会调用注册的sess_write方法进行处理,为了方便了解传入的参数内容,我print出了data变量。我们来看一下赋值的这个过程:
可以看出data变量传入的就是PHP SESSION机制序列化好的内容,也就是说即使我们自定义了处理SESSION的函数,但是如果没有对数据进行处理的话,SESSION还是那个PHP自己序列化的SESSION。了解到这里后,我们再回来看Joomla的SESSION的处理部分:
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
|
session_set_save_handler
(
array
(
$
this
,
'open'
)
,
array
(
$
this
,
'close'
)
,
array
(
$
this
,
'read'
)
,
array
(
$
this
,
'write'
)
,
array
(
$
this
,
'destroy'
)
,
array
(
$
this
,
'gc'
)
)
;
}
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
-
&
gt
;
getQuery
(
true
)
-
&
gt
;
update
(
$
db
-
&
gt
;
quoteName
(
'#__session'
)
)
-
&
gt
;
set
(
$
db
-
&
gt
;
quoteName
(
'data'
)
.
' = '
.
$
db
-
&
gt
;
quote
(
$
data
)
)
-
&
gt
;
set
(
$
db
-
&
gt
;
quoteName
(
'time'
)
.
' = '
.
$
db
-
&
gt
;
quote
(
(
int
)
time
(
)
)
)
-
&
gt
;
where
(
$
db
-
&
gt
;
quoteName
(
'session_id'
)
.
' = '
.
$
db
-
&
gt
;
quote
(
$
id
)
)
;
// Try to update the session data in the database table.
$
db
-
&
gt
;
setQuery
(
$
query
)
;
if
(
!
$
db
-
&
gt
;
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
;
}
}
|
从上面的代码我们可以看出,Joomla注册了write函数作为写入SESSION内容时的处理函数。但是write函数除了对data变量做了一次字符替换,没有再做任何操作就存入了数据库,而这个替换是不会影响到SESSION序列化和反序列化的。
而这几篇分析文章中都提到的一句话:
键名 + 竖线 + 经过 serialize() 函数反序列处理的值。
其实就是PHP处理SESSION的默认序列化格式。
所以到这里我能得出的结论就是:
Joomla的自定义SESSION处理并不是导致对象注入的元凶,SESSION仍然是按照默认的机制进行序列化的。
到这里我们前面提出来的问题得到了答案,但是我们貌似更看不懂这个漏洞了,既然所有的SESSION都是使用PHP默认机制来完成序列化的,那么这个漏洞是怎么形成的呢?现在,我们带一个新问题来继续分析:
PHP默认的SESSION机制有问题吗?
0x03 老司机带我来跳坑
前面章节提到的几篇分析文章除了共同将矛头指向了Joomla自定义SESSION处理,还有一个共同点就是都引用ryat老司机的《PHP Session 序列化及反序列化处理器设置使用不当带来的安全隐患》这篇文章。
这篇文章指出,如果写入SESSION和读取SESSION使用的方式不一样,可能会造成对象注入的问题。这里引用ryat文章中的例子来说明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
session_start
(
)
;
$
\
_SESSION
[
'ryat'
]
=
$
\
_GET
[
'ryat'
]
;
//foo2.php
class
ryat
{
var
$
hi
;
function
__wakeup
(
)
{
echo
'hi'
;
}
function
__destruct
(
)
{
echo
$
this
-
&
gt
;
hi
;
}
}
|
在存储SESSION使用的是php_serialize方式,而读取使用的是php方式。这两种方式对应的序列化格式是这样的:
处理器 | 对应的存储格式 |
---|---|
php | 键名 + 竖线 + 经过 serialize() 函数反序列处理的值 |
php_serialize (php>=5.5.4) | 经过 serialize() 函数反序列处理的数组 |
在这种存储和读取方式不同的情况,我们很容易理解对象注入的问题,通过foo1.php?ryat=|O:4:”ryat”:1:{s:2:”hi”;s:4:”ryat”;}来将竖线前面的字符作为键名,让serialize()函数反序列化竖线后面我们输入的内容。难道Joomla在SESSION也像实例代码那样修改了session.serialize_handler?但是并没有,我翻遍了Joomla的代码,也没有找到session.serialize_handler相关的任何代码,所以Joomla仍然使用统一的SESSION序列化和反序列化方式。
我卡在了这个地方好久,为了找到原因,我决定脱离Joomla代码,使用我前面给出的PHP SESSION自定义函数来复现这个反序列化漏洞,因为这样整个序列化和反序列过程很简单,并且我还可以print序列化内容。就在我第一次尝试的时候,我发现了一个问题:
注入的竖线居然原封不动的print的出来了!天啊!老司机在他的文章里留了一个扣子。PHP的SESSION使用php方式进行序列化时,是不会对输入内容检查、过滤或者转义竖线的,那么这里我们可以得到第二个问题的答案:
PHP的SESSION机制存在潜在的对象注入隐患
PHP序列化SESSION内容的源码内容如下:
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
|
c
#define PS_ENCODE_LOOP(code) do { \
HashTable *
_ht
=
Z_ARRVAL_P
(
PS
(
http_session_vars
)
)
;
\
int
key_type
;
\
\
for
(
zend_hash_internal_pointer_reset
(
_ht
)
;
\
(
key_type
=
zend_hash_get_current_key_ex
(
_ht
,
&
amp
;
key
,
&
amp
;
key_length
,
&
amp
;
num_key
,
0
,
NULL
)
)
!=
HASH_KEY_NON_EXISTANT
;
\
zend_hash_move_forward
(
_ht
)
)
{
\
if
(
key_type
==
HASH_KEY_IS_LONG
)
{
\
continue
;
\
}
\
key_length
--
;
\
smart_str_appendl
(
&
amp
;
buf
,
key
,
key_length
)
;
if
(
memchr
(
key
,
PS_DELIMITER
,
key_length
)
||
memchr
(
key
,
PS_UNDEF_MARKER
,
key_length
)
)
{
smart_str_free
(
&
amp
;
buf
)
;
return
FAILURE
;
}
smart_str_appendc
(
&
amp
;
buf
,
PS_DELIMITER
)
;
}
else
{
smart_str_appendc
(
&
amp
;
buf
,
PS_UNDEF_MARKER
)
;
smart_str_appendl
(
&
amp
;
buf
,
key
,
key_length
)
;
smart_str_appendc
(
&
amp
;
buf
,
PS_DELIMITER
)
;
\
}
\
}
\
}
while
(
0
)
|
0x04 Joomla漏洞原理
通过user-agent注入序列化对象代码到SESSION内容中,利用SESSION内容会存入数据库,通过使用utf-8的畸形字符截断部分内容,使注入对象后的序列化字符串仍保证正确结构。利用PHP默认SESSION序列化的php方式,通过传入带竖线的字符串,来使前面的序列化内容作为键值,保证发序列化过程不会在解析注入对象内容前停止,从而实现用户自定义对象解析。
对于SESSION反序列化部分的内容,感兴趣的各位可以看一下LN的这篇文章,从PHP源码的层面分析了这个过程。
0x05 总结
这个漏洞最有意义的地方,就是告诉了我们在PHP下使用默认的SESSION机制会存在对象注入的风险。如果你对于SESSION的序列化内容进行了存储(文件或者数据库),那么请注意这些存储对象的一些截断特性,否则和SESSION序列化特性配合起来,威力不容小觑啊。