如何检查服务器是否存在安全漏洞?系统补丁、端口与服务排查

安全检查最尴尬的状态是:不知道自己有哪些问题。补丁打没打、端口开了哪些、有没有多余账号、服务版本是不是已经进了漏洞库——这些问题如果只能靠"应该没问题吧"来回答,那出事只是时间问题。

这篇文章给出一套可以周期性执行的自查流程:补丁与内核、端口与服务、账号与权限、文件权限与 SUID、已知漏洞版本核对、Web 组件依赖、弱口令、以及自动化扫描工具的定位。所有命令都针对自己管理的机器。

先说一条底线:本文所有的检查命令只适用于你自己拥有或已获得明确授权管理的服务器。在未获得授权的目标上运行扫描器、探测端口或尝试漏洞验证,在多数司法辖区都属于违法行为。 这一条不是免责声明,是操作前提。

一、补丁与内核更新状态

补丁是最重要也最容易拖延的一项。检查分两步:有没有待更新、当前内核是不是最新的。

# RHEL、Rocky、CentOS
sudo dnf check-update                    # 列出可用更新
sudo dnf updateinfo list security        # 只看安全更新(需 dnf-plugins-core)
sudo dnf updateinfo summary
rpm -qa --last | head -15                # 最近安装的包及时间

# Debian、Ubuntu
sudo apt update
sudo apt list --upgradable
apt-get -s upgrade | grep -i security    # 模拟运行,筛出安全更新
grep -h ' upgrade ' /var/log/dpkg.log | tail -15

只看"有没有待更新"还不够,真正的安全更新要单独识别。dnf updateinfo list security 和 apt-get -s upgrade 加 grep 能区分安全修复和功能更新,这在需要评估重启风险时很有用:安全更新优先装,功能更新可以排期。

内核更新后需要重启才生效,判断是否需要重启:

# 当前运行的内核
uname -r
uname -a

# RHEL 系:判断是否需要重启
sudo dnf install -y dnf-plugins-core
sudo needs-restarting -r; echo "退出码 $?"
# 退出码 1 表示需要重启

# Debian、Ubuntu
ls -l /var/run/reboot-required 2>/dev/null && echo "需要重启"
cat /var/run/reboot-required.pkgs 2>/dev/null     # 哪些包要求重启

# 已安装的所有内核
rpm -qa kernel | sort            # RHEL 系
dpkg -l | grep linux-image       # Debian 系

这里有个常见困惑:uname -r 显示的是当前运行的内核版本,而 /boot 下可能已经装了更新的内核,只是还没重启。检查时用 uname -r 对比已安装内核列表的最新版本,如果不一致就说明有新内核待生效。

重启代价高的场景可以考虑内核热补丁方案(主流云厂商和部分发行版提供),它能在不重启的情况下应用关键内核安全修复。但要清楚它的适用边界:只覆盖特定的高危漏洞,不是所有内核更新都能热补丁,长期看仍需要安排重启窗口。

无人值守更新能解决"忘了打"的问题:

# Debian、Ubuntu
sudo apt install -y unattended-upgrades
sudo systemctl status unattended-upgrades.timer
cat /etc/apt/apt.conf.d/50unattended-upgrades | grep -v '^//'

# RHEL、Rocky、CentOS
sudo dnf install -y dnf-automatic
sudo systemctl status dnf-automatic-install.timer

建议生产环境只自动安装安全更新,不自动安装全部更新,避免版本跳变。自动更新之后要有检查和告警,知道它更新了什么、是否需要重启。

二、端口与服务排查

第一步,列出所有监听端口。 ss 已经取代 netstat,后者属于 net-tools 包,在新系统上默认不装且不再维护:

sudo ss -lntup                    # TCP + UDP 监听,含进程
sudo ss -lntp                     # 只看 TCP
sudo ss -antp                     # 所有连接(含已建立)
sudo ss -s                        # 连接数汇总

重点看两列:Local Address 里的绑定地址和 Process 里的进程名。

监听地址 含义 风险
0.0.0.0:3306 所有网卡可见 高,数据库对外暴露
:::8080 所有 IPv6 网卡可见 高,常被忽略
127.0.0.1:6379 仅本机 低
10.0.1.5:8080 仅内网网卡 中,需配合安全组

第二步,逐个问一句"这个端口为什么开着"。 这是最有效的方法。说不出用途的监听就是待处理项。常见的情况包括:装系统时自带的服务(cups 打印、avahi、rpcbind、postfix)、测试时临时启动的服务忘了关、以及容器或编排组件暴露的管理端口。

特别要检查这些容易出问题的服务:

# Redis、Memcached、MongoDB 等默认无认证的服务
sudo ss -lntp | grep -E 'redis|memcach|mongod|etcd|docker'
# Docker daemon 的 2375/2376 端口绝不应暴露在公网
sudo ss -lntp | grep 2375
# 检查是否监听了 0.0.0.0
sudo ss -lnt | grep '0.0.0.0' | awk '{print $4}' | sort -u

第三步,关闭不需要的服务。

sudo systemctl list-unit-files --state=enabled
sudo systemctl stop 服务名 && sudo systemctl disable 服务名
sudo systemctl status 服务名

风险提示:停用服务前先确认它不是业务依赖,查看 systemctl status 和 journalctl -u 服务名 的历史记录,在低峰期操作,并准备好 systemctl start 的回滚。数据库类服务停用会直接中断业务。

如果暂时不能停,至少做两层收敛:在安全组里限制来源、在服务配置里绑定内网地址(如 Redis 的 bind 127.0.0.1、MySQL 的 bind-address),并开启认证。

三、账号与权限审计

# 空口令账号
sudo awk -F: '($2 == "" || $2 == "!") {print "检查: " $1}' /etc/shadow

# UID 为 0 的账号(除 root 外不应有)
sudo awk -F: '($3 == 0) {print "UID 0: " $1}' /etc/passwd

# 有登录 shell 的账号
sudo grep -E ':/bin/(bash|sh|zsh)$' /etc/passwd

# 口令有效期配置
sudo grep -E '^PASS_(MAX|MIN|WARN)' /etc/login.defs
sudo chage -l 用户名

# 长期未登录的账号
sudo lastlog | grep -v 'Never logged in' | head -30
sudo lastlog -b 90          # 列出 90 天未登录的账号

四项检查的判断标准:

不应有空口令账号。 /etc/shadow 第二字段为空表示无口令可直接登录,出现即为高危。值为 ! 或 * 表示账号被锁定,这是正常的。

除 root 外不应有 UID 0 账号。 出现说明可能存在提权后的后门账号,要立即查清来源。

有登录 shell 的账号应该都能解释用途。 系统服务账号的 shell 应该是 /sbin/nologin 或 /bin/false。发现不认识的交互式账号要追查。

长期未登录的账号应该锁定。 离职人员账号、废弃的服务账号是常见的失守点。

# 锁定账号(不删除,避免文件属主变成孤立 UID)
sudo usermod -L 用户名
sudo usermod -s /sbin/nologin 用户名

# 或设置口令过期,强制下次修改
sudo chage -d 0 用户名

sudo 权限审计:

sudo cat /etc/sudoers
sudo ls -la /etc/sudoers.d/
sudo grep -r 'NOPASSWD' /etc/sudoers /etc/sudoers.d/
sudo grep -r 'ALL=(ALL)' /etc/sudoers.d/

NOPASSWD 加 ALL 的组合等于把 root 权限无条件交出去,除了明确需要的自动化场景,都应该去掉。修改 sudoers 一定要用 visudo,它会做语法检查,避免写错导致所有人都无法提权:

sudo visudo
sudo visudo -c          # 只检查语法

四、文件权限与 SUID 检查

# 关键文件权限
ls -l /etc/passwd /etc/shadow /etc/group /etc/gshadow
ls -ld /root ~/.ssh
ls -l ~/.ssh/authorized_keys ~/.ssh/id_rsa 2>/dev/null

# 期望值:shadow 应为 600 或 640,私钥 600,.ssh 目录 700

全局可写文件是提权的跳板,尤其是属主为 root 的:

sudo find / -xdev -type f -perm -0002 -user root 2>/dev/null | head -20
sudo find / -xdev -type d -perm -0002 ! -path '/proc/*' ! -path '/tmp/*' 2>/dev/null | head -20

SUID 与 SGID 文件要定期核对:

sudo find / -xdev -perm -4000 -type f -exec ls -l {} \; 2>/dev/null
sudo find / -xdev -perm -2000 -type f -exec ls -l {} \; 2>/dev/null

首次执行时把结果保存为基线:

sudo find / -xdev -perm -4000 -type f | sort > /root/baseline-suid.txt
# 以后对比
sudo find / -xdev -perm -4000 -type f | sort | diff /root/baseline-suid.txt -

多出来的条目要逐个查证。常见的合法 SUID 程序有 passwd、sudo、su、ping、mount 等;一个 SUID 的 bash、find、vim 或 /tmp 下的可执行文件,基本可以确定是后门。

完整性校验用发行版自带的工具检查系统文件是否被替换:

# RHEL、Rocky、CentOS
sudo rpm -Va | grep -v '^.........' | head -30
# Debian、Ubuntu
sudo dpkg --verify | head -30
sudo debsums -c 2>/dev/null | head -30      # 需安装 debsums

rpm -Va 输出的第一列是校验标志:5 表示 MD5 不符(文件内容被改)、S 大小变化、M 权限变化、U 属主变化。发现 ps、top、ls、netstat 这类命令被改动,说明系统已不可信。

更强的完整性检查用 AIDE,它在干净状态下建立数据库,之后定期比对:

sudo dnf install -y aide        # RHEL 系
sudo apt install -y aide        # Debian 系
sudo aide --init && sudo mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
sudo aide --check

五、已知漏洞版本核对

很多攻击是先探测版本再选择手法的,所以要知道自己跑的是什么版本、是否在受影响范围内。

# 系统与内核
cat /etc/os-release
uname -r

# 常见服务版本
ssh -V 2>&1                    # OpenSSH
openssl version                # OpenSSL
nginx -v 2>&1                  # Nginx
httpd -v                        # Apache(RHEL 系包名)
php -v                          # PHP
mysql --version                 # MySQL/MariaDB
python3 --version
java -version 2>&1

拿到版本号之后,对照公开的漏洞数据库(如 CVE、NVD、各大发行版的安全公告)确认是否在受影响范围内。检查时要注意:发行版会通过 backport 的方式把安全修复移植到旧版本号上,所以 RHEL 上看到 OpenSSH 8.0 不一定就受某个 8.0 的漏洞影响,要看发行版的安全公告(如 rpm -q --changelog openssh | grep CVE)。这是很多人误判的原因:直接用上游版本号比对会得出错误结论。

# 查看包的更新日志里有没有修复过某个 CVE
rpm -q --changelog openssh | grep -i 'CVE' | head -10
zcat /usr/share/doc/openssh-client/changelog.Debian.gz 2>/dev/null | grep -i CVE | head -10

更高阶的做法是软件物料清单(SBOM):生成系统上所有软件包的清单,当新的 CVE 公布时可以批量比对受影响范围。

# 导出包清单
rpm -qa > /root/pkg-list-$(date +%F).txt        # RHEL 系
dpkg-query -W -f='${Package} ${Version}\n' > /root/pkg-list-$(date +%F).txt   # Debian 系

# 使用漏洞扫描工具比对(需先安装)
sudo dnf install -y openscap-scanner scap-security-guide      # RHEL 系
sudo apt install -y openscap-scanner                          # Debian 系

六、Web 组件与依赖漏洞

服务器上跑的应用往往比系统本身更容易出问题,因为依赖层层嵌套。

# PHP Composer
composer audit

# Node.js
npm audit
npm audit --production        # 只看生产依赖

# Python
pip-audit                     # 需 pip install pip-audit
pip list --outdated

# Java Maven(需插件)
mvn org.owasp:dependency-check-maven:check

# Go
govulncheck ./...

这些工具的原理都是把依赖清单与漏洞数据库比对,给出受影响的组件和修复版本。使用要点:

只检查生产依赖。 开发依赖的漏洞通常不影响运行时,但也不绝对,有的构建工具链漏洞会影响部署过程。

注意传递依赖。 你直接依赖的库没问题,不代表它依赖的库没问题。npm audit 和 composer audit 会递归检查,看报告时注意层级。

修复方式要评估。 自动升级(如 npm audit fix)可能引入不兼容变更,生产环境应该在测试环境验证后再升级。不能立即升级的,评估实际可达性:漏洞函数是否真的被调用、是否可以从外部触发。

把依赖检查接入 CI。 每次构建时自动跑,把漏洞发现提前到上线之前,而不是上线后靠人工抽查。

同时检查 Web 目录下的遗留文件,这些是常见的信息泄露源:

sudo find /var/www -name '*.sql' -o -name '*.bak' -o -name '*.zip' -o -name '*.tar.gz' 2>/dev/null
sudo find /var/www -name '*.git' -o -name '.env' -o -name '*.log' 2>/dev/null

.git 目录暴露会导致源码泄露,.env 文件暴露会泄露数据库口令和 API 密钥,这两项要在 Web 服务器配置里显式拒绝访问。

七、弱口令自查与日志核查

口令强度自查要在授权范围内进行,且只针对自己的系统。检查思路是对已知账号尝试常见弱口令组合(由自己编写字典),发现命中后强制修改。生产环境更推荐的做法是从策略上杜绝:

# 检查口令复杂度策略是否生效
grep -E 'minlen|dcredit|ucredit|ocredit|lcredit' /etc/security/pwquality.conf
# 检查 pam 配置是否引用了 pwquality
grep -r 'pwquality\|pam_unix' /etc/pam.d/ | head

对服务类账号(数据库、Redis、管理后台)也要单独检查,这些往往不在系统口令策略的覆盖范围内。

日志核查能发现正在发生的攻击和已经发生的入侵:

# 失败登录统计
sudo grep 'Failed password' /var/log/auth.log /var/log/secure 2>/dev/null | wc -l
sudo grep 'Failed password' /var/log/auth.log | grep -oP 'from \K[0-9.]+' | sort | uniq -c | sort -rn | head

# 成功登录记录,重点看有无陌生来源
sudo grep -E 'Accepted (password|publickey)' /var/log/auth.log 2>/dev/null | tail -20
last -30

# sudo 使用记录
sudo grep 'sudo' /var/log/auth.log 2>/dev/null | tail -20

判断标准是"能不能解释每一条记录的来源"。出现解释不了的,就按应急流程处理。

八、自动化扫描工具的定位

手工检查覆盖面和持续性都有限,成熟的做法是引入工具:

工具 类型 定位
Lynis 主机审计 轻量、无依赖、输出加固建议,适合日常自检
OpenSCAP 合规基线 对照 CIS、等保等基线做合规检查
Nessus / OpenVAS 漏洞扫描 网络与主机漏洞发现,需授权且注意扫描影响
云厂商主机安全服务 持续检测 与云平台集成,含基线检查和入侵检测

Lynis 是最容易上手的一个,纯 shell 实现,不需要安装依赖,输出的是可操作的加固建议:

# RHEL 系(EPEL)
sudo dnf install -y lynis
# Debian、Ubuntu
sudo apt install -y lynis

sudo lynis audit system
sudo less /var/log/lynis.log
sudo grep -i 'warning\|suggestion' /var/log/lynis-report.dat | head -40

它不会自动修改任何东西,只给建议,适合作为定期检查的起点。

OpenSCAP 适合有合规要求的场景,能对照标准基线输出不合规项:

sudo dnf install -y openscap-scanner scap-security-guide
oscap info /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml
sudo oscap xccdf eval --profile xccdf_org.ssgproject.content_profile_cis \
  --results /root/scan-results.xml \
  --report /root/scan-report.html \
  /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml

Nessus 与 OpenVAS 这类网络漏洞扫描器能力更强,但有两个前提必须强调:

第一,必须有明确授权。 只扫描自己拥有或获得书面授权的目标。对未授权目标运行扫描器,在多数司法辖区属于违法行为,即使只是"看看端口"。

第二,注意扫描本身的影响。 漏洞扫描会产生大量探测流量,可能对生产服务造成压力,某些检查项还可能触发服务异常。做法是在测试环境先验证、选择业务低峰期、排除敏感目标(如工业控制设备、老旧数据库)、并提前通知相关方。

形成周期性检查机制

一次检查解决不了长期问题,环境会漂移。建议按下面的节奏建立机制:

频率 检查项
每周 待更新补丁、失败登录统计、监听端口变化、备份任务结果
每月 账号审计、sudo 授权、SUID 基线比对、日志留存检查
每季度 完整自查(本文全部项)、安全组与防火墙规则审计、依赖漏洞扫描、恢复演练
事件驱动 新 CVE 公布时立即核对受影响范围;新人入职离职时调整账号

把检查结果落到能看趋势的地方。 单次数值意义有限,突然的变化才是信号。比如失败登录从每天几十涨到几万、监听端口突然多了一个、SUID 清单突然多了一条,这些都比绝对值更值得关注。

做成脚本和清单。 手工检查会漏项,脚本化的巡检能保证每次覆盖同样的范围。定期执行的命令可以写成脚本,输出送到日志系统或告警通道:

#!/bin/bash
# /usr/local/bin/sec-check.sh  每周巡检示例
{
  echo "=== $(date +%F) 安全巡检 ==="
  echo "-- 待更新包 --"
  dnf check-update -q 2>/dev/null | wc -l
  echo "-- 监听端口 --"
  ss -lntup
  echo "-- UID 0 账号 --"
  awk -F: '($3==0){print $1}' /etc/passwd
  echo "-- 失败登录 Top5 --"
  grep 'Failed password' /var/log/secure 2>/dev/null | \
    grep -oP 'from \K[0-9.]+' | sort | uniq -c | sort -rn | head -5
} | mail -s "安全巡检 $(hostname)" ops@example.com

常见问题

版本号看着很旧,但发行版说已经修复了,到底受不受影响?

看发行版的安全公告,而不是上游版本号。RHEL、Debian 等会把安全修复反向移植到旧版本上,所以版本号不变也可能已经修复。用 rpm -q --changelog 包名 | grep CVE 或发行版的公告页面确认。这是最容易误判的一点。

扫描器扫出一堆漏洞,都要修吗?

按可达性和影响排序。先看:是否对外可达、是否已有公开验证方式、修复成本。不能立即修的要有补偿措施(网络隔离、访问控制、加强监控),并记录风险和计划修复时间。不要因为数量多就全部忽略,也不要不分轻重地立刻全修。

生产环境能跑漏洞扫描吗?

可以,但要谨慎:选低峰期、先在测试环境验证、排除敏感目标、控制扫描强度、提前通知相关方。某些检查项可能触发服务异常,要有随时中止的准备。更稳妥的方式是先在预发环境或镜像副本上扫。

容器环境怎么检查?

镜像层面用镜像扫描工具(如 trivy、grype)检查依赖和基础镜像漏洞;运行时层面检查容器权限(是否 privileged、是否挂载了宿主目录、是否以 root 运行);编排层面检查 API Server、etcd、kubelet 的暴露与认证配置。宿主系统本身的检查与本文相同。

多久做一次完整检查合适?

季度一次完整自查,每周一次自动化巡检。此外在重大 CVE 公布、系统重大变更后要做临时检查。关键是把检查固化成流程和清单,而不是靠记忆和临时起意。

总结

  • 补丁检查要区分安全更新与功能更新,用 dnf updateinfo list security 或 apt-get -s upgrade 筛选;内核更新后需重启生效,用 needs-restarting -r 或 reboot-required 判断,重启代价高的场景可考虑热补丁。
  • 端口排查用 ss -lntup(netstat 已被替代),逐个询问每个监听的用途,重点检查绑定 0.0.0.0 的服务以及 Redis、Memcached、Docker 2375 这类默认无认证或高危的端口。
  • 账号审计查四项:空口令、多余 UID 0 账号、有登录 shell 的陌生账号、长期未登录账号;sudo 配置重点看 NOPASSWD 加 ALL 的组合,改配置必须用 visudo。
  • 版本核对要看发行版安全公告而不是上游版本号(backport 会导致误判),依赖检查用 composer audit、npm audit、pip-audit 并接入 CI,把漏洞发现提前到上线之前。
  • 自动化工具按定位选择:Lynis 做日常轻量自检、OpenSCAP 做合规基线、Nessus/OpenVAS 做漏洞扫描;使用扫描器必须有明确授权,只扫自己拥有或获授权的目标,并注意扫描本身对生产的影响。
  • 把检查固化为周期机制:每周巡检补丁与日志、每月审计账号与权限、每季度完整自查和恢复演练、重大 CVE 公布时临时核对,并把结果落在能看趋势的地方。