加入收藏 | 设为首页 | 会员中心 | 我要投稿 草根网 (https://www.0591zz.com/)- 运维、云管理、管理运维、图像技术、AI硬件!
当前位置: 首页 > 建站 > 正文

PHP安全审计:90%高危漏洞藏于配置末行

发布时间:2026-11-03 11:26:00 所属栏目:建站 来源:DaWei
导读:  “PHP安全审计:90%高危漏洞藏于配置末行”——去年二月份,告警灯闪了一夜,我盯着跳板机上三个终端,左屏tail -f /var/log/apache2/error.log,中屏watch -n1 'ps aux | grep php-fpm | wc -l',右屏开着vim在改php.ini,手

  “PHP安全审计:90%高危漏洞藏于配置末行”——去年二月份,告警灯闪了一夜,我盯着跳板机上三个终端,左屏tail -f /var/log/apache2/error.log,中屏watch -n1 'ps aux | grep php-fpm | wc -l',右屏开着vim在改php.ini,手边泡面汤已经凉透,谁搁那儿嗦得山响,是老张。他刚把最后一口面吸进去,我顺手敲了:q!,结果发现光标停在第892行,后面还粘着个没删干净的,后面没跟,就一换行符孤零零吊着。


  我原以为是.htaccess搞鬼,毕竟那玩意儿谁都能扔进目录里——结果翻到/usr/local/etc/php/7.4/conf.d/50-custom.ini末尾,三行空行,第四行写着“;disable_functions = pcntl_exec,system,shell_exec”,第五行空白,第六行是“extension=openssl.so”,第七行又空着,第八行突然冒出来“auto_prepend_file=/tmp/monitor.php”。不是注释。没分号。没引号漏掉。就是它。那个/tmp/monitor.php是我半年前配监控用的,后来换了方案,文件早删了,但这行没删。PHP启动不报错,加载失败静默吞掉,直到某个请求触发auto_prepend失败时才吐出Warning,而那个Warning被error_log塞进syslog后,被我们的日志采集中间件判定为“非关键”给滤掉了。


  椅子轮子卡着一根网线,我踢了一脚,没动。冷风机一直在响,不是嗡——是嗡…嗡…嗡…每七秒顿一下,像服务器在咳嗽。我们盯的是load 30+、swap用了82%、inode用爆、磁盘IO wait飙到95%,但所有指标都在指向“资源不够”,没人去翻php-fpm的slowlog,更没人敢动prod环境的ini——因为“上次改完重启,下游调用方说他们收到502了,说是咱们回源超时”。其实哪是回源超时,是他们硬编码了超时时间,我们慢1ms他们就炸,但他们不修自己的timeout,只骂我们证书续签太晚——可证书根本没到期,是解析缓存没刷,是dnssec验证路径里有个老CA中间证书被上游云厂商偷偷下线了,他们压根没看证书链,光瞅Subject就点确认。这事后来拖了三天,最后是实习生在值班室角落里翻云厂商工单历史,找到一行“为提升兼容性,已移除DST Root CA X3”才对上号。可那会儿我已经在删第6版php.ini备份了,备份名分别是backup_20231212_bak、backup_20231212_bak_v2、backup_20231212_bak_final——全他妈在/home/deploy/.bash_history里留着echo命令,连空格都没对齐。


  优胜劣汰——这话不是吹的。你想想,所有能活过三年的PHP项目,配置文件都被改过至少十七八次,每次上线都加两行、删一行、注释三行。人走了,注释不走;架构升级了,旧扩展配置还在;安全加固扫了一遍,补丁打上了,可那个“expose_php=Off”底下还蹲着一行“session.cookie_httponly=On ; 这个必须开,王经理说的”,王经理半年前调去管采购了。所以不是配置写错了,是配置“没死透”。僵尸进程是活着但不动的进程,这些末行配置是死了但还在生效的文本。它们比代码更难审计,因为IDE不报错,PHP不会拦,WAF不认识,扫描器当注释跳过。唯一拦它的是——你某天半夜翻配置,恰好手指悬停在第1023行,恰好那天你刚被老板指着鼻子问“为什么用户数据能从/var/www/html/public/export.php直接下载”,而export.php里只有一句file_get_contents($_GET['f']),没校验路径,没白名单,没open_basedir限制。哦对,open_basedir是关的。为什么?因为第999行写着“;open_basedir = /var/www/html:/tmp”,第1000行是空的,第1001行是“;open_basedir = /var/www/html:/tmp:/proc/self/fd”,第1002行又是空的,第1003行写着“open_basedir =”,没了。就一个等号。空值。PHP默认当作空字符串,等于放开全部。没人发现,因为没人检查“=”后面有没有东西。只有那次故障复盘,老李拿鼠标框选复制这行发到群里,放大字体,截图裁成正方形,标红“=”,配字:“这里没值,不是false,是null,PHP当真null,相当于unset。”然后撤回了。三秒后他又发一遍,这次写了“撤回错了,不是撤回,是删。”


  跳板机密码又过期了。我输三次,弹窗提示“Your password has expired”,只好切窗口切到邮箱找重置链接,等邮件进来的时候,看见小陈趴在桌上睡着了,耳机线垂下来,在桌角缠着外卖袋子上的水汽——那袋子还是昨天晚上的,纸板软塌塌地吸着桌面,边缘微微卷起。我点开邮箱附件,是运维组发的《PHP基线配置检查清单V3.7》,最后一页写着“重点检查配置文件末尾是否存在未注释的遗留参数、空行后的有效指令及孤立的open_tag”。我看了眼自己正在编辑的那个php.ini,光标还在第1003行那个“open_basedir =”上,左边是空格,右边是光标,中间什么都没有。我按了Backspace,删掉等号,敲了个#,再敲个空格,变成“# open_basedir =”。保存。退出。没reload。就只是改了。反正明天早上还有人来改——是老张,他说他今天白班,要改数据库连接池配置,顺便帮我看看那个monitor.php到底该不该留。我说别看了,留着吧,反正/tmp会被清,它活不过午夜。可他说“那就留个空文件占位啊,总比写个无效路径强”。我没回。我盯着屏幕右下角时间:03:17。冷风机又顿了一下。嗡……


文章配图,仅供参考

  开发同事?哦,老张刚才嗦完面就去改CI脚本了,说要把php.ini校验加进pre-commit钩子——结果跑了十分钟,报错说“找不到git hooks目录”,原来他本地git init都忘写了,init在另一个空文件夹里。小陈醒了,揉眼睛说她梦见自己在往php.ini里填正则,写的pattern是/^[a-zA-Z0-9._-]+$/,但测试字符串是../../../../etc/passwd,然后PHP真给放过去了。“因为”她说,“dot和dash是合法字符啊”。我没纠正她。我关掉了中间那个终端。它还在watch着php-fpm进程数,但数字早就掉回4了。没用。真正卡住的是第892行那串没闭合的,是第1003行那个等号,是第999行被分号注释掉的open_basedir,是那一行行没死干净、却还挂着process的文本残骸。它们不消耗CPU,不占内存,不发HTTP请求,它们只消耗你的判断力。你越信监控,越容易放过它们。你越信人,越容易信“这行我上周刚看过”——结果上周你看的是第891行。


  现在我把那个php.ini传到线上,准备跑一次find . -name ".ini" -exec grep -l "open_basedir.=" {} \\; ,但我知道,grep不抓空值。它只抓“=xxx”。我要写个sed -n '/open_basedir[[:space:]]=[^[:space:]]/p' 才行。或者,干脆让老张改CI脚本的时候,顺便加个check-empty-equals。不过——他今天白班,可能赶不上。先睡吧。椅子轮子还在卡那根网线,我懒得动。

(编辑:草根网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!