作者:Balisong
稿费:500RMB
Exponent cms是一款国外的cms,功能比较强大。但是在2.3.8版本及以下,存在着一个全版本通杀的任意文件上传漏洞。攻击者可以通过该漏洞直接getshell.
官方最新版2.3.9已经修复(http://www.exponentcms.org)
漏洞分析:
我们首先看一下漏洞触发点在:
/framework/modules/ecommerce/controllers/eventregistrationController.php中第1161行:
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
|
if
(!
empty
(
$_FILES
[
'attach'
][
'size'
])) {
$dir
=
'tmp'
;
$filename
= expFile::fixName(time() .
'_'
.
$_FILES
[
'attach'
][
'name'
]);
$dest
=
$dir
.
'/'
.
$filename
;
//Check to see if the directory exists. If not, create the directory structure.
if
(!
file_exists
(BASE.
$dir
)) expFile::makeDirectory(
$dir
);
// Move the temporary uploaded file into the destination directory, and change the name.
$file
= expFile::moveUploadedFile(
$_FILES
[
'attach'
][
'tmp_name'
], BASE .
$dest
);
// $finfo = finfo_open(FILEINFO_MIME_TYPE);
// $relpath = str_replace(PATH_RELATIVE, '', BASE);
// $ftype = finfo_file($finfo, BASE.$dest);
// finfo_close($finfo);
if
(!
empty
(
$file
))
$mail
->attach_file_on_disk(BASE .
$file
, expFile::getMimeType(BASE .
$file
));
}
$from
=
array
(ecomconfig::getConfig(
'from_address'
) => ecomconfig::getConfig(
'from_name'
));
if
(
empty
(
$from
[0]))
$from
= SMTP_FROMADDRESS;
$mail
->quickBatchSend(
array
(
'headers'
=>
$headers
,
'html_message'
=>
$this
->params[
'email_message'
],
'text_message'
=>
strip_tags
(
str_replace
(
"
,
""
,
$this
->params[
'email_message'
])),
'to'
=>
$email_addy
,
'from'
=>
$from
,
'subject'
=>
$this
->params[
'email_subject'
]
));
if
(!
empty
(
$file
))unlink(BASE .
$file
);
// delete temp file attachment
flash(
'message'
, gt(
"You're email to event registrants has been sent."
));
expHistory::back();
}
|
然后我们可以看到这里有一个文件上传的操作,我们跟踪一下moveUploadedFile函数,在/framework/modules/file/models/expFile.php中第1508行:
1
2
3
4
5
6
7
8
9
|
public
static
function
moveUploadedFile(
$tmp_name
,
$dest
) {
move_uploaded_file(
$tmp_name
,
$dest
);
if
(
file_exists
(
$dest
)) {
$__oldumask
= umask(0);
chmod
(
$dest
, octdec(FILE_DEFAULT_MODE_STR + 0));
umask(
$__oldumask
);
return
str_replace
(BASE,
''
,
$dest
);
}
else
return
null;
}
|
这里没有对后缀名进行一个检测,可以上传任意文件。文件命名的方式是time()+下划线+文件名。
然后我们看到紧跟着就有一个文件删除的操作:
if (!empty($file))unlink(BASE . $file);
看起来是没有问题的,传上去之后立马删除掉了,因为文件存在的时间超级短,并且文件命名的方式里面带有时间戳,导致我们无法利用这个文件。
但是这里有个细节,就是在上传文件到删除文件的过程中,调用了一个函数操作:
也就是
1
2
3
4
5
6
7
8
|
$mail
->quickBatchSend(
array
(
'headers'
=>
$headers
,
'html_message'
=>
$this
->params[
'email_message'
],
'text_message'
=>
strip_tags
(
str_replace
(
"
,
""
,
$this
->params[
'email_message'
])),
'to'
=>
$email_addy
,
'from'
=>
$from
,
'subject'
=>
$this
->params[
'email_subject'
]
));
|
我们开始跟踪一下该函数:
在/framework/core/subsystems/expMail.php中第378行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public
function
quickBatchSend(
$params
=
array
()) {
if
(
empty
(
$params
[
'html_message'
]) &&
empty
(
$params
[
'text_message'
])) {
return
false;
}
// set up the to address(es)
if
(
is_array
(
$params
[
'to'
])) {
$params
[
'to'
] =
array_filter
(
$params
[
'to'
]);
}
else
{
$params
[
'to'
] =
array
(trim(
$params
[
'to'
]));
}
if
(
empty
(
$params
[
'to'
])) {
$params
[
'to'
] =
array
(trim(SMTP_FROMADDRESS));
// default address is ours
}
$this
->addTo(
$params
[
'to'
]);
// we only do this to save addresses in our object
// set up the from address(es)
if
(
is_array
(
$params
[
'from'
])) {
$params
[
'from'
] =
array_filter
(
$params
[
'from'
]);
}
else
{
$params
[
'from'
] = trim(
$params
[
'from'
]);
}
|
在这里又调用了一个函数addto(),我们继续跟踪该函数,在该文件的 644行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public
function
addTo(
$email
= null) {
// attempt to fix a bad to address
if
(
is_array
(
$email
)) {
foreach
(
$email
as
$address
=>
$name
) {
if
(
is_integer
(
$address
)) {
if
(
strstr
(
$name
,
'.'
) === false) {
$email
[
$address
] .=
$name
.
'.net'
;
}
}
}
}
else
{
if
(
strstr
(
$email
,
'.'
) === false) {
$email
.=
'.net'
;
}
}
$this
->to =
$email
;
if
(!
empty
(
$email
)) {
$this
->message->setTo(
$email
);
//fixme this resets the 'to' addresses, unless using $this->message->addTo($email);
// $this->message->addTo($email); //if you need to reset the 'to' addresses, use $this->flushRecipients();
}
}
|
这里又调用了一个setTo()函数,我们继续跟踪该函数,在/external/swiftmailer-5.4.2/lib/classes/Swift/Mime/SimpleMessage.php中第316行:
1
2
3
4
5
6
7
8
9
10
11
|
public
function
setTo(
$addresses
,
$name
= null)
{
if
(!
is_array
(
$addresses
) && isset(
$name
)) {
$addresses
=
array
(
$addresses
=>
$name
);
}
if
(!
$this
->_setHeaderFieldModel(
'To'
, (
array
)
$addresses
)) {
$this
->getHeaders()->addMailboxHeader(
'To'
, (
array
)
$addresses
);
}
return
$this
;
}
|
这里调用了一个addMailboxHeader函数,我们继续追踪该函数,在/external/swiftmailer-5.4.2/lib/classes/Swift/Mime/SimpleHeaderSet.php中第65行:
1
2
3
4
5
|
public
function
addMailboxHeader(
$name
,
$addresses
= null)
{
$this
->_storeHeader(
$name
,
$this
->_factory->createMailboxHeader(
$name
,
$addresses
));
}
|
这里又调用了一个createMailboxHeader函数,我们继续跟踪,在/external/swiftmailer-5.4.2/lib/classes/Swift/Mime/SimpleHeaderFactory.php中第54行:
1
2
3
4
5
6
7
8
9
10
|
public
function
createMailboxHeader(
$name
,
$addresses
= null)
{
$header
=
new
Swift_Mime_Headers_MailboxHeader(
$name
,
$this
->_encoder,
$this
->_grammar);
if
(isset(
$addresses
)) {
$header
->setFieldBodyModel(
$addresses
);
}
$this
->_setHeaderCharset(
$header
);
return
$header
;
}
|
这里又调用到了一个setFieldBodyModel函数,我们继续跟踪, /external/swiftmailer-5.4.2/lib/classes/Swift/Mime/Headers/MailboxHeader.php中第61行:
1
2
3
4
|
public
function
setFieldBodyModel(
$model
)
{
$this
->setNameAddresses(
$model
);
}
|
这里调用了一个setNameAddresses函数,我们继续跟踪该函数,在该文件104行:
1
2
3
4
5
|
public
function
setNameAddresses(
$mailboxes
)
{
$this
->_mailboxes =
$this
->normalizeMailboxes((
array
)
$mailboxes
);
$this
->setCachedValue(null);
//Clear any cached value
}
|
这里又调用了normalizeMailboxes函数,我们继续跟踪该函数,在该文件的250行:
这里调用了一个_assertValidAddress函数,我们继续跟踪该函数,在该文件的第344行:
1
2
3
4
5
6
7
8
9
10
11
|
private
function
_assertValidAddress(
$address
)
{
echo
$this
->getGrammar()->getDefinition(
'addr-spec'
);
if
(!preg_match(
'/^'
.
$this
->getGrammar()->getDefinition(
'addr-spec'
).
'$/D'
,
$address
)) {
throw
new
Swift_RfcComplianceException(
'Address in mailbox given ['
.
$address
.
'] does not comply with RFC 2822, 3.6.2.'
);
}
}
|
可以看到这里对于我们传入的$address做了一个正则匹配,如果正则不匹配的话,就会throw出错误信息,导致运行的程序运行的中止。那么结合我们上面所说的,这个步骤是在上传文件完成之后,删除文件之前执行的,如果这个步骤出了错,那么就不会对上传文件进行删除。那么我们上传的文件就存活了下来。
那么怎样让这个正则匹配失效呢?
可以看到这个正则匹配是验证你是否是有效的邮箱地址,如果不是有效的邮箱地址就会报错,那么我们传入一个错误的邮箱地址的话,就会报错了。但是这里我们不这么“简单”的做,我们搞一点有意思的事情。
我们首先看一下我们参数传入的地方:
在/framework/modules/ecommerce/controllers/eventregistrationController.php中第1149行:
1
2
3
|
$email_addy
=
array_flip
(
array_flip
(
$this
->params[
'email_addresses'
]));
$email_addy
=
array_map
(
'trim'
,
$email_addy
);
$email_addy
=
array_filter
(
$email_addy
);
|
这里的$email_addy是我们可控的。用户正常的输入的话,这个地方$this->params['email_addresses']应该是一个数组,然后后面的一切都能正规的运行下去,不会出错,但是!!!如果这个地方我们不传入数组会怎么样?正如大家知道的,array_flip()是对数组进行操作的,但是如果我们给它传入一个字符串的话,那么结果会返回一个null,意思就是说现在$email_addy=NULL。然后我们看到将$email_addy带入到了quickBatchSend函数中去:
1
2
3
4
5
6
7
8
|
$mail
->quickBatchSend(
array
(
'headers'
=>
$headers
,
'html_message'
=>
$this
->params[
'email_message'
],
'text_message'
=>
strip_tags
(
str_replace
(
"
,
""
,
$this
->params[
'email_message'
])),
'to'
=>
$email_addy
,
'from'
=>
$from
,
'subject'
=>
$this
->params[
'email_subject'
]
));
|
在quickBatchSend中又对$email_addy做了处理:
1
2
3
4
5
6
7
8
9
|
if
(
is_array
(
$params
[
'to'
])) {
$params
[
'to'
] =
array_filter
(
$params
[
'to'
]);
}
else
{
$params
[
'to'
] =
array
(trim(
$params
[
'to'
]));
}
if
(
empty
(
$params
[
'to'
])) {
$params
[
'to'
] =
array
(trim(SMTP_FROMADDRESS));
// default address is ours
}
$this
->addTo(
$params
[
'to'
]);
// we only do this to save addresses in our object
|
首先会判断是否是数组,如果不是的话,就变成一个数组。我们知道开始$Params[‘to’]为NULL,经过强行转换之后现在的$param[‘to’]就是array(0=>””),接下来的判断很有意思,:
1
|
if
(
empty
(
$params
[
'to'
]))
|
你觉得是true还是false呢?很多人认为会是ture,但是实际上是false。因为这个数组不是空数组,它有一个元素啊!!,虽然只是一个空字符串,但是它还是有元素啊,所以数组不为空,这个条件不成立。也就不会有赋值默认邮箱的操作:
1
|
$params
[
'to'
] =
array
(trim(SMTP_FROMADDRESS));
// default address is ours
|
然后将$params[‘to’]传递给了addTo函数,我们看一下addTo函数是怎样处理$params[‘to’]的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public
function
addTo(
$email
= null) {
// attempt to fix a bad to address
if
(
is_array
(
$email
)) {
foreach
(
$email
as
$address
=>
$name
) {
if
(
is_integer
(
$address
)) {
if
(
strstr
(
$name
,
'.'
) === false) {
$email
[
$address
] .=
$name
.
'.net'
;
}
}
}
}
else
{
if
(
strstr
(
$email
,
'.'
) === false) {
$email
.=
'.net'
;
}
}
$this
->to =
$email
;
if
(!
empty
(
$email
)) {
$this
->message->setTo(
$email
);
|
里经过处理后,$email的值为array(1) { [0]=> string(4) ".net" }。然后传递给了setTo做操作:
1
2
3
4
5
6
7
8
9
10
11
|
public
function
setTo(
$addresses
,
$name
= null)
{
if
(!
is_array
(
$addresses
) && isset(
$name
)) {
$addresses
=
array
(
$addresses
=>
$name
);
}
if
(!
$this
->_setHeaderFieldModel(
'To'
, (
array
)
$addresses
)) {
$this
->getHeaders()->addMailboxHeader(
'To'
, (
array
)
$addresses
);
}
|