
刚做完一次授权范围内的内网安全测试我在目标环境里遇到一个很有意思的小工具——专门针对Linux PAM认证链路中的环境变量注入问题做探测验证。这类工具本身不算复杂但它背后牵扯出来的一大串东西非常值得展开聊聊PAM模块栈的执行顺序、pam_env模块对用户可控文件的信任、环境变量在会话建立前那段过渡态里的传递路径以及下游特权进程为什么会对注入进来的变量毫无防备。这篇文章不打算只讲某一个漏洞的利用细节而是把这个漏洞类的全景拆开给你看它到底卡在哪儿、测试工具通常由哪些功能模块组成、蓝队怎么用日志和审计规则发现这类行为、最后怎么把口子堵上。适合系统运维、安全工程师、做应急响应和攻防演练的人阅读如果你还不太熟悉PAM建议先翻一下/etc/pam.d/下面的配置文件明白模块栈的基本概念再往下读会顺畅很多。1. PAM机制全貌环境变量这一环卡在哪个位置1.1 PAM模块栈与会话建立流程PAM把整个认证过程拆成四类模块auth负责验证身份account负责检查账户状态是否过期、是否锁定password负责修改密码session负责会话建立和销毁。四类模块按顺序堆叠在/etc/pam.d/目录下每个服务对应的配置文件中一行就是一个模块加一个控制标记。SSH登录走sshd栈本机登录走login栈sudo、su各自的栈也互不相同。控制标记里required、requisite、sufficient、optional决定了某个模块失败后认证栈是直接中断还是继续往下走。排查这一块问题时我建议先把控制标记的含义记牢因为环境变量注入通常发生在session类模块执行的那一步——此时用户身份已经认证通过系统正处于“准备用户会话”的过渡态任何被错误信任的输入都会顺着这个过渡态一路流到用户shell里。pam_env这个模块就经常被放在session层。它的功能很简单读取配置文件把里面的KEYVALUE导出成环境变量注入当前会话后续要执行的进程环境。配置来源包括系统级/etc/environment、/etc/security/pam_env.conf以及老版本Linux-PAM里默认会读取的用户级文件~/.pam_environment。1.2 环境变量的投递链路从文件到进程环境我们把整条链路画出来看攻击面就一目了然用户通过SSH登录sshd调用该服务的PAM栈认证模块校验密码接着session模块执行pam_env读取配置文件变量写入当前进程环境最终fork出来的用户shell继承这些变量。整条链路的信任起点在“配置文件内容是否可信”这个地方一旦用户在认证前就能影响某个配置文件的读取结果那么这条投递链的源头就已经不可信了。值得注意的是攻击者不一定要等登录成功才触发读取。有些服务在认证过程中就会提前调用session模块或者直接读取用户环境文件这会导致一个问题从日志时间线上看文件被读取和会话建立这两个事件之间可能隔着很短的窗口。做应急响应时如果你只盯着“会话打开”的日志去看很容易漏掉早期阶段已经发生的环境注入行为。这也是我把观测点拆成“文件访问”和“环境变量输出”两条线的根本原因。1.3 敏感环境变量的真实杀伤力为什么不能只盯着LD_PRELOAD很多人一听到“环境变量注入”就条件反射想到LD_PRELOAD然后立刻说“setuid程序有AT_SECURE保护glibc会自动忽略危险变量”觉得这个攻击面不过如此。这话一半对一半错。LD_PRELOAD这类变量在setuid场景下确实会被过滤但攻击面从来不止一个变量PATH可以改变命令解析顺序PYTHONPATH可以劫持模块导入路径BASH_ENV可以在bash启动时自动执行指定脚本PERL5LIB、RUBYLIB这类解释器专用变量也都能污染下游进程的加载行为。真正危险的地方在于PAM这个节点恰好处于身份转换的临界位置。特权进程在认证完成后如果直接继承环境变量、没有做清理注入的变量就会跟着特权进程一起走。我实测过的一个场景是某服务以root权限直接拉起子进程并且把PAM会话里带进来的环境变量原样传给子进程最终实现了对指定库文件的加载控制。换句话说这个漏洞类的核心不在某个模块的某一个函数而在于“用户可写输入 模块未过滤 下游未消毒”三个条件叠加的结果。2. 漏洞根因拆解pam_env模块的几个病灶点2.1 用户可控的输入点有哪些先列一张配置文件的权限清单这决定了哪些输入点是用户可以动、哪些点是管理员独占的配置文件默认路径默认可控方风险说明/etc/environment系统级root可写全局环境变量篡改影响所有登录会话pam_env.conf/etc/security/pam_env.confroot可写全局环境变量规则支持变量展开~/.pam_environment用户家目录用户自己可控老版本PAM默认读取风险最高的输入点/etc/pam.d/service系统级root可写PAM栈被打上恶意模块或参数的来源~/.pam_environment是整个漏洞类里最经典的用户可控输入点。它放在用户家目录用户自己可以写内容而老版本Linux-PAM在session阶段默认就会去读它。很多系统管理员压根不知道这个文件的存在更不知道它会被PAM自动消费。早期社区针对这类问题做过一轮修复后续版本也对用户级环境文件的默认行为做了大幅收敛但存量系统上仍然能见到相关配置残留。2.2 变量展开机制的“善意”变成了攻击面pam_env.conf支持${VAR}形式的变量引用意思是拿已有变量去拼新变量的值。设计初衷是让管理员少写重复配置但这个特性在实际使用中极其容易被错误使用。问题在于“已有变量”有一部分来自用户登录时的初始环境或者来自更早模块setenv的值。一旦配置里出现类似SOME_PATH${HOME}/data这样的写法而HOME又可能受到用户侧影响展开结果就不再受管理员控制了。更微妙的是展开过程中模块还会对拼出来的路径做文件存在性检查之类的操作。如果展开式最终拼出了任意路径就存在把文件系统信息带出来、或者诱导模块去读取特定路径的风险。这里我不展开讲具体触发机制因为测试工具正是利用这些组合验证问题的但防守方必须记住一条审计红线配置里出现“引用了外部变量的展开式”就是重点审计对象。我在第五部分会给出一条相对安全的管理员配置方法核心就一句话永远不要在展开式里放用户可能影响的变量名。2.3 敏感变量过滤为何缺失老版本pam_env导出环境变量时对LD_PRELOAD、LD_LIBRARY_PATH这类危险变量确实缺少足够严格的过滤。攻击者在自己的会话里写入这些变量会话进程就原样继承。后来社区修复了模块自身的问题也收紧了用户级环境文件的默认策略但实际系统里的问题往往不在pam_env本身而在下游。最常见的三个下游坑位一个是sudo配置里的env_reset没有开启导致sudo拉起特权进程时保留了大量原始环境一个是某些服务进程自己用pam_env加载配置但后续启动子进程时没有清理环境还有一个是业务方自己写的自定义PAM模块直接基于这些变量做路径拼接、没有做校验。理解这一点非常重要它意味着单纯升级pam包并不能解决问题必须同时检查系统里每一个会读取PAM环境的特权进程。2.4 测试工具的功能结构探测、匹配、投递、验证我在测试中分析的这类工具功能结构大同小异大概可以拆成五个模块探测模块枚举/etc/pam.d/下所有服务的PAM栈筛选出session阶段调用pam_env的目标权限审计模块检查系统级配置文件与用户目录环境文件的写权限判断是否存在可控输入点策略匹配模块扫描pam_env.conf与目标服务配置里的变量展开式、危险变量名和内置的危险清单做比对投递与触发模块生成测试用例写入可控文件然后触发一次认证流程常见做法是本机SSH回环观察注入是否生效结果验证模块对比基线环境和注入后的进程环境变量列表输出差异并标记风险项。你注意到没有这套工具本质上不是一个全新的0day利用链而是把已知问题组合成一条自动化的验证流水线。防守方完全可以照着同样结构写自己的巡检脚本不需要依赖工具本身。我在第四部分会给出命令行级的检测示例你会发现原理是相通的。3. 在隔离环境里做验证与行为观测3.1 搭建一个干净、可复现的实验环境做这类验证我强烈建议用虚拟机加快照不要用容器。容器里的PAM栈往往被裁剪过很多模块压根没装环境变量的传递路径也会被容器runtime改写得到的结果不具有代表性。我的实验环境是这样准备的第一步新建虚拟机做最小化安装选一个常见的发行版就行Debian系和RHEL系各准备一个最好因为两者对~/.pam_environment的默认处理策略不同对比着测非常有价值。第二步安装所需软件包openssh-server、auditd、strace、ltrace。第三步确保系统自带PAM基础组件完整确认pam_env.so存在。第四步配置SSH允许本机回环登录方便后面用ssh localhost触发认证流程。最后对干净系统拍一张快照。验证开始前先做一轮基线观察命令很简单# 查看当前PAM栈里session阶段加载了哪些模块 grep -E session|pam_env /etc/pam.d/sshd # 检查是否存在用户级环境文件 find /home /root -name .pam_environment -ls 2/dev/null # 记录当前登录会话的环境变量基线 export -p /tmp/env_baseline.txt这轮基线记录非常重要。没有它后面你根本无法区分哪些变量是注入进来的、哪些变量本来就是系统正常设置的。3.2 观测指标文件访问、进程行为、环境变量差异我在验证过程中主要盯三个方面的数据第一类是配置文件访问行为。用auditd监控敏感路径的读写规则如下auditctl -w /etc/pam.d/ -p wa -k pam_config auditctl -w /etc/environment -p wa -k pam_env_cfg auditctl -w /etc/security/pam_env.conf -p wa -k pam_env_cfg后续用ausearch -k pam_env_cfg -ts today查看结果。重点看哪些进程在什么时间点读写了这些文件以及是否有非授权进程在登录事件之外触碰它们。第二类是认证全过程的系统调用。用strace跟踪SSH认证过程中打开的文件路径确认PAM模块实际读取了哪些配置文件strace -f -e traceopenat,read,write -o /tmp/sshd_pam_trace.log ssh localhost -l testuser然后从跟踪日志里过滤PAM相关模块的行为。这么做可以直接看到模块“实际读的文件”和“配置里声明的文件”之间是否对得上很多隐蔽问题就是这么暴露的。第三类是环境变量输出。登录成功后执行export -p和基线文件做diffexport -p /tmp/env_after_test.txt diff /tmp/env_baseline.txt /tmp/env_after_test.txtdiff结果里出现的异常变量名尤其是LD_、PYTHONPATH、PATH相关的就是需要重点记录的风险信号。3.3 验证结果怎么判定一张风险分类表把观测数据汇总后我习惯用下面这张判定表给系统归类风险等级判定条件说明高用户可写环境文件存在且PAM栈启用pam_env读取用户文件且配置含变量展开或过滤缺失攻击路径完整工具可直接验证成功中系统级配置存在写权限问题或配置里出现变量展开但未启用用户级输入需要配合其他漏洞或社工才能被利用低使用pam_env但禁用了用户级文件且无变量展开可作为加固基线参考但仍有审计价值信息系统完全不使用pam_env风险面很小但其他PAM模块仍可能引入环境变量判定过程的要点是把“配置条件”和“行为证据”对起来看不能只看配置就说危险也不能因为没观察到异常就认定安全。我在一次实测中遇到过配置和日志都匹配“高风险”条件、但实际注入没生效的情况原因是该发行版的新版本pam_env.so默认已经不读取用户级环境文件了。配置旧、二进制新这种错位非常常见所以务必以行为观测为准。4. 检测方法与排查技巧实录4.1 主动巡检“一把梭”命令清单日常巡检不需要像实验那么复杂一条Shell脚本就能完成第一轮筛选。我自己用的巡检脚本简化后长这样#!/bin/bash # 检查PAM配置文件写权限 echo PAM config write perms find /etc/pam.d -type f -perm /022 -exec ls -l {} \; 2/dev/null ls -l /etc/environment /etc/security/pam_env.conf 2/dev/null # 检查用户级环境文件 echo user .pam_environment files find /home /root -name .pam_environment -ls 2/dev/null # 扫描配置里的危险变量与展开式 echo dangerous vars in pam_env config grep -nE LD_PRELOAD|LD_LIBRARY_PATH|PATH|PYTHONPATH|BASH_ENV|\$\{ \ /etc/security/pam_env.conf 2/dev/null # sudo环境清理策略 echo sudo env_reset grep -riE env_reset|env_keep /etc/sudoers /etc/sudoers.d/ 2/dev/null这套脚本输出项不多但每一行都对应一个明确的攻击路径条件。find /etc/pam.d -perm /022找出组写或全局可写的PAM配置这类文件一旦被篡改攻击者可以直接往认证栈里加模块影响面比环境变量注入大得多。grep \${找展开式就是前面说的那个审计红线。4.2 日志关联journald与auditd时间线拼接PAM模块本身打得日志非常克制pam_env成功执行时默认不产生明显日志所以应急响应时主要靠外部审计手段。我习惯的做法是把journald和auditd的事件放在同一条时间线上看journalctl -u sshd --since today | grep -iE pam_env|session opened|session closed ausearch -k pam_config -ts today -i应急场景里的标准动作是先锁定异常登录事件的时间点然后以该时间点为圆心前后各取一小时查PAM配置文件有没有被读写、用户环境文件有没有被创建或修改、有没有非预期进程执行了execve。时间线拼接看起来笨但在没有EDR、不做流量侧分析的内网环境里这条路往往是最快定位问题的。4.3 误报场景与分辨技巧巡检和审计都会产生大量告警其中不少是误报。我踩过的坑列几个典型的告警内容误报原因分辨技巧/etc/environment里有PATH系统默认配置几乎所有发行版都有对比发行版官方默认值用户.bashrc里设置了LD_PRELOAD用户自己的会话内变量不影响特权进程确认下游进程身份和权限auditd记录大量/etc/pam.d/读取正常登录过程中login、sshd都会读比对写入方和读取方看时间是否对应登录事件pam_env.conf里出现${VAR}管理员有意为之且变量来自系统级固定输入${VAR}的赋值源头是否可被用户影响这这里我最想强调的是第三类误报。/etc/pam.d/下的文件被读取是非常正常的因为每次登录都要加载PAM栈。真正需要盯的是“写入”和“修改”以及读取行为发生在非登录时段的异常情况。告警规则的过滤条件宁准勿宽不然每天金字塔式的误报堆几天就会麻木。5. 加固与修复把口子堵上再说5.1 pam_env配置安全基线加固的第一步是让配置文件本身站得住脚。我给出的安全基线很简单/etc/environment权限必须为root:root 0644检查命令stat -c %U:%G %a /etc/environment/etc/security/pam_env.conf一旦配置里面禁止出现引用用户可控变量的展开式如果确实需要定义全局环境变量只允许静态绝对路径。pam_env.conf的安全写法示例# 安全静态绝对路径赋值 MY_APP_HOME DEFAULT/opt/myapp # 不安全不能出现这种写法 MY_APP_DATA DEFAULT${HOME}/data在/etc/pam.d/里的session配置建议显式指定读取路径session required pam_env.so readenv1 envfile/etc/environmentreadenv1明确启用系统级环境文件envfile参数把读取范围限定到指定文件避免模块再去碰其他位置的配置文件。这一步做完PAM这一层的基本面就干净了。5.2 禁用用户级环境文件不同发行版要不同对待针对~/.pam_environment的处理不同发行版策略差异很大。老版本Linux-PAM默认会读它新版则收紧甚至直接忽略Debian系和RHEL系的默认行为也有区别。所以入户加固之前先确认当前系统二进制版本的实际行为不然你会在配置上白费功夫。实际加固动作按优先级排列第一步确认pam_env.so是否支持控制用户环境文件的参数若支持在调用处显式禁用第二步从系统层面禁止创建用户级环境文件强度最高的做法是通过SELinux/AppArmor策略阻止普通用户域对~/.pam_environment的写入第三步对sudo场景强制环境清理在/etc/sudoers中添加Defaults env_reset并显式Defaults env_delete LD_PRELOAD LD_LIBRARY_PATH PYTHONPATH BASH_ENV第四步像排查僵尸配置一样把存量系统里已经存在的.pam_environment文件全部清掉并记录台账。5.3 纵深防护别把宝押在单一机制上环境变量注入这个漏洞类的本质是信任链问题所以加固思路也是多层信任切割。除了配置层面的操作我还会做四件事第一确认setuid程序的glibc保护开启但明确它是兜底而非主力因为下游特权进程如果不在setuid标记下启动AT_SECURE保护根本不会触发。第二用SELinux或AppArmor限制进程对PAM配置目录的访问默认策略之外单独加一条只读规则。第三将关键静态目录做只读挂载或引入文件完整性监控对/etc/pam.d/、/etc/security/、/etc/environment的每次变化留痕。第四在业务侧排查所有“以root启动、又经过PAM认证链路、且没有清理环境变量”的服务进程这类进程才是环境变量注入的最终落点。6. 实操心得与踩坑记录最后分享几个我在做这个课题时踩过的坑希望对你有参考价值。第一个坑是拿容器做实验。早期图省事直接在容器里验证结果容器里的PAM栈被裁剪得七零八落pam_env.so的行为和真实虚拟机完全两样浪费了整整一天。后来一切验证都回到虚拟机加快照结论才可信。第二个坑是auditd规则过宽。我一度对/home整个目录加写监控结果登录高峰期日志量直接爆炸重要告警被淹没。最后收敛成只精确监控用户家目录里特定文件名的变更才恢复正常。审计规则宁精勿广这道理在别的安全场景也通用。第三个坑是对AT_SECURE保护的过度信任。前面说过我原以为所有特权进程都会自动忽略危险环境变量直到在一个以root直接拉子进程的服务上复现注入成功才反应过来glibc的保护只针对setuid挂起场景服务进程自己拉了子进程又不清理环境保护机制完全绕过了。从此我记住了一条缓解措施必须在每个具体程序上逐个验证不能靠推演。第四个体会来自对比测试。同一套验证流程在Debian系和RHEL系的老版本上结果截然不同关键差异就在对用户级环境文件的默认处理策略。做这类课题研究时建议把两个体系的系统都准备一遍对比着测你对“到底是模块漏洞还是配置问题”的判断会准确很多。最后再分享一个小技巧验证结束后不要只回滚快照先把过程中留下的测试文件和测试账号清掉再导出一次完整的巡检记录存档。这个习惯帮我避免过好几次“测试留下的后门被当成真实漏洞”的乌龙也让你在写报告时有完整的数据支撑。环境变量注入这个课题的内容量不小但只要你顺着“PAM链路怎么走、变量从哪来、特权进程信什么”这条主线去查整个攻击面和防御面都会非常清晰。