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 |
|
0x01 流程跟踪
根据之前的PHP代码的生命周期我们这里直接跟脚本代码执行阶段
触发过程
触发的过程是在zend_execute
中的,此时op_array
已经在编译后被赋值,其中的handler
也已经被赋值,现在的指针指向第一个handler
,是类似初始的handler
,其中没有什么操作,只是把指针指向下一个
然后就开始循环调用handler
的模式
循环跟进几次之后,发现了一个关键函数ZEND_FE_FETCH_R_SPEC_VAR_HANDLER
这里还有个关键点,后续会用到,就是根据我们的代码我们传入的遍历对象是一个类
那么此处的if语句不成立,会进入else
分支
else分支里面会对我们类的类型进行判断
跟进之后,存在一个宏定义,可以看到就是获取handlers
可以看到我们这个数组迭代器就是
接下来将不会返回NULL,会进入else分支
存在一个关键调用
继续跟进发现关键定义zend_call_method_with_0_params
发现会调用zend_call_method
,而其中需要的function_name
正式current
除此之外还看到了
zend_call_method_with_1_params
zend_call_method_with_2_params
最终通过zend_execute_ex
触发,此时的call
内容如下
再继续跟进后可以发现,已经调用了current
到了此处无需再跟进了,其实这种方式还可以写马,针对一些动态监测的,不知道不可以,如下,没测试,就算不行的话还可以变形
1 |
|
当然这里有一点就是,编译的handler是根据foreach操作来的
后续判断是否是迭代器来的
所以理论上所有正常运行的继承自迭代器的都可以
其他的还得构造,这里用个和array
类似的RecursiveArrayIterator
0x02 为什么?
其实我更想知道的是为什么会调用这个ZEND_FE_FETCH_R_SPEC_VAR_HANDLER
根据之前的知识可以知道,每种写法都对应固定的操作,操作就是调用一个handler
函数
而这一步的完成其实是在编译过程的
而这里的关键操作就是foreach
跟进zend_compile_foreach
方法
这里设置了两条指令
倒数第二个传参其实就是op1
,op2
为NULL
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
随便找一处
继续根进
会调用offsetSet
方法
构造代码
1 |
|
0x04 end
…底层还是美妙