前言(文章首发于先知社区)
发现自己对tp的底层不太熟悉,看了@phpoop师傅文章有所启发,于是有此文,记录自己的分析过程
希望大师傅们嘴下留情,有分析不对的地方还请师傅们指出o r
Thinkphp3.2.3
首先开启调试
在/Application/Home/Conf/config.php
加上1
'SHOW_PAGE_TRACE' => true,
并且添加数据库配置1
2
3
4
5
6
7
8
9
10//数据库配置信息
'DB_TYPE' => 'mysql', // 数据库类型
'DB_HOST' => 'localhost', // 服务器地址
'DB_NAME' => 'thinkphp', // 数据库名
'DB_USER' => 'root', // 用户名
'DB_PWD' => '123456', // 密码
'DB_PORT' => 3306, // 端口
'DB_PREFIX' => 'think_', // 数据库表前缀
'DB_CHARSET'=> 'utf8', // 字符集
'DB_DEBUG' => TRUE, // 数据库调试模式 开启后可以记录SQL日志 3.2.3新增
测试数据如下
添加实例代码
用I函数进行动态获取参数
field
field方法属于模型的连贯操作方法之一,主要目的是标识要返回或者操作的字段,可以用于查询和写入操作
1 |
|
执行语句相当于
where
1 |
|
接着请求http://127.0.0.1/thinkphp3/index.php?m=Home&c=index&a=index&age=1
转义代码分析
当我们请求age=1'
尝试注入的时候
被自动转义了
find函数里,会解析出options
跟入
继续跟进
在parseSql
里会依此执行函数
发现在ThinkPHP/Library/Think/Db/Driver.class.php
的函数parseWhere
里
1 | protected function parseWhere($where) { |
继续跟进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
69protected 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)){ // IN 运算
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)){ // BETWEEN运算
$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;
}
此时我们的key是age,val是1,于是执行
1 | }else { |
继续跟进parseValue
1
2
3
4
5
6
7
8
9
10
11
12
13
14protected function parseValue($value) {
if(is_string($value)) {
$value = strpos($value,':') === 0 && in_array($value,array_keys($this->bind))? $this->escapeString($value) : '\''.$this->escapeString($value).'\'';
}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;
}
可以发现这里就执行了escapeString
返回了转义后的结果
调用栈如下
如何注入
既然如此,那么怎么去注入呢,底层就调用了escapeString
我们看到parseWhereItem
函数
在绿色标记的几个判断语句里,是没有调用parseValue
函数的,也就不会调用到escapeString
然后我们又可以看到,exp就是val数组的第一个值
那么我们是不是就能注入了呢
我们修改代码如下1
2
3
4
5
6
7
8
9
10
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$age = $_GET['age'];
$User = M("user"); // 实例化User对象
$User->field('username,age')->where(array('age'=>$age))->find();
}
}
这里暂时不用I函数接收参数
传入payload
`http://127.0.0.1/thinkphp3/index.php?
我们进入了判断
返回值并没有转义,页面上也能够直接看出来
为什么用exp不用bind呢,因为bind执行后的结果
会拼接一个 = :
这显然是对我们注入不利的
那么 我们利用报错注入1
http://127.0.0.1/thinkphp3/index.php?m=Home&c=index&a=index&age[0]=exp&age[1]==%271%27%20and%20(extractvalue(1,concat(0x7e,(select%20user()),0x7e)))%20%23
成功造成了注入
不过我们接收参数修改为I函数
I函数
修改代码1
2
3
4
5
6
7
8
9
10
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$age = I("GET.age");
$User = M("user"); // 实例化User对象
$User->field('username,age')->where(array('age'=>$age))->find();
}
}
同样的请求发现报错了
在
我们跟进调试一下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
134function 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;
}
首先获取method
然后取age值并赋值给data
接着看是否传入了filter
在手册中也是介绍了
https://www.kancloud.cn/manual/thinkphp/1841
这里就是默认的htmlspecialchars
关于该函数的一些用法
https://www.w3school.com.cn/php/func_string_htmlspecialchars.asp
跟入函数,最终是要调到这个call_user_func
调用htmlspecialchars
处理后,对我们的payload影响不太大,那么继续跟
这里又对是数组data里的两个值exp
和$payload
进行了think_filter
函数的调用1
2
3
4
5
6
7
8function 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 .= ' ';
}
}
这里就对一些sql敏感的东西进行了过滤
此时,我们的data[0]
是exp
字符串,这里就匹配了,于是他在exp
后面加上了一个空格
也就是exp
然后我们的payload并没有匹配到
那么自然
到了parseWhereItem
也就进不了exp那一个判断了,直接进入报错的地方
这样 我们也就没办法再进行注入了
总结
也就是说在thinkphp3下,使用了I函数,我们的注入就不太能成功,如果接收参数的时候并没有使用I函数,而是直接接收就传入M函数并实例化,那么我们注入的可能性就更大
Thinkphp5.0.24
在Thinkphp5里,所有单个字母的函数都被取消了
查询语句变成了1
Db::table('think_user')->where('id',1)->find();
于是修改index.php代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace app\index\controller;
use think\Db;
class Index
{
public function index()
{
$age = $_GET['age'];
$User = Db::name('user');
//为了方便调试我将select设置false
echo $User->where(array('age'=>$age))->select(false);
}
}
接着修改application/database.php
1
'debug' => true,
于是看到查询语句
底层过滤
我们改一下代码1
2
3
4
5
6
7
8
9
10
11
12
13
namespace app\index\controller;
use think\Db;
class Index
{
public function index()
{
$age = $_GET['age'];
Db::name('user')->where(array('age'=>$age))->find();
}
}
传入单引号同样被转义,应该也是在select函数里进行了转义
跟入select
继续跟进,在parseWhere时,返回了占位符
在select结束后,返回了预编译的sql语句,:where_AND_age
是占位符
跟入getRealSql
提取age的值
在这里,就发生了转义1
2
3if (PDO::PARAM_STR == $type) {
$value = $this->quote($value);
}
跟进quote
里面又调用了quote
,关于PDO::quote的介绍
PDO::quote
会转义特殊字符串,也就是我们的单引号
如果一开始的代码是select()不用false
那么调用栈如下
insert方法
看了网上有分析该方法存在注入,于是调试
修改代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace app\index\controller;
use think\Db;
class Index
{
public function index()
{
$username = $_GET['username'];
$User = Db::name('user');
echo $User->where(array('age'=>'13'))->insert(array('username'=>$username));
}
}
跟进insert
继续跟进parseData
1 | protected function parseData($data, $options) |
跟tp3类似的思路
但是,注意这里的拼接1
2case 'inc':
$result[$item] = $item . '+' . floatval($val[1]);
对$val[1]
进行了一个floatval
的强转,那么我们的payload也就不行了
Thinkphp6开发版
使用composer安装1
composer create-project topthink/think=6.0.x-dev tp
然后运行1
php think run
访问127.0.0.1:8000
或者直接访问public目录
index.php代码修改1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace app\controller;
use app\BaseController;
use think\facade\Db;
class Index extends BaseController
{
public function index()
{
$age = input('get.age');
echo Db::table('think_user')->where(array('age'=>$age))->fetchSql()->find(1);
}
}
跟tp5类似预加载
跟入fetch
跟入getRealSql
这里 调用了addslashes
对单引号进行了转义
我们再看看其他方法
insert
修改代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace app\controller;
use app\BaseController;
use think\facade\Db;
class Index extends BaseController
{
public function index()
{
$age = input('get.age');
echo Db::table('think_user')->where(array('age'=>'15'))->fetchSql()->insert(array('age'=>$age));
}
}
跟进insert
跟入parsedata
同样的处理方式,把payload进行强转,不过取消了exp