0x00 简介

最近获取一段代码,以前竟然没有关注类似的,现在动手调式一下

代码如下,这里在foreach的时候会自动触发current方法,

就像是魔术方法一样,我们知道PHP的魔术方法如下

  • __construct构造方法
  • __destruct析构方法
  • __clone:当对象复制完成时调用,如:clone $this
  • __call:非静态方式调用不存在的方法
  • __callStatic:静态方式调用不存在方法
  • __get:反序列化中多通过不存在的属性触发,如:$this->a->b
  • __isset:当对不可访问方法调用isset或者empty时调用
  • __set:设置私有属性,反序列化中不这么用
  • __set_state:调用var_export导出类时,此方法会被自动调用
  • __invoke:调用函数的方式调用一个类时,如:(new A())()
  • __sleep:序列化时调用
  • __toString:类被当成字符串时,这就很多了,如:strstr(new A())
  • __unset:当对不可访问方法调用unset时调用
  • __wakeup:反序列化时调用
  • __debugInfo:打印所需调式信息
  • __autoload :尝试加载未定义的类

就是比魔术方法加了条件,需要继承自数组迭代器ArrayIterator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class A extends ArrayIterator{
protected $call;
public function __construct($data, $callback){
parent::__construct($data);
$this->call = $callback;
}

public function current(){
// $value = "whoami";
$value = parent::current();
$value = call_user_func($this->call,$value);
var_dump($value);
return $value;
}
}

$a = new A(array('whoami'),'system');
foreach ($a as $b){
echo ":aaa";
}
?>

image-20220427105759230

0x01 流程跟踪

根据之前的PHP代码的生命周期我们这里直接跟脚本代码执行阶段

触发过程

触发的过程是在zend_execute中的,此时op_array已经在编译后被赋值,其中的handler也已经被赋值,现在的指针指向第一个handler,是类似初始的handler,其中没有什么操作,只是把指针指向下一个

image-20220427111849277

image-20220427111814924

然后就开始循环调用handler的模式

image-20220427112445154

循环跟进几次之后,发现了一个关键函数ZEND_FE_FETCH_R_SPEC_VAR_HANDLER

这里还有个关键点,后续会用到,就是根据我们的代码我们传入的遍历对象是一个类

那么此处的if语句不成立,会进入else分支

image-20220427113107263

else分支里面会对我们类的类型进行判断

image-20220427113209699

跟进之后,存在一个宏定义,可以看到就是获取handlers

image-20220427113457278

可以看到我们这个数组迭代器就是

image-20220427113628814

接下来将不会返回NULL,会进入else分支

存在一个关键调用

image-20220427113841119

image-20220427113946241

继续跟进发现关键定义zend_call_method_with_0_params

image-20220427114025558

发现会调用zend_call_method,而其中需要的function_name正式current

除此之外还看到了

  • zend_call_method_with_1_params
  • zend_call_method_with_2_params

image-20220427114155898

image-20220427114535418

最终通过zend_execute_ex触发,此时的call内容如下

image-20220427114650651

再继续跟进后可以发现,已经调用了current

image-20220427115023354

到了此处无需再跟进了,其实这种方式还可以写马,针对一些动态监测的,不知道不可以,如下,没测试,就算不行的话还可以变形

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
class A extends ArrayIterator{
protected $call;
public function __construct($data, $callback){
$data = array(base64_decode($data));
$callback = base64_decode($callback);
parent::__construct($data);
$this->call = $callback;
}

public function current(){
// $value = "whoami";
$value = parent::current();
$value = call_user_func($this->call,$value);
var_dump($value);
return $value;
}
}

$a = "A";
$a = new $a('d2hvYW1p','c3lzdGVt');
foreach ($a as $b){
echo ":aaa";
}
?>

当然这里有一点就是,编译的handler是根据foreach操作来的

后续判断是否是迭代器来的

所以理论上所有正常运行的继承自迭代器的都可以

image-20220427123300846

其他的还得构造,这里用个和array类似的RecursiveArrayIterator

image-20220427123403454

0x02 为什么?

其实我更想知道的是为什么会调用这个ZEND_FE_FETCH_R_SPEC_VAR_HANDLER

根据之前的知识可以知道,每种写法都对应固定的操作,操作就是调用一个handler函数

而这一步的完成其实是在编译过程的

而这里的关键操作就是foreach

跟进zend_compile_foreach方法

这里设置了两条指令

image-20220427120014353

倒数第二个传参其实就是op1op2NULL

  • 8对应的是CV,第一条指令调用的是ZEND_FE_RESET_R_SPEC_CV_HANDLER

  • 4对应的VAR,第二条指令调用的就是ZEND_FE_FETCH_R_SPEC_VAR_HANDLER

这里就是关键了

0x03 思考

刚才提到了三个方法都会调用zend_call_method

  • zend_call_method_with_0_params

  • zend_call_method_with_1_params

  • zend_call_method_with_2_params

随便找一处

image-20220427122420103

继续根进

image-20220427122454351

会调用offsetSet方法

image-20220427122554747

构造代码

1
2
3
4
5
6
7
8
9
10
11
<?php
class A extends ArrayObject {
public function offsetSet($key,$value){
phpinfo();
}
}

$a = "A";
$a = new $a();
$a->append('fourth');
?>

image-20220427122647872

0x04 end

…底层还是美妙