ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Shell脚本实现Linux安全检测:密码强度与未授权访问排查

用Shell脚本实现Linux安全检测:密码强度与未授权访问排查 1. 项目概述与安全检测思路Shell是Linux系统管理的“万能钥匙”但很多人只拿它来做日常的文件操作、服务启停其实它还有个被轻视的能力——充当轻量级的安全体检工具。我之前给一批线上CentOS服务器做过一轮安全自查全部用纯Shell脚本完成没有引入任何重量级扫描器核心就两个方向一是账户密码强度是否达标二是是否存在未授权访问的入口。这套东西跑完以后我直接把发现的问题整理成了一个文档效果出奇地好很多同事看了以后才意识到自己机器的安全基线压根没过关。这个第53章的核心定位不是教你写一个花哨的扫描器而是围绕“系统安全漏洞”这个目标用Shell把最容易出问题的两个面——账户密码强度和未授权访问检测——系统性地过一遍。它能解决的问题很具体哪些账号是空密码或弱密码哪些账号快过期了还在用哪些SSH配置项在引狼入室哪些SUID文件给了普通用户不该有的权限哪些端口暴露在不该暴露的地方。适合谁看呢主要是有一定Linux基础、想在不引入商业工具的前提下给系统做快速安全体检的运维人员也包括安全工程师做基线核查前想先用Shell粗筛一遍的场景。说个背景很多中小团队没有专职安全岗系统部署完以后就那么跑着密码策略三五年不换SSH配置用默认值SUID文件有多少个没人说得清。真等出事再排查往往已经晚了一步。所以这套Shell安全检测方案的价值在于“低成本、可复现、能落地”——每台机器都可以跑结果可以直接人工复核不需要装agent也不需要编译第三方组件。在动手之前我需要把概念讲清楚。账户密码强度不是一个单纯的长度问题它包含了几层意思密码是否有最低长度和复杂度要求、是否存在空密码或共享默认密码、密码使用周期是否过长、root之外是否还有特权账户在裸奔。未授权访问检测则更宽泛凡是让无凭证或低权限用户能触及不该访问的资源的行为都算这一类。比如SSH开了root直接登录且密码简单等于把大门钥匙挂在了门框上再比如某个SUID文件被篡改过普通用户跑一下就能读取shadow文件这种事Shell完全能查。我的核心设计原则是每个检测项都要有输出有结论有建议。不能光报“有问题”得告诉你看完结果后下一步做什么。脚本做到后面会越来越像一个简化版的安全基线检查器这正是Shell的魅力所在——你能一步步把专业工具里的重点逻辑用几百行脚本复现出来而且逻辑完全透明没有任何黑盒。2. 账户密码强度检测的设计与实现账户密码这块是安全检测的第一道关卡。Linux账户体系里口令数据在/etc/shadow所有密码策略相关参数几乎都围绕这个文件展开。但直接读shadow文件需要root权限普通用户没法操作所以在写脚本时第一件事就是检测当前运行状态如果不是root直接给出提示并结束这个前置判断非常关键能避免后面一堆权限类报错。2.1 空密码账户与弱密码识别空密码账户是一个几乎稳被拉满的安全漏洞。怎么用Shell检测呢/etc/shadow文件的每一行格式是这样用户名:密码哈希:.....多个字段用冒号分隔。密码哈希字段如果是空或者是一些特殊的占位符比如!、*、!!说明该账户没有可用密码或处于锁定状态。空密码账户会直接让所有持该用户名的人免密登入这个危害级别是需要立即修复的。我写脚本时用了一条简洁的awk命令来筛查awk -F: ($2 || $2 ! || $2 * || $2 !! $3 ! 0) {print $1} /etc/shadow这段逻辑有个细节容易被忽略——$3 ! 0这个条件。$3是密码最近修改时间距1970年1月1日天数root账户在绝大多数系统里该值是0如果直接过滤掉第二字段为特殊字符的行会把root误报成空密码。我当初第一次跑脚本就踩过这个坑系统提示root密码安全性为0一看shadow文件果然root的密码哈希位置是!!但这并不代表root没有密码而是某些系统加固脚本把root的哈希标记为锁定状态。所以判断空密码的真实条件是“第二字段为空”并且要排除UID为0的系统特权账户否则误报率会非常高。弱密码检测没有那么直接。系统本身不会把明文密码存在任何地方所以在纯Shell层面我们真正能做的是检测“密码策略是否允许弱密码存在”而不是直接判断某个密码是否太短。两个核心配置项PASS_MAX_DAYS是密码最长使用天数PASS_MIN_LEN是最小长度要求。如果PASS_MIN_DAYS是0意味着用户可以立刻修改密码等于密码没有最低使用期限换密码的实际意义就大打折扣。我用这样的方式读取配置grep -E PASS_MAX_DAYS|PASS_MIN_DAYS|PASS_MIN_LEN /etc/login.defs然后针对结果做出分级判断90天以上强制更换属于合格基线不设限制或者最小长度低于8位的直接标记为高风险。很多新手会直接改/etc/login.defs里的参数但我要提醒一个坑这个文件里的策略只对新用户和密码修改操作生效对存量账户的有效性要通过chage命令去具体调整两者配套使用才能覆盖全部账户。我在检测脚本里还会加一个循环挨个检查所有非系统账户的密码过期信息for user in $(awk -F: $3 1000 || $3 0 {print $1} /etc/passwd); do expiry_info$(chage -l $user 2/dev/null | grep 密码过期 ) echo [账户] $user - $expiry_info done2.2 密码复杂度的静态分析与判定标准有人说要检测密码复杂度但Linux同时存在两套密码机制旧式/etc/login.defs中的PASS_MIN_LEN以及基于pam_pwquality模块的动态策略。前者在很多现代发行版中已经不再是唯一权威后者才是实际强制检查复杂度的模块。在Shell层面我们能做的静态检测包括/etc/pam.d/system-auth或/etc/pam.d/common-password中是否启用了pam_pwquality.so模块pam_pwquality相关参数中的minlen、ucredit、lcredit、dcredit、ocredit这些大小写字母、数字、特殊字符的强制要求是否启用了pam_unix.so的remember参数来控制历史密码重用次数我写了一段检测pam配置的逻辑if grep -rE pam_pwquality\.so /etc/pam.d/ /dev/null 21; then echo [检测] pam_pwquality 模块已启用 grep -rhE pam_pwquality\.so /etc/pam.d/ | grep -oE minlen[0-9]|ucredit-?[0-9]|dcredit-?[0-9]|lcredit-?[0-9]|ocredit-?[0-9] else echo [警告] 未启用 pam_pwquality 模块复杂度策略可能缺失 fi注意ucredit-1这类负值的意思它表示至少要包含1个大写字母如果是正值则只是“鼓励包含”不强制。很多配置里写的是ucredit1看着差不多实际作用天差地远。检测结果出来后我会根据参数情况打一个综合分90-100分复杂度策略完整长度不低于12位各类字符都有强制要求60-89分基本可用但某些维度放宽了要求0-59分基本等于裸奔密码大概率是弱口令这个评分逻辑是我自己调出来的不一定严格对标等保标准但至少能让使用者对当前状态有个直观理解。检测结果从来不应该是“非黑即白”给一个可量化的参考值反而更容易推动后续整改。2.3 特权账户与用户分组风险排查密码强度检测如果只停留在策略层面就漏掉了最重要的一块特权账户管理。UID为0的账户拥有系统最高权限这个集合里通常只有root一个但如果某个系统中多了几个UID0的账号那等于系统里有几个“隐形管理员”任何一个密码泄露都会导致整个系统沦陷。Shell检测精细之处在于能直接对比passwd和shadow两个文件找出只存在于其中一个文件的“幽灵账户”。具体思路是先把两个文件的用户名列表取出来再用comm命令做交集和差集awk -F: {print $1} /etc/passwd | sort -u /tmp/passwd_users.txt awk -F: {print $1} /etc/shadow | sort -u /tmp/shadow_users.txt echo 仅在 passwd 中出现的用户 comm -23 /tmp/passwd_users.txt /tmp/shadow_users.txt echo 仅在 shadow 中出现的用户 comm -13 /tmp/passwd_users.txt /tmp/shadow_users.txt这种查法的原理是正常情况下每个可以利用密码登录的系统账户都应该同时在passwd和shadow中存在。如果passwd有但shadow没有通常意味着该账户无法正常使用密码认证可能是某种伪账户如果shadow有但passwd没有则可能是清理用户时残留的数据。两种情况都应该人工确认一遍。用户分组方面重点看/etc/group里哪些成员被加入了wheel组CentOS/RHEL或sudo组Debian/Ubuntu。这两个组默认拥有sudo执行权限组内每个成员等于都是准管理员。我一般会跑这样一段group_listwheel sudo for grp in $group_list; do members$(grep ^${grp}: /etc/group | cut -d: -f4) if [ -z $members ]; then echo [信息] $grp 组暂无额外成员 else echo [检查] $grp 组成员: $members fi done审核组里成员是否都是“该在的人”是权限治理的基本功。很多时候系统被搞乱就是从多了个不该在sudo组里的人开始的。3. 未授权访问检测的检测策略未授权访问是个大类我挑几个既高频又容易被忽略的点来展开。很多检测逻辑看起来简单但真正组合起来跑一遍能发现的惊喜通常叫惊吓不在少数。3.1 SSH服务配置与密钥审查SSH是服务器最常暴露的服务端口水也最深。我写检测脚本时第一步是确认SSH配置文件位置因为不同发行版路径有差异但常见的都是/etc/ssh/sshd_config。这个文件里有几个参数直接决定系统对外的安全姿态PermitRootLogin是否允许root直接SSH登录yes是极度危险的配置建议改成no或prohibit-passwordPasswordAuthentication是否允许密码认证如果你有密钥登录机制建议直接关掉密码登录Protocol协议版本老旧的v1协议因为算法强度问题已经不再被现代OpenSSH支持但如果看到了相关配置可以直接标记为严重漏洞AllowUsers和DenyUsers白名单和黑名单配置如果为空则等于所有人都能尝试登录MaxAuthTries单次连接的最大认证尝试次数默认是6建议改小到3以内暴力破解的尝试空间会显著缩小我做了一个自动提取并转换为可读结果的函数sshd_config/etc/ssh/sshd_config if [ -f $sshd_config ]; then echo SSH 关键安全配置 grep -E ^(PermitRootLogin|PasswordAuthentication|Protocol|MaxAuthTries|AllowUsers|DenyUsers) $sshd_config || echo [警告] 以上关键配置项缺失SSH可能使用默认值运行 fi这里虽然用了grep提取配置但要注意grep在Shell脚本中的常见用法有一个大坑grep -E和egrep不能混用另外如果配置文件里同一项出现了多行比如被条件Include外部配置那么只查主配置会漏掉实际生效值。更严谨的做法是额外跑一下sshd -T 2/dev/null来输出实际生效的配置但考虑到sshd -T在某些老系统上不兼容脚本里要做个双重判断如果-T执行成功就以其输出为准。密钥审查是另一个容易忽视的未授权访问源。~/.ssh/authorized_keys文件定义了哪些公钥可以免密登录如果这个文件权限不对比如其他用户可读、可写那么你设再复杂的密码也没用黑客直接往里面塞一把自己的公钥就能登进来。检测逻辑for home_dir in /home/*; do user$(basename $home_dir) auth_keys$home_dir/.ssh/authorized_keys if [ -f $auth_keys ]; then perms$(stat -c %a %U $auth_keys) echo $user 的 authorized_keys 权限与属主: $perms if [ $(stat -c %U $auth_keys) ! $user ] [ $(stat -c %U $auth_keys) ! root ]; then echo [高危] $user 的 authorized_keys 属主异常 fi fi done3.2 SUID与SGID文件扫描SUID是Linux里一个冷门但高风险的概念。简单解释当一个可执行文件设置了SUID位任何用户运行它时该进程的有效用户ID会临时变成文件属主通常是root。这种机制本身是系统某些命令正常工作所必需的比如/usr/bin/passwd需要临时提升权限来修改shadow文件但如果有任何多余的普通文件也被赋予了SUID位那就等于给普通用户发了一张临时root卡。我用find命令做全盘扫描这在Shell脚本中的使用频率非常高。核心命令find / -perm -4000 -type f 2/dev/null echo ---SGID--- find / -perm -2000 -type f 2/dev/null-perm -4000是包含SUID位注意是“包含”而不是“恰好等于”因为文件可能同时还有其他权限位。不加2/dev/null的话find在扫描/proc等虚拟目录时会输出大量权限拒绝提示这倒不影响结果但会淹没正常输出属于Shell脚本中的常见坑之一。扫描结果出来后拿它跟系统默认SUID清单做对比。每台机器默认的SUID文件通常就那么二三十个如果发现冒出了一个奇怪路径下的SUID文件基本可以断定有问题。同样重要的还有SGID目录。目录设置了SGID位之后新创建的文件会自动继承目录所属组这在共享协作目录里是有意为之但如果没有配合正确的组权限管理也可能变成横向渗透的工具。所以我扫描后会特意列出非标准路径下的SGID项单独提醒。3.3 系统端口监听与开放状态检查未授权访问不一定走SSH其他服务如果监听了不安全的端口又是另一条攻击面。在Shell层面我用ss或netstat来做监听状态收集以ss为主因为它在现代系统中更高效而且很多发行版已经不再自带netstat了。ss -tulnp | awk NR1 {print $5, $6}这个命令输出的信息包括本地监听地址、端口、进程名和PID。检测逻辑里我重点标记三类情况监听地址是0.0.0.0或::的端口意味着对所有网卡开放外部网络可达常见管理端口22、3306、6379等直接暴露没有限制来源IP出现未知的高位端口但又对应着标准系统服务的端口检查的价值在于它能直接暴露“我的机器比我想象中多开了好多口子”。我有一次跑完这个命令发现某台测试机竟然监听了一个FTP端口21一问才知道是半年前搭了个临时文件服务忘了关。这种安全问题不通过扫描根本不会被人注意到。4. 完整检测脚本的集成与运行前几章都在讲单个检测逻辑这一章把它们揉成一个可以直接跑的完整Shell脚本。动手之前有个设计问题要想清楚是追求全面还是追求轻快我的选择是兼顾但默认参数偏向快速巡检只有在加了--full参数时才做全盘扫描。4.1 整体脚本结构与分段设计脚本结构大体分四段环境检测段确认操作系统类型、当前用户权限、检查所需命令是否存在账户密码检测段空密码、策略配置、特权账户、过期账户访问控制检测段SSH配置、authorized_keys、SUID/SGID、端口监听报告输出段把结果分类整理输出为带时间戳的文本报告环境检测段是一个经常被人遗忘的模块。脚本要兼容不同发行版早期我在Debian系的机器上跑CentOS脚本单是配置文件路径不同就够喝一壶。所以第一步先做这样的判断if [ -f /etc/redhat-release ]; then distroRHEL/CentOS elif [ -f /etc/debian_version ]; then distroDebian/Ubuntu elif [ -f /etc/alpine-release ]; then distroAlpine else distroUnknown fi echo [信息] 当前系统类型: $distro路径差异方面Debian系的/etc/pam.d/common-password对应CentOS的/etc/pam.d/system-authlogin.defs都叫这个名字但内容基本一致SSH相关的groups在CentOS有wheel在Ubuntu是sudo。适配完这些差异脚本才真正具备多平台可用性。4.2 核心函数的实现与输出示例把重复逻辑封装成函数是Shell脚本工程化的基本功。我定义了一个风险评估函数和一个输出格式化函数。风险评估函数接收一个数值型分数和一个风险等级输出时带上颜色如果终端支持的话。在纯文本环境下则用[高危]、[中危]、[低危]这些醒目前缀代替。一个典型的检测输出长这样 账户密码检测报告 [高危] 用户 guest 存在空密码 [高危] UID0 的非root账户: tester [中危] PASS_MAX_DAYS 未设置密码永不过期 [中危] 用户 old_admin 密码将于3天后过期请尽快处理 [信息] 已启用 pam_pwquality 复杂度策略minlen12 [信息] 未找到异常 SUID 文件报告中每条都带修复建议这一点很重要。光告诉运维“你高危了”是不够的他们都知道高危缺的是下一步怎么办。比如空密码账户就直接给出passwd -l 用户名的锁定命令SSH的RootLogin风险给出建议改成prohibit-password的具体方法。让检测报告变成一份可执行的整改清单整个项目才有实用价值。4.3 脚本运行方式与结果留存脚本的头和尾我做了两个设计头部提供一个使用说明并检查是否以root运行尾部用tee把完整输出同时打印到屏幕和写入文件。文件名带时间戳形如security_audit_20250614.log方便以后追溯对比。我把核心调用逻辑放最后方便阅读者按章节阅读各模块代码而不被整体执行流程干扰。关于定时运行借助cron可以很轻松实现每周一凌晨自动巡检并发送报告到指定邮箱。我一般是把脚本放在/usr/local/bin/security_audit.sh然后加一条0 3 * * 1 /usr/local/bin/security_audit.sh /var/log/security_audit.log 21但要注意cron调用常有一个环境变量问题最小PATH可能不含/usr/local/bin或sbin下的命令路径所以脚本里所有关键命令最好用绝对路径或者在脚本开头统一export PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin。这个细节我踩过很多次提醒大家不要忽略。5. 脚本运行中的常见问题与避坑经验这个部分是我最想分享的因为其中很多坑是文档里不会写的。跑安全检测脚本不同于写个小工具安全场景下误报比漏报更让人头疼误报多了没人信漏报了又等于白跑。5.1 权限问题与版本兼容性不以root身份运行脚本会触发大量权限拒绝错误而且很多检查项根本没权限读文件。我建议脚本开头直接做UID检查非root用户则打印提示并退出。但特别说明一下有些检测项比如端口监听普通用户也能跑在一些离线的环境里运维可能没有sudo权限这时可以做降级运行模式只输出可读项不强制中断。另外发行版兼容性是我提过的一个大坑。ss命令在旧版CentOS 6上不存在只有netstatchage -l在某些精简系统上输出中文还是英文取决于locale。我写脚本时统一用LANGC设置保证输出是英文否则中文环境下grep 密码过期能匹配但英文环境的机器匹配不上。跨环境运行前先用一个测试文件跑一遍是最稳妥的做法。还有一个隐患是系统自带命令的扩展性。很多生产系统里有安全加固软件如某主机安全Agent会在PATH中注入自己的同名命令比如find、ls的别名或包装器。虽然这种情况不太常见但也提醒了一句如果脚本跑出来的结果跟预期严重不符可以试试用/usr/bin/find这种绝对路径命令绕过别名。5.2 误报处理与结果复核技巧安全检测的目标不是追求数字好看而是如实反映状态。对每一类检测结果我都保留人工复核的建议操作比如SUID扫描结果用ls -lrt看文件修改时间判断是否近期新增SSH配置检测结果用ssh -G实测当前主机实际生效参数端口监听结果用lsof -iTCP:端口号精确确认进程路径我坚持在报告中加一个“复核指引”字段把每条告警对应的验证命令放在同一行这样即使自动检测有误报复核的人也可以快速判断真伪。这个设计看起来很朴素但在实际操作环境里极大提升了报告的可信度。没这个字段的脚本给人看的时候总要附带一堆解释大家不信任有了这个字段报告本身就是一份可验证的文档。5.3 检测脚本本身的安全加固安全工具本身如果不安全那才是最大的安全漏洞。这个项目里我还做了一个小加固脚本文件权限设置成700属主为root脚本内部的临时文件统一写入/tmp下的随机目录并在退出时用trap清理部分命令行参数做了白名单校验防止被注入特殊字符。关于trap的用法TMPDIR_SAFE$(mktemp -d /tmp/security_audit.XXXXXX) trap rm -rf $TMPDIR_SAFE EXIT这两行走位很关键因为审计脚本会处理passwd、shadow这些敏感文件留下的临时文件如果权限太松等于把系统的影子文件复制了一份丢在/tmp里等人捡。我见过有审计脚本跑完不清理直接把带有整个shadow内容的报告留在默认目录权限还是644这已经不能叫测漏洞了你这是制造漏洞。另外在调用外部命令时最好对所有包含路径的参数做引号包裹避免路径中出现空格或特殊字符导致的命令解析异常。虽然服务器路径一般不会那么妖但find / -name扫到的文件名可能包含空格不引号包裹的话百分百会在处理时炸锅。6. 扩展应用与自动化安全巡检体系单次检测拿到报告只是第一步安全不是一锤子买卖。那这套Shell脚本体系还能往哪个方向发展呢我结合实际项目说说扩展方向。最常见的扩展是把它嵌到CICD流水线的发布环境检查环节。比如每次代码发布前自动在目标测试机上跑一遍快速巡检如果新增了高危项就直接拦截发布。这个模式在很多公司已经实际落地了。原理非常简单就是流水线里加一个执行步骤security_audit.sh --quick --json产出JSON结构的结果然后由流水线去解析关键字段并决定是否放行。另一个方向是结合监控系统的主动采集。比如通过cron定时跑检测在出现新的高危告警时用curl推送到企业微信或钉钉群。这样运维不用每天手动登录服务器看报告系统会自动把风险事件推到负责人面前。推送脚本核心逻辑if [ $high_risk_count -gt 0 ]; then curl -s -X POST -H Content-Type: application/json \ -d {\content\:\发现 $high_risk_count 个高危安全项请立即登录 $HOSTNAME 查看\} \ https://example.com/webhook-url fi至于要不要做成一个无限循环的守护进程我的建议是不要。Shell守护进程的稳定性难以保证一旦有人误kill进程反而影响运维判断。更合理的架构是保留被动检测思路用cron把执行和结果推送做好这已经能覆盖绝大多数应用场外景。真的需要实时监控的就去上成熟的HIDS产品不必在脚本层面硬扛。从长期维护的角度我也建议给脚本加上版本号和变更记录任何安全工具的更新都应该有迹可循。这样每次检测报告出现变化时能判断是系统配置变了还是检测逻辑升级了避免被假报警拖去加班。这个习惯养成了之后你的安全运维日志会越来越可靠。这套脚本从开始写到现在我维护了大半年每次在真实服务器上跑完都会顺手补一个之前没考虑到的检测项。很多Shell安全检测的进阶逻辑其实都源于一次次实际扫描后的复盘。希望这一章的记录也能帮你的排查工作省一些时间。
返回列表