根本原因是PHP以Web服务器用户(如www-data或nginx)运行,该用户无权读取/etc/shadow等敏感文件,这是Linux权限模型的主动防护;需通过file_exists()和is_readable()分层排查,并检查open_basedir与SELinux策略。

PHP 无法读取受保护的系统文件,根本原因是它默认以 Web 服务器用户身份运行(如 www-data 或 nginx),而该用户天然不具备读取 /etc/shadow、/proc/kcore 等系统敏感路径的权限 —— 这不是 PHP 的 bug,是 Linux 权限模型的主动防护。
PHP 进程实际运行在哪个用户下
不看配置、不猜,直接用代码验证:
<?php echo posix_getpwuid(posix_geteuid())['name']; ?>
输出通常是 www-data(Apache/Debian)或 nginx(Nginx/CentOS),极少数情况是 apache。这个用户属于最小权限组,连 ls -l /etc/passwd 都能过,但 /etc/shadow 的权限是 -r-------- 1 root root,意味着只有 root 能读 —— www-data 连碰都碰不了。
- 不要试图用
sudo php script.php绕过:Web 服务器不会以 sudo 启动,这么做反而暴露严重安全风险 - 不要改系统文件权限(如
chmod 644 /etc/shadow):直接破坏系统安全性,多数发行版会拒绝或触发 SELinux 报警 - 检查方式不止
posix_geteuid(),也可在终端执行ps aux | grep php-fpm看 USER 列
open_basedir 和 SELinux 是双重拦截器
即使你把目标文件软链接到 Web 目录下,也常被拦住 —— 因为还有两层策略在生效:
立即学习“PHP免费学习笔记(深入)”;
-
open_basedir是 PHP 自己的沙箱:如果 php.ini 里设了open_basedir = /var/www/html:/tmp,那任何超出这两个路径的file_get_contents('/etc/hosts')都会直接返回false,且错误日志里只报“failed to open stream”,不提权限 - SELinux(尤其 CentOS/RHEL)会静默拒绝:即使文件权限是
644、open_basedir放行,只要上下文类型不对(比如文件是etc_t,而 Web 进程只能访问httpd_sys_content_t),操作就失败,getlasterror()拿不到有用信息,得查ausearch -m avc -ts recent - 临时验证是否 SELinux 导致:执行
setenforce 0(仅测试!),再试读取;若成功,说明就是它 —— 正确解法是用semanage fcontext加规则,而非永久关 SELinux
fopen() 失败时怎么快速定位是权限还是别的问题
别只看 if (!$fp) die('fail');,要分层排查:
- 先确认文件存在:
file_exists('/path/to/file')—— 返回false就不用往下查权限了 - 再查可读性:
is_readable('/path/to/file')—— 它内部已综合判断了权限、open_basedir、SELinux 状态,比手动ls -l更贴近 PHP 实际行为 - 如果
is_readable()返回false但file_exists()为true,基本锁定是权限/策略拦截;此时立刻检查open_basedir输出(ini_get('open_basedir'))和 SELinux 状态(getenforce) - 注意:
fopen('/proc/cpuinfo', 'r')在多数系统上能成功,因为该文件对所有用户可读;但/proc/1/environ就不行 —— 进程环境变量默认仅属主可见
真有需求读系统文件时的合规做法
绕过权限不是办法,替代方案才可靠:
- 用
shell_exec('cat /etc/os-release 2>/dev/null')前,确认disable_functions没禁掉shell_exec,且 Web 用户有对应命令的执行权限(通常有) - 把需要的数据提前导出成 Web 可读文件:比如用 cron 每小时跑一次
df -h > /var/www/html/_disk_usage.json,PHP 只读这个 JSON - 写个轻量级本地 API(如用 Python Flask 监听
127.0.0.1:8000),由 root 启动,PHP 用file_get_contents('http://127.0.0.1:8000/disk')获取 —— 权限隔离清晰,无提权风险 - 绝对不要用
system('sudo cat /etc/shadow'):哪怕配了 NOPASSWD,也是巨大攻击面,一旦 PHP 被注入,等于送 root 权限给攻击者
最易被忽略的一点:很多开发者查完 ls -l 就以为权限 OK,却忘了目录的 x 权限才是路径遍历的关键 —— /etc 目录若没有 x(即 drw-r--r--),www-data 连 opendir('/etc') 都失败,更别说读里面文件。这点在容器或精简系统中特别常见。



















