考点
- php-7.4 新特性 preload
- 在预加载文件中使用PHP FFI特性
- 反序列化
题解
打开题目,是很简单的源码:
1 |
|
越简单越搞事,所以先看一下phpinfo
信息:
1 | ?a=phpinfo(); |
可以看到的是php的版本很高:7.4.0-dev
。
另外一个是disable_function
信息:
在这里过滤掉了我们常用的命令执行的函数,但是没有过滤scandir
,那就用这个函数来看一下其他文件信息。
1 | ?a=var_dump(scandir(".")); |
有一个preload.php
文件,看一下里面是啥:
1 | ?a=highlight_file("preload.php"); |
得到preload.php
,该文件就是opcache.preload
指定的preload
文件:
1 |
|
然后我本来还想再确定一些flag文件在哪里的,所以看了下根目录/
:
1 | ?a=var_dump(scandir("/")); |
结果发现因为设置了open_basedir
路径,所以无法读取除了/var/www/html
目录以外的文件。
其实这些信息最好在刚才查看phpinfo
信息的时候就收集好。所以重新收集一下本关需要的phpinfo信息(看其他的大佬写的wp):
1 | php version: 7.4.0-dev |
opcache.preload是PHP 7.4中的重量级特性,也就是我们说的预加载。在这之前,需要先了解下opcache
是什么东西。
我们学习过一些高级语言,比如C/C++、Java、ASP、C#以及世界上最好的语言PHP(逃走),这些语言可以分为两种:解释型语言和编译型语言。
编译型语言,也就是用对应的编译器,将高级语言一次翻译成对应平台硬件下可执行的机器码(机器指令和操作数),并且打包成可执行文件,这样就能在多个平台上运行,也就是一人挖井,众人吃水就行了,C/C++就是属于编译型语言。
而解释型语言,就是在程序运行前将源程序预编译成中间语言(中间码),然后再由解释器执行中间语言。解释型语言的缺点就是,每次执行程序都需要重新编译,所以运行效率就降低了,而且不能脱离解释器单独执行,就像Java语言依赖于JVM(但其实Java不能简单的说就是解释型语言,因为所有的Java代码都是需要编译的),PHP依赖于Zend引擎。而且因为需要重新编译,所以相比于编译型语言,解释型语言通常会占用更多的内存和CPU资源。C#、PHP都是解释型语言。
总结下两者的区别,也就是,编译型语言是在执行程序之前由编译器将代码一条一条编译成硬件能懂的机器语言,所以运行速度也会很快;而解释型语言是先进行预编译,生成中间码,然后在执行程序时,由解释器一条一条地将中间码解释为机器语言(按照执行顺序进行),因此速度慢,消耗计算机资源多。对于PHP来说,中间码就是opcode(也被称为操作码),解释器就是Zend引擎,它会将opcode翻译成计算机能懂的机器指令,因此执行效率就下降了。
比如我们有一段PHP代码:
1 |
|
php是这样去执行这段代码的:
主要分为四个部分:
1 | Lexicon scan: 词法分析阶段,将PHP代码转换为语言片段(Tokens) |
上面扯了这么多,其实重点就是PHP作为一种解释型语言,在执行效率和资源占用上面有缺点。为了缓解这个缺点,开发者就想,因为有一些代码是会被重复执行的,在一定时间内可能都不会发生改变,那么如果对这些中间码进行缓存,那么就可以节省掉很多执行时间并减少资源占用。这就是opcache的由来了。
下面这张图就是opcache的工作原理:
可以看到,在执行完create opcode之后,会将其放入cache(共享内存)中。用户再次请求时,如果该php代码片段在cache中,如果用户再次请求该代码片段命中,直接取出该opcode,进行进行执行,从节省了词法分析,预编译生成opcode的时间,提升了性能。
更具体的信息可以参考鸟哥的文章:
因为opcache带来的新优势,在现实的生产环境中,基本都会选择开启opcode,但是opcache也并非十全十美。虽然消除了编译开销,但是opcache无法解决跨文件依赖问题。举个例子来说明,Class A是继承自Class B的,但是这两个类被存储在不同的php文件中,那么在执行时仍然需要将它们链接在一起,因为每个php文件的编译和缓存都是独立于其他文件的。并且我们仍然需要检查php源文件是不是被修改了,如果是,那么原先的opcache就失效了。为了解决这个问题,php开发者们就又提出了新的机制——preload(预加载)。
preload的灵感来自于为Java HotSpot VM设计的Class Data Sharing(类数据共享)技术。preload不仅仅可以将源文件编译成为opcode,还可以将相关的class,interface链接在一起,然后将这个编译后的可执行代码保存在内存中(opcache是保存opcode)。当用户向服务器发起请求时,就从内存中取出这部分可执行代码进行执行。
更具体的信息可以参考文章:
在preload的rfc手册可以看到 https://wiki.php.net/rfc/preload#future_scope :
当与ext/FFI
一起使用时,只允许在preload的PHP文件中使用FFI
功能,而不允许在常规的PHP文件中使用。因为FFI能够直接调用相对底层的C语言库函数,所以具有一定的危险性。
FFI全称为Foreign Function interface,在 https://wiki.php.net/rfc/ffi 中对FFI的描述为:
For PHP, FFI opens a way to write PHP extensions and bindings to C libraries in pure PHP.
也就是说,对于PHP,FFI提供了一种用PHP编写PHP扩展并绑定到C函数库的方法。
查看手册,我们可以看到FFI::cdef
函数最多接受两个参数,第二个参数$lib
是可选的:
在官网上有一些调用c lib函数的例子
1 |
|
这里在命令行测试一下,确实是执行成功了:
这里没有.so文件也是可以成功执行,可以看到官方手册给出的解释是因为:
If
lib
is omitted, platforms supporting RTLD_DEFAULT attempt to lookup symbols declared incode
in the normal global scope. Other systems will fail to resolve these symbols.
引用下mochazz师傅的说法就是:
当
$lib
参数为空时,默认为RTLD_DEFAULT
, 程序会按照默认共享库的顺序,从中搜索 system 函数第一次出现的地方并使用。搜索范围还包括了可执行程序极其依赖中的函数表(如果设置了 RTLD_GLOBAL 还会搜索动态加载库中的函数表),所以应该是这些搜索范围中存在可调用的 system 函数。
既然利用FFI可以调用C函数,那么我们就可以将$data['func']
设置为FFI::cdef
,$data['arg']
设置为int system(char *command);
来执行系统命令。
class A implements Serializable
表明类A实现了自定义的对象反序列化方法。
在preload.php
中还引入了两个新的魔术方法:__serialize
和__unserialize
。这也是在php 7.4
中新引入的特性,具体可以参考:https://wiki.php.net/rfc/custom_object_serialization 。但在本题中这个特性没啥用处,等到有时间了可以好好研究一下。
payload(接口Serializable
必须实现方法serialize()
和unserialize()
):
1 |
|
最后的payload就是:
1 | // 列出根目录下的文件 |
这道题最奇葩的是我卡在了反弹shell这里,所以到最后我也没拿到最后的flag。。。唉,还是太菜了。。。
虽然最后卡在了这里,但是总体来讲收获还是颇丰的。
踩坑记录
除了上面没有实现的反弹shell之外,在学习PHP 7.4的FFI新特性时,想要自己搭建一个环境来复现一下这个环境,然后就要修改php.ini
文件,下面这是测试之后发现必须要配置的几个选项:
1 | [opcache] |
一般来说,opcache.preload
不允许是root
用户。还有一个点就是ffi.enable
要设置为1
,如果是false
,肯定不支持preload功能,但是设置为preload
也是不够的,当ffi.enable=preload
时,仅仅支持在命令行运行,在web端是无法成功利用ffi
功能的。
好像今年12月PHP 8就要发布了,而不再发布PHP 7.5,蹲一波新的特性。