Thinkphp作为php的经典框架之一,其安全行还是基本可靠的。这里分析一下Thinkphp3.2中的几个网上披露出来的漏洞

orderby注入

切换到补丁前:git checkout 9e1db19
测试代码如下

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()
{
$order = I('get.order');
M('users')->where('1=1')->order($order)->select();
}
}

payload为:?order[updatexml(0,concat(0x7e,user()),1)]=1

造成漏洞的原因在parseOrder函数中/ThinkPHP/Library/Think/Db/Driver.class.php:754

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
protected function parseOrder($order)
{
if (empty($order)) {
return '';
}
$array = array();
if (is_array($order)) {
foreach ($order as $key => $val) {
if (is_numeric($key)) {
if (false === strpos($val, '(')) {
$array[] = $this->parseKey($val);
}
} else {
$sort = in_array(strtolower($val), array('asc', 'desc')) ? ' ' . $val : '';
$array[] = $this->parseKey($key) . $sort;
}
}
} elseif ('[RAND]' == $order) {
// 随机排序
$array[] = $this->parseRand();
} else {
foreach (explode(',', $order) as $val) {
if (preg_match('/\s+(ASC|DESC)$/i', rtrim($val), $match, PREG_OFFSET_CAPTURE)) {
$array[] = $this->parseKey(ltrim(substr($val, 0, $match[0][1]))) . ' ' . $match[1][0];
} elseif (false === strpos($val, '(')) {
$array[] = $this->parseKey($val);
}
}
}
$order = implode(',', $array);
return !empty($order) ? ' ORDER BY ' . $order : '';
}

如果我们传入的是数组形式,则进入的是下面的代码:

1
2
3
4
5
6
7
8
9
10
11
if (is_array($order)) {
foreach ($order as $key => $val) {
if (is_numeric($key)) {
if (false === strpos($val, '(')) {
$array[] = $this->parseKey($val);
}
} else {
$sort = in_array(strtolower($val), array('asc', 'desc')) ? ' ' . $val : '';
$array[] = $this->parseKey($key) . $sort;
}
}

如果传入的数组$key不是数字,则通过$this->parseKey($key)后,拼接进$array,跟进看一下/ThinkPHP/Library/Think/Db/Driver/Mysql.class.php:101

1
2
3
4
5
6
7
8
protected function parseKey($key)
{
$key = trim($key);
if (!is_numeric($key) && !preg_match('/[,\'\"\*\(\)`.\s]/', $key)) {
$key = '`' . $key . '`';
}
return $key;
}

这里的正则形同虚设,也就是说我们的恶意数据压根没处理,导致了SQL注入

修复


给parseKey加了一个参数控制判断,调用时使用parseKey($key,true)即可

update注入

git checkout 977806d
测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
namespace Home\Controller;

use Think\Controller;

class IndexController extends Controller
{
public function index()
{
$data['name'] = I('get.name');
$data['passwd'] = I('get.passwd');
$map['id'] = I('get.id');
M('users')->where($map)->save($data);
}
}

模仿已有的payload的写一个:index.php?passwd[]=123&name=xiaot&id[0]=bind&id[1]=0+and+(updatexml(1,concat(0x7e,(select+user()),0x7e),1))

看到报错,但是并没有注出信息来,通过断点调试一波,追到/ThinkPHP/Library/Think/Db/Driver.class.php:416

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected function parseSet($data)
{
foreach ($data as $key => $val) {
if (isset($val[0]) && '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) . '=' . $val;
} else {
$name = count($this->bind);
$set[] = $this->parseKey($key) . '=:' . $key . '_' . $name;
$this->bindParam($key . '_' . $name, $val);
}
}
}
return ' SET ' . implode(',', $set);
}

经过该函数处理后,我们的$data数据进入set操作,形成参数绑定的形式,只不过和网上的有点不一样

先不管,继续追到/ThinkPHP/Library/Think/Db/Driver.class.php:606下的parseWhereItem函数,其中对bind表达式有处理:

1
2
3
} elseif ('bind' == $exp) {
// 使用表达式
$whereStr .= $key . ' = :' . $val[1];

这里的拼接操作形成如下形式:

也就是说在执行后面绑定的时候,这里的:0并没有参数去替换,所以我们需要更改一下id[1]=name_0+and+(updatexml(1,concat(0x7e,(select+user()),0x7e),1)),于是就可以继续注入了,所以要利用还需要根据实际情况来???

修复

官方直接在think_filter中添加了对bind过滤