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

作者: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 );
         }

你可能感兴趣的:(php-hack)