#!/usr/bin/env bash #=============================================================================== # HW_Toolkit_Enhanced v1.0 # Linux 应急响应一键排查工具(只读、无破坏、跨发行版) # # 思路参考公众号"一个包包菜"《HW-Linux 应急工具排查》, # 扩展到 28 个模块:主机 / Web / 网络 / 容器 / 反弹 shell / PAM / SSH 密钥 / # 凭据搜索 / Timestomping / 防火墙 / DNS 劫持 / Shell history 异常。 # # 兼容: Debian/Ubuntu/Kali, RHEL/CentOS/Rocky/Alma/Fedora, Alpine, Arch # 依赖: bash 4+, 标准 Linux 工具 (可选 python3/curl/docker/kubectl) # 用法: sudo ./hw_toolkit.sh [选项] # --webdir DIR 指定 Web 根目录(默认自动探测) # --logdir DIR 指定 Web 日志目录(默认自动探测) # --api-key KEY 微步在线 API Key(模块 17 使用) # --quick 快速巡检模式,跳过耗时深度分析 # --skip-web 跳过 Web 层(模块 14-15) # --skip-vuln 跳过漏洞扫描(模块 18) # --skip-ti 跳过威胁情报(模块 17) # --skip-log 跳过 Web 日志分析(模块 15) # --skip-container 跳过容器层(模块 19-20) # --skip-extra 跳过扩展模块(21-28) # -h, --help 帮助 # # 输出: /tmp/hw_toolkit_YYYYMMDD_HHMMSS/ # full_report.txt 完整报告 # summary.txt 简报 # suspect_files.txt 疑似恶意文件清单 #=============================================================================== set -u umask 077 #==================== 全局变量 ==================== # 注意:变量名不用 VERSION(会被 /etc/os-release 的 VERSION 覆盖) TOOLKIT_VERSION="1.0" SCRIPT_NAME="$(basename "$0" 2>/dev/null || echo hw_toolkit.sh)" SCRIPT_PATH="$(readlink -f "$0" 2>/dev/null || echo "$0")" SCRIPT_PID=$$ START_TS=$(date +%s) OUTPUT_DIR="/tmp/hw_toolkit_$(date +%Y%m%d_%H%M%S)" FULL_REPORT="$OUTPUT_DIR/full_report.txt" SUMMARY_FILE="$OUTPUT_DIR/summary.txt" SUSPECT_FILES="$OUTPUT_DIR/suspect_files.txt" # 计数器:最终以 $OUTPUT_DIR/.cnt_* 字段文件为准(generate_summary 中读取) # 定义空值仅为 set -u 兜底,实际计数由 log_* 写入字段文件。 CRIT_COUNT=0 WARN_COUNT=0 OK_COUNT=0 INFO_COUNT=0 # 命令行参数 QUICK=0 SKIP_WEB=0 SKIP_VULN=0 SKIP_LOG=0 SKIP_TI=0 SKIP_CONTAINER=0 SKIP_EXTRA=0 WEBDIR="" LOGDIR="" API_KEY="" # 发行版 DISTRO_ID="unknown" DISTRO_LIKE="" DISTRO_VERSION="" USE_JOURNAL=0 AUTH_LOG="" SYS_LOG="" # 自身进程树(用于隐藏进程检测排除) SELF_PIDS="$$" SELF_PIDS_COLLECTED=0 #==================== 参数解析 ==================== while [ $# -gt 0 ]; do case "$1" in --webdir) WEBDIR="${2:-}"; shift 2 ;; --logdir) LOGDIR="${2:-}"; shift 2 ;; --api-key) API_KEY="${2:-}"; shift 2 ;; --quick) QUICK=1; shift ;; --skip-web) SKIP_WEB=1; shift ;; --skip-vuln) SKIP_VULN=1; shift ;; --skip-log) SKIP_LOG=1; shift ;; --skip-ti) SKIP_TI=1; shift ;; --skip-container) SKIP_CONTAINER=1; shift ;; --skip-extra) SKIP_EXTRA=1; shift ;; -h|--help) # 打印文件头部注释块(从第 2 行到第二个 #===== 分隔线),对修改头部保持健壮 sed -n '2,/^#=====/p' "$SCRIPT_PATH" 2>/dev/null || cat < "$FULL_REPORT" : > "$SUSPECT_FILES" # 计数器文件:管道子 shell 中也能累加统计 : > "$OUTPUT_DIR/.cnt_crit" : > "$OUTPUT_DIR/.cnt_warn" : > "$OUTPUT_DIR/.cnt_ok" : > "$OUTPUT_DIR/.cnt_info" } # 同时输出到终端和报告文件(去除 ANSI 色码) _emit() { local line="$1" printf '%b\n' "$line" printf '%s\n' "$(printf '%b' "$line" | sed 's/\x1b\[[0-9;]*m//g')" >> "$FULL_REPORT" } log_banner() { local text="$1" _emit "" _emit "${C_BLU}${C_BLD}================================================================${C_RST}" _emit "${C_BLU}${C_BLD} $text${C_RST}" _emit "${C_BLU}${C_BLD}================================================================${C_RST}" } log_module() { local num="$1" local name="$2" _emit "" _emit "${C_CYN}${C_BLD}[模块 $num] $name${C_RST}" _emit "${C_CYN}----------------------------------------------------------------${C_RST}" } log_sub() { _emit "" _emit "${C_MAG}> $1${C_RST}" } # 计数写入字段文件:管道/子 shell 中变量会丢失,文件 append 不受影响 log_ok() { printf '1\n' >> "$OUTPUT_DIR/.cnt_ok" 2>/dev/null || true; _emit " ${C_GRN}[√ 正常]${C_RST} $1"; } log_warn() { printf '1\n' >> "$OUTPUT_DIR/.cnt_warn" 2>/dev/null || true; _emit " ${C_YEL}[! 警告]${C_RST} $1"; } log_crit() { printf '1\n' >> "$OUTPUT_DIR/.cnt_crit" 2>/dev/null || true; _emit " ${C_RED}[× 异常]${C_RST} $1"; } log_info() { printf '1\n' >> "$OUTPUT_DIR/.cnt_info" 2>/dev/null || true; _emit " $1"; } log_raw() { _emit " $1"; } # 记录疑似文件 add_suspect() { local path="$1" local reason="$2" if [ -n "$path" ]; then printf '%s\t%s\n' "$path" "$reason" >> "$SUSPECT_FILES" fi } #==================== 工具函数 ==================== # 安全获取文件行数,空/不存在返回 0 safe_wc_l() { local f="$1" if [ ! -f "$f" ] || [ ! -r "$f" ] || [ ! -s "$f" ]; then echo 0 else wc -l < "$f" 2>/dev/null | tr -d ' ' || echo 0 fi } # 命令存在检测 has_cmd() { command -v "$1" >/dev/null 2>&1; } # 文件归属包检测(输出包名;空输出 = 不属于任何包) file_owned_by_pkg() { local f="$1" rl pkg="" rl=$(readlink -f "$f" 2>/dev/null || echo "$f") if has_cmd dpkg; then pkg=$(dpkg -S "$f" 2>/dev/null | head -1 | cut -d: -f1) [ -z "$pkg" ] && pkg=$(dpkg -S "$rl" 2>/dev/null | head -1 | cut -d: -f1) fi if [ -z "$pkg" ] && has_cmd rpm; then pkg=$(rpm -qf "$rl" 2>/dev/null | head -1) case "$pkg" in ""|*"not owned"*|error*) pkg="" ;; esac fi [ -n "$pkg" ] && printf '%s' "$pkg" return 0 } # 包完整性校验 # 返回: 0=通过 1=失败(差异输出到 stdout) 2=该文件不属于任何包 3=无可用校验工具 verify_pkg_file() { local f="$1" rl out pkg rl=$(readlink -f "$f" 2>/dev/null || echo "$f") if has_cmd rpm && rpm -qf "$rl" >/dev/null 2>&1; then out=$(rpm -Vf "$rl" 2>/dev/null || true) if [ -n "$out" ]; then printf '%s\n' "$out" return 1 fi return 0 fi if has_cmd dpkg; then pkg=$(dpkg -S "$rl" 2>/dev/null | head -1 | cut -d: -f1) [ -z "$pkg" ] && return 2 out=$(LC_ALL=C dpkg -V "$pkg" 2>/dev/null | grep -E "(^|[[:space:]])${rl}\$" || true) if [ -n "$out" ]; then printf '%s\n' "$out" return 1 fi return 0 fi return 3 } # root 检测 require_root() { if [ "$(id -u)" -ne 0 ]; then echo "警告: 当前非 root,部分模块(/etc/shadow、其他用户目录)可能读取失败" >&2 fi } # 收集自身进程树 PID(含子进程、孙进程) _collect_self_tree() { local pid="$1" local child SELF_PIDS="$SELF_PIDS $pid" # pgrep -P 可能不存在,用 ps 兜底 if has_cmd pgrep; then for child in $(pgrep -P "$pid" 2>/dev/null); do _collect_self_tree "$child" done else for child in $(ps -o pid= --ppid "$pid" 2>/dev/null | tr -d ' '); do [ -n "$child" ] && _collect_self_tree "$child" done fi } # 判断 PID 是否属于自身树 _is_self_pid() { local pid="$1" local p # 懒采集,只跑一次 if [ "$SELF_PIDS_COLLECTED" -eq 0 ]; then SELF_PIDS="" _collect_self_tree "$SCRIPT_PID" SELF_PIDS_COLLECTED=1 fi for p in $SELF_PIDS; do [ "$pid" = "$p" ] && return 0 done return 1 } # 可登录 shell 判定 is_login_shell() { local sh="$1" case "$sh" in /bin/bash|/bin/sh|/bin/zsh|/bin/dash|/bin/fish|/bin/ksh|/bin/csh|/bin/tcsh|/bin/rbash|/bin/bash-fortune) return 0 ;; /usr/bin/bash|/usr/bin/sh|/usr/bin/zsh|/usr/bin/dash|/usr/bin/fish|/usr/bin/ksh|/usr/bin/csh|/usr/bin/tcsh) return 0 ;; /usr/local/bin/bash|/usr/local/bin/zsh|/usr/local/bin/fish) return 0 ;; *) return 1 ;; esac } #==================== 发行版检测 ==================== detect_distro() { if [ -f /etc/os-release ]; then # shellcheck disable=SC1091 . /etc/os-release DISTRO_ID="${ID:-unknown}" DISTRO_LIKE="${ID_LIKE:-}" DISTRO_VERSION="${VERSION_ID:-}" fi # 判断日志系统 case "$DISTRO_ID" in debian|ubuntu|kali|linuxmint|pop|raspbian|deepin|uos) AUTH_LOG="/var/log/auth.log" SYS_LOG="/var/log/syslog" ;; rhel|centos|rocky|almalinux|fedora|ol|amzn|anolis|openeuler|kylin|neo4j|uos-server) AUTH_LOG="/var/log/secure" SYS_LOG="/var/log/messages" ;; arch|manjaro|endeavouros) AUTH_LOG="" SYS_LOG="" USE_JOURNAL=1 ;; alpine) AUTH_LOG="/var/log/messages" SYS_LOG="/var/log/messages" ;; opensuse*|sles|sled) AUTH_LOG="/var/log/messages" SYS_LOG="/var/log/messages" ;; *) AUTH_LOG="/var/log/auth.log" SYS_LOG="/var/log/syslog" [ -f /var/log/secure ] && AUTH_LOG="/var/log/secure" [ -f /var/log/messages ] && SYS_LOG="/var/log/messages" ;; esac # 如果对应文件不存在且 journalctl 可用,走 journald if [ -n "$AUTH_LOG" ] && [ ! -f "$AUTH_LOG" ] && has_cmd journalctl; then USE_JOURNAL=1 fi if [ -z "$AUTH_LOG" ] && has_cmd journalctl; then USE_JOURNAL=1 fi } #==================== 模块 01: 基本系统信息 ==================== mod_01_system_info() { log_module "01" "基本系统信息" local hostname kernel os_ver uptime_str ip_addrs hostname=$(hostname 2>/dev/null || cat /proc/sys/kernel/hostname 2>/dev/null || echo "unknown") kernel=$(uname -r 2>/dev/null) os_ver=$(uname -s 2>/dev/null) uptime_str=$(uptime -p 2>/dev/null || awk '{print int($1/86400)"d "int($1%86400/3600)"h"}' /proc/uptime 2>/dev/null) ip_addrs=$(ip -4 addr show 2>/dev/null | awk '/inet /{print $2}' | grep -v '127.0.0.1' | tr '\n' ' ') log_info "主机名: $hostname" log_info "IP地址: ${ip_addrs:-无}" log_info "操作系统: $os_ver ($DISTRO_ID $DISTRO_VERSION)" log_info "内核版本: $kernel" log_info "运行时间: $uptime_str" log_info "当前时间: $(date '+%Y-%m-%d %H:%M:%S %Z')" log_info "日志后端: $([ $USE_JOURNAL -eq 1 ] && echo 'journald' || echo 'rsyslog')" # 内核已知提权 CVE 提示(简化清单,仅提示版本区间) log_sub "内核提权漏洞提示" local k_major k_minor k_patch k_major=$(echo "$kernel" | cut -d. -f1) k_minor=$(echo "$kernel" | cut -d. -f2) k_patch=$(echo "$kernel" | cut -d. -f3 | grep -oE '^[0-9]+' || echo 0) k_patch=${k_patch:-0} local vuln_hit=0 # CVE-2016-5195 DirtyCow (< 4.8.3) if [ "$k_major" -lt 4 ] || { [ "$k_major" -eq 4 ] && [ "$k_minor" -lt 8 ]; }; then log_warn "内核 $kernel 可能受 DirtyCow (CVE-2016-5195) 影响" vuln_hit=1 fi # CVE-2022-0847 DirtyPipe (5.8 <= x < 5.16.11 / 5.15.25 / 5.10.102) if [ "$k_major" -eq 5 ] && [ "$k_minor" -ge 8 ] && [ "$k_minor" -lt 17 ]; then log_warn "内核 $kernel 可能受 DirtyPipe (CVE-2022-0847) 影响,请核对补丁版本" vuln_hit=1 fi # CVE-2021-4034 PwnKit (polkit, 全版本受影响,看是否装 polkit) if [ -f /usr/bin/pkexec ] || [ -f /bin/pkexec ]; then if has_cmd dpkg; then if dpkg -l policykit-1 2>/dev/null | grep -q '^ii' && ! dpkg -l policykit-1 2>/dev/null | awk '/^ii/{print $3}' | grep -qE '0\.105-31|0\.113'; then log_warn "polkit 已装,请核对是否修复 PwnKit (CVE-2021-4034)" vuln_hit=1 fi fi fi [ $vuln_hit -eq 0 ] && log_ok "未发现明显匹配的高危内核提权 CVE" } #==================== 模块 02: 用户与认证排查 ==================== mod_02_users() { log_module "02" "用户与认证排查" # UID=0 用户 log_sub "UID=0 用户(应仅有 root)" local uid0_users uid0_users=$(awk -F: '($3 == 0) {print $1}' /etc/passwd 2>/dev/null) local uid0_count=0 if [ -n "$uid0_users" ]; then uid0_count=$(printf '%s\n' "$uid0_users" | grep -c . 2>/dev/null) uid0_count=${uid0_count:-0} fi if [ ! -r /etc/passwd ]; then log_warn "无法读取 /etc/passwd,跳过 UID=0 检查(请用 sudo 运行)" elif [ "$uid0_count" -eq 1 ] && [ "$uid0_users" = "root" ]; then log_ok "UID=0 用户仅有 root" elif [ "$uid0_count" -gt 1 ]; then log_crit "发现多个 UID=0 用户(疑似后门账户): $(echo $uid0_users | tr '\n' ' ')" echo "$uid0_users" | while read -r u; do [ "$u" != "root" ] && add_suspect "/etc/passwd:$u" "UID=0 非root账户" done elif [ "$uid0_count" -eq 0 ]; then log_crit "未发现 UID=0 用户(/etc/passwd 可能被篡改,请核对)" else log_warn "UID=0 用户列表异常: $uid0_users" fi # 可登录 shell 用户 log_sub "可登录 Shell 用户" local login_users login_users=$(awk -F: '{print $1"|"$3"|"$7}' /etc/passwd 2>/dev/null | while IFS='|' read -r u uid sh; do if is_login_shell "$sh"; then echo "$u (UID=$uid, Shell=$sh)" fi done) if [ -n "$login_users" ]; then echo "$login_users" | while read -r line; do log_info "$line"; done local lu_count=0 lu_count=$(printf '%s\n' "$login_users" | grep -c . 2>/dev/null) lu_count=${lu_count:-0} if [ "$lu_count" -gt 5 ]; then log_warn "可登录用户数偏多 ($lu_count),请核对是否都是必要账号" else log_ok "可登录用户数正常 ($lu_count)" fi else log_info "无可登录 shell 用户" fi # 空密码用户(仅检查可登录 shell 账号,避免系统服务账号误报) log_sub "空密码用户检查(仅可登录 shell 账号)" local empty_pw_found=0 if [ -r /etc/shadow ]; then while IFS=: read -r u pw _rest; do local sh sh=$(getent passwd "$u" 2>/dev/null | cut -d: -f7) is_login_shell "$sh" || continue if [ -z "$pw" ]; then log_crit "用户 $u (shell=$sh) 密码为空" add_suspect "/etc/shadow:$u" "空密码可登录账号" empty_pw_found=1 fi done < /etc/shadow else log_warn "无 /etc/shadow 读取权限,跳过空密码检查(请用 sudo 运行)" fi [ $empty_pw_found -eq 0 ] && log_ok "可登录账号中未发现空密码" # sudo 权限 log_sub "sudo 权限配置" if [ -r /etc/sudoers ]; then local sudo_users sudo_users=$(grep -vE '^\s*#|^\s*$' /etc/sudoers 2>/dev/null | grep -E 'ALL\s*=\s*\(.*\)\s*(NOPASSWD\s*:\s*)?ALL' || true) if [ -n "$sudo_users" ]; then echo "$sudo_users" | while read -r line; do log_info "$line"; done if echo "$sudo_users" | grep -q NOPASSWD; then log_warn "存在 NOPASSWD 免密 sudo 配置,请核对是否必要" fi fi # sudoers.d 目录 if [ -d /etc/sudoers.d ]; then local sd_count sd_count=$(find /etc/sudoers.d -type f ! -name '.*' ! -name 'README*' 2>/dev/null | wc -l) log_info "/etc/sudoers.d 下附加配置文件数: $sd_count" find /etc/sudoers.d -type f ! -name '.*' ! -name 'README*' 2>/dev/null | while read -r f; do if grep -q NOPASSWD "$f" 2>/dev/null; then log_warn "$f 包含 NOPASSWD 配置" fi done fi else log_warn "无 /etc/sudoers 读取权限" fi # SSH 配置 log_sub "SSH 服务配置" local sshd_cfg="/etc/ssh/sshd_config" if [ -r "$sshd_cfg" ]; then local v v=$(awk '/^[[:space:]]*PermitRootLogin/{print $2; exit}' "$sshd_cfg") case "$v" in no|prohibit-password|without-password) log_ok "PermitRootLogin=$v(安全)" ;; yes) log_warn "PermitRootLogin=yes(建议改为 prohibit-password)" ;; "") log_info "PermitRootLogin 未显式配置(默认 prohibit-password)" ;; *) log_info "PermitRootLogin=$v" ;; esac v=$(awk '/^[[:space:]]*PasswordAuthentication/{print $2; exit}' "$sshd_cfg") case "$v" in no) log_ok "PasswordAuthentication=no(仅密钥登录)" ;; yes) log_info "PasswordAuthentication=yes(允许密码登录)" ;; "") log_info "PasswordAuthentication 未显式配置(默认 yes)" ;; *) log_info "PasswordAuthentication=$v" ;; esac v=$(awk '/^[[:space:]]*Port/{print $2; exit}' "$sshd_cfg") if [ -z "$v" ] || [ "$v" = "22" ]; then log_warn "SSH 使用默认端口 22(易被扫描,建议改高位端口)" else log_ok "SSH 端口: $v(非默认)" fi v=$(awk '/^[[:space:]]*PermitEmptyPasswords/{print $2; exit}' "$sshd_cfg") if [ "$v" = "yes" ]; then log_crit "PermitEmptyPasswords=yes(严重风险)" fi else log_info "未安装 sshd 或无配置读取权限" fi # authorized_keys 审计 log_sub "authorized_keys 审计" local ak_total=0 ak_suspicious=0 while IFS= read -r ak; do [ -z "$ak" ] && continue ak_total=$((ak_total+1)) local owner owner=$(stat -c '%U' "$ak" 2>/dev/null || echo "?") local keys keys=$(grep -c '^[^#]' "$ak" 2>/dev/null || echo 0) keys=${keys:-0} log_info "$ak (owner=$owner, keys=$keys)" # 检查是否有 command= / no-pty 之外的可疑 option if grep -qE '(command=|environment=|permitopen=)' "$ak" 2>/dev/null; then log_warn "$ak 包含受限选项(command/environment/permitopen),请核对" ak_suspicious=$((ak_suspicious+1)) fi done < <(find /root /home -maxdepth 3 -name authorized_keys -type f 2>/dev/null) if [ $ak_total -eq 0 ]; then log_info "未发现 authorized_keys 文件" else log_ok "共发现 $ak_total 个 authorized_keys,可疑选项 $ak_suspicious 处" fi } #==================== 模块 03: 登录与认证日志 ==================== mod_03_login_logs() { log_module "03" "登录与认证日志" # 最近成功登录 log_sub "最近成功登录 (last -n 15)" if has_cmd last; then last -n 15 -F 2>/dev/null | head -20 | while read -r line; do [ -n "$line" ] && log_info "$line" done else log_info "last 命令不可用" fi # 凌晨异常登录(00-06) log_sub "凌晨时段 (00:00-06:00) 登录检查(近 7 天)" local night_logins="" if [ $USE_JOURNAL -eq 1 ] && has_cmd journalctl; then night_logins=$(journalctl --since "7 days ago" _COMM=sshd 2>/dev/null \ | grep -E 'Accepted (password|publickey)' \ | awk '{ split($3,t,":"); if (t[1]+0 >= 0 && t[1]+0 < 6) print }' | head -20) elif [ -n "$AUTH_LOG" ] && [ -r "$AUTH_LOG" ]; then night_logins=$(grep -E 'Accepted (password|publickey)' "$AUTH_LOG" 2>/dev/null \ | awk '{ split($3,t,":"); if (t[1]+0 >= 0 && t[1]+0 < 6) print }' | tail -20) fi if [ -n "$night_logins" ]; then log_warn "发现凌晨登录记录(请核对是否合法运维):" echo "$night_logins" | while read -r line; do log_raw " $line"; done else log_ok "近 7 天无凌晨异常登录" fi # 失败登录统计 log_sub "失败登录统计" local failed_lines="" if [ $USE_JOURNAL -eq 1 ] && has_cmd journalctl; then failed_lines=$(journalctl --since "7 days ago" _COMM=sshd 2>/dev/null | grep -E 'Failed password|Invalid user|authentication failure' || true) elif [ -n "$AUTH_LOG" ] && [ -r "$AUTH_LOG" ]; then failed_lines=$(grep -E 'Failed password|Invalid user|authentication failure' "$AUTH_LOG" 2>/dev/null || true) fi if [ -n "$failed_lines" ]; then local fail_count=0 fail_count=$(printf '%s\n' "$failed_lines" | grep -c . 2>/dev/null) fail_count=${fail_count:-0} log_info "失败登录总数: $fail_count" if [ "$fail_count" -gt 100 ]; then log_warn "失败登录超 100 次,疑似暴力破解" fi log_raw " Top 10 攻击源 IP:" echo "$failed_lines" | grep -oE 'from [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | awk '{print $2}' | sort | uniq -c | sort -rn | head -10 | while read -r line; do log_raw " $line" done log_raw " Top 5 目标用户:" echo "$failed_lines" | grep -oE '(Invalid user|Failed password for) [a-zA-Z0-9_-]+' | awk '{print $NF}' | sort | uniq -c | sort -rn | head -5 | while read -r line; do log_raw " $line" done else log_ok "近 7 天无失败登录记录" fi # btmp(失败登录二进制日志) if [ -s /var/log/btmp ] && has_cmd lastb; then local btmp_count btmp_count=$(lastb -n 1000 2>/dev/null | wc -l | tr -d ' ') btmp_count=${btmp_count:-0} if [ "$btmp_count" -gt 0 ]; then log_info "/var/log/btmp 记录数(近1000): $btmp_count" fi fi } #==================== 模块 04: 进程排查 ==================== mod_04_processes() { log_module "04" "进程排查" log_sub "CPU 使用率 Top 10" ps aux --sort=-%cpu 2>/dev/null | head -11 | while read -r line; do log_info "$line"; done log_sub "内存使用率 Top 10" ps aux --sort=-%mem 2>/dev/null | head -11 | while read -r line; do log_info "$line"; done # 隐藏进程检测(修复:排除自身进程树) log_sub "隐藏进程检测 (ps 与 /proc 对比,已排除脚本自身)" local hidden_found=0 local ps_pids proc_pids ps_pids=$(ps -eo pid= 2>/dev/null | tr -d ' ' | sort -n | uniq) proc_pids=$(ls -1 /proc 2>/dev/null | grep -E '^[0-9]+$' | sort -n | uniq) # /proc 有但 ps 没有 = 疑似隐藏 local diff_pids diff_pids=$(comm -13 <(echo "$ps_pids") <(echo "$proc_pids") 2>/dev/null || true) if [ -n "$diff_pids" ]; then while read -r pid; do [ -z "$pid" ] && continue _is_self_pid "$pid" && continue # 二次确认(可能采集瞬间已退出) sleep 0.05 if [ ! -d "/proc/$pid" ]; then continue; fi if ! ps -p "$pid" >/dev/null 2>&1; then local cmd cmd=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || echo "<无法读取>") log_crit "疑似隐藏进程 PID=$pid CMD=$cmd" add_suspect "/proc/$pid" "隐藏进程(ps不可见但/proc存在)" hidden_found=1 fi done <<< "$diff_pids" fi [ $hidden_found -eq 0 ] && log_ok "未发现隐藏进程" # 可疑路径进程 log_sub "可疑路径进程 (/tmp /var/tmp /dev/shm)" local susp_procs susp_procs=$(ps auxww 2>/dev/null | awk 'NR>1 {for(i=11;i<=NF;i++) printf "%s ", $i; print ""}' \ | grep -E '(^|/)(tmp|var/tmp|dev/shm)/' | head -20 || true) if [ -n "$susp_procs" ]; then log_crit "发现可疑路径进程:" echo "$susp_procs" | while read -r line; do log_raw " $line"; done else log_ok "未发现可疑路径进程" fi # 僵尸进程 log_sub "僵尸进程检测" local zombie_count=0 zombie_count=$(ps -eo stat= 2>/dev/null | grep -c '^Z' 2>/dev/null) zombie_count=${zombie_count:-0} if [ "$zombie_count" -gt 0 ]; then log_warn "发现 $zombie_count 个僵尸进程" ps -eo pid,ppid,stat,cmd 2>/dev/null | awk '$3 ~ /^Z/' | head -10 | while read -r line; do log_raw " $line" done else log_ok "无僵尸进程" fi # 已删除文件但仍运行的进程 log_sub "已删除文件但仍运行的进程(恶意程序常用手段)" local deleted_procs deleted_procs=$(ls -l /proc/*/exe 2>/dev/null | grep -E '\(deleted\)' | head -20 || true) if [ -n "$deleted_procs" ]; then local dp_count dp_count=$(echo "$deleted_procs" | wc -l | tr -d ' ') log_warn "发现 $dp_count 个已删除但仍运行的进程(部分为正常升级,需人工确认):" echo "$deleted_procs" | while read -r line; do local pid pid=$(echo "$line" | grep -oE '/proc/[0-9]+' | grep -oE '[0-9]+') if [ -n "$pid" ] && _is_self_pid "$pid"; then continue; fi log_raw " $line" done else log_ok "未发现已删除但仍运行的可疑进程" fi } #==================== 模块 05: 网络连接排查 ==================== mod_05_network() { log_module "05" "网络连接排查" log_sub "监听端口全景" if has_cmd ss; then ss -tulnp 2>/dev/null | head -50 | while read -r line; do log_info "$line"; done elif has_cmd netstat; then netstat -tulnp 2>/dev/null | head -50 | while read -r line; do log_info "$line"; done else log_warn "ss/netstat 均不可用" fi log_sub "已建立连接(非回环,含内网)" local ext_conns if has_cmd ss; then ext_conns=$(ss -tnp state established 2>/dev/null | tail -n +2 | grep -vE '127\.0\.0\.1|::1' | head -30) else ext_conns=$(netstat -tnp 2>/dev/null | awk '$6=="ESTABLISHED"' | grep -vE '127\.0\.0\.1|::1' | head -30) fi if [ -n "$ext_conns" ]; then echo "$ext_conns" | while read -r line; do log_info "$line"; done else log_info "无已建立的连接" fi log_sub "后门端口检测" local backdoor_ports="4444 5555 6666 7777 8888 9999 12345 31337 1234 4321 5555 6379 27017" local hit=0 for p in $backdoor_ports; do local listen if has_cmd ss; then listen=$(ss -tuln 2>/dev/null | awk '{print $5}' | grep -E "[:.]$p\$" | head -3) else listen=$(netstat -tuln 2>/dev/null | awk '{print $4}' | grep -E "[:.]$p\$" | head -3) fi if [ -n "$listen" ]; then # 6379/27017 是 Redis/Mongo 正常端口,只作提示 if [ "$p" = "6379" ] || [ "$p" = "27017" ]; then log_info "端口 $p 监听中(数据库服务,请核对是否公网可达)" else log_warn "端口 $p 监听中: $listen" hit=1 fi fi done [ $hit -eq 0 ] && log_ok "未发现常见后门端口监听" log_sub "外连 IP Top 15" if has_cmd ss; then local top15 top15=$(ss -tn state established 2>/dev/null | tail -n +2 | awk '{print $5}' \ | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' \ | grep -vE '^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|127\.|169\.254\.)' \ | sort | uniq -c | sort -rn | head -15 || true) if [ -n "$top15" ]; then echo "$top15" | while read -r line; do log_info "$line"; done else log_info "无公网方向外连 IP" fi fi log_sub "内网扫描行为检测(单进程大量连接)" if has_cmd ss; then local scan_procs scan_procs=$(ss -tn state established 2>/dev/null | tail -n +2 | awk '{print $NF}' \ | grep -oE 'pid=[0-9]+' | sort | uniq -c | sort -rn | awk '$1>50' | head -10) if [ -n "$scan_procs" ]; then log_warn "发现单进程连接数 >50 的情况(疑似扫描/爆破):" echo "$scan_procs" | while read -r line; do log_raw " $line"; done else log_ok "未发现单进程大量连接" fi fi } #==================== 模块 06: 定时任务排查 ==================== mod_06_cron() { log_module "06" "定时任务排查" log_sub "系统级 crontab (/etc/crontab)" if [ -r /etc/crontab ]; then grep -vE '^\s*#|^\s*$' /etc/crontab 2>/dev/null | while read -r line; do log_info "$line"; done else log_info "/etc/crontab 不存在或不可读" fi log_sub "/etc/cron.* 目录" for d in /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly /etc/cron.d; do if [ -d "$d" ]; then local cnt cnt=$(find "$d" -maxdepth 1 -type f 2>/dev/null | wc -l | tr -d ' ') log_info "$d: $cnt 个文件" find "$d" -maxdepth 1 -type f 2>/dev/null | while read -r f; do # 检查近期修改 if [ -n "$(find "$f" -mtime -7 2>/dev/null)" ]; then log_warn "$f 近 7 天修改过" fi done fi done log_sub "用户级 crontab" local user_cron_hit=0 for u in $(awk -F: '{print $1}' /etc/passwd 2>/dev/null); do local cron_content cron_content=$(crontab -l -u "$u" 2>/dev/null | grep -vE '^\s*#|^\s*$' || true) if [ -n "$cron_content" ]; then log_info "用户 $u 的 crontab:" echo "$cron_content" | while read -r line; do log_raw " $line"; done user_cron_hit=1 fi done [ $user_cron_hit -eq 0 ] && log_info "无用户级 crontab" log_sub "可疑定时任务命令特征" local susp_cron="" # 1) crontab 条目文件(/etc/crontab、/etc/cron.d/、/var/spool/cron/) # 短命令用非字母数字边界匹配,避免 "nc" 误命中 sessionclean 这类单词 susp_cron=$(grep -rhE '(^|[^[:alnum:]_])(curl|wget|ncat|nc|base64|eval|bash[[:space:]]+-i)([^[:alnum:]_]|$)|/dev/tcp/|python[0-9.]*[[:space:]]+-c[[:space:]]' \ /etc/crontab /etc/cron.d/ /var/spool/cron/ 2>/dev/null | grep -vE '^[[:space:]]*#' | head -20 || true) if [ -n "$susp_cron" ]; then log_crit "定时任务中发现可疑命令:" echo "$susp_cron" | while read -r line; do log_raw " $line" add_suspect "cron" "$line" done fi # 2) cron.* 目录中的脚本文件:发行版维护脚本(包管理文件)正常使用 eval/环境逻辑, # 只对"不属于任何包"的脚本报告可疑命令(用进程替换保持计数在当前 shell) local cron_script_hit=0 for d in /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly; do [ -d "$d" ] || continue while read -r f; do [ -f "$f" ] || continue grep -qE '(^|[^[:alnum:]_])(curl|wget|ncat|nc|base64|bash[[:space:]]+-i)([^[:alnum:]_]|$)|/dev/tcp/' "$f" 2>/dev/null || continue if [ -z "$(file_owned_by_pkg "$f")" ]; then log_crit "非包管理的 cron 脚本包含可疑命令: $f" add_suspect "$f" "cron 脚本可疑命令" cron_script_hit=$((cron_script_hit+1)) grep -nE '(curl|wget|ncat|base64|/dev/tcp/)' "$f" 2>/dev/null | head -3 | while read -r l; do log_raw " $l"; done fi done < <(find "$d" -maxdepth 1 -type f 2>/dev/null) done [ -z "$susp_cron" ] && [ "$cron_script_hit" -eq 0 ] && log_ok "定时任务中未发现可疑命令特征" log_sub "at 任务" if has_cmd atq; then local at_out at_out=$(atq 2>/dev/null || true) if [ -n "$at_out" ]; then log_info "at 队列:" echo "$at_out" | while read -r line; do log_raw " $line"; done else log_ok "at 队列为空" fi else log_info "atq 不可用(未安装 at 工具)" fi } #==================== 模块 07: 启动项与服务排查 ==================== mod_07_services() { log_module "07" "启动项与服务排查" log_sub "已启用的 systemd 服务" if has_cmd systemctl; then local enabled enabled=$(systemctl list-unit-files --type=service --state=enabled --no-pager 2>/dev/null | grep -c '\.service' || echo 0) enabled=${enabled:-0} log_info "已启用服务数: $enabled" systemctl list-unit-files --type=service --state=enabled --no-pager 2>/dev/null \ | grep -vE '^[[:space:]]*$|unit files? listed' | head -40 | while read -r line; do log_info "$line" done else log_info "systemctl 不可用" fi log_sub "失败的服务" if has_cmd systemctl; then local failed failed=$(systemctl --failed --no-pager 2>/dev/null | grep -E '\.service' | head -20) if [ -n "$failed" ]; then log_warn "存在失败服务:" echo "$failed" | while read -r line; do log_raw " $line"; done else log_ok "无失败服务" fi fi log_sub "近 7 天修改的 unit 文件" local recent_units recent_units=$(find /etc/systemd/system /usr/lib/systemd/system /lib/systemd/system \ -type f -name '*.service' -mtime -7 2>/dev/null | head -30 || true) if [ -n "$recent_units" ]; then log_warn "近 7 天修改的 unit 文件(请核对是否合法变更):" echo "$recent_units" | while read -r line; do log_raw " $line" add_suspect "$line" "近期修改的 systemd unit" done else log_ok "近 7 天无 unit 文件修改" fi log_sub "rc.local 与 shell rc 后门检查" local rcl_found=0 for f in /etc/rc.local /etc/rc.d/rc.local; do if [ -f "$f" ]; then rcl_found=1 log_info "$f 内容:" grep -vE '^\s*#|^\s*$' "$f" 2>/dev/null | while read -r line; do log_raw " $line"; done if grep -qE '(curl|wget|nc|base64|/dev/tcp/)' "$f" 2>/dev/null; then log_crit "$f 包含可疑命令" add_suspect "$f" "rc.local 可疑命令" fi fi done [ "$rcl_found" -eq 0 ] && log_info "无 rc.local 文件" local rc_hit=0 for f in /etc/bash.bashrc /etc/profile /root/.bashrc /root/.profile /root/.bash_profile; do if [ -f "$f" ] && [ -r "$f" ]; then if grep -qE '(curl|wget|nc\s+-e|base64\s+-d|/dev/tcp/|python.*pty\.spawn)' "$f" 2>/dev/null; then log_crit "$f 包含可疑启动命令" add_suspect "$f" "shell rc 可疑启动命令" rc_hit=1 grep -nE '(curl|wget|nc\s+-e|base64\s+-d|/dev/tcp/|python.*pty\.spawn)' "$f" 2>/dev/null | head -5 | while read -r line; do log_raw " $line" done fi fi done [ "$rc_hit" -eq 0 ] && log_ok "Shell rc 文件未发现后门命令" log_sub "/etc/profile.d 近期变更" if [ -d /etc/profile.d ]; then local pd_recent pd_recent=$(find /etc/profile.d -type f -mtime -30 2>/dev/null | head -10) if [ -n "$pd_recent" ]; then log_warn "近 30 天 /etc/profile.d 变更:" echo "$pd_recent" | while read -r line; do log_raw " $line"; done else log_ok "无近期变更" fi fi } #==================== 模块 08: 文件系统排查 ==================== mod_08_filesystem() { log_module "08" "文件系统排查" log_sub "SUID/SGID 文件审计" local suid_standard="/usr/bin/passwd /usr/bin/sudo /usr/bin/chsh /usr/bin/chfn /usr/bin/gpasswd /usr/bin/newgrp /usr/bin/mount /usr/bin/umount /usr/bin/su /usr/bin/pkexec /usr/bin/crontab /usr/bin/ssh-agent /usr/lib/openssh/ssh-keysign /usr/lib/dbus-1.0/dbus-daemon-launch-helper /usr/lib/policykit-1/polkit-agent-helper-1 /usr/libexec/dbus-1/dbus-daemon-launch-helper /usr/libexec/openssh/ssh-keysign /usr/libexec/polkit-1/polkit-agent-helper-1 /bin/su /bin/mount /bin/umount /bin/ping /usr/bin/ping /usr/sbin/pppd" local suid_files suid_files=$(find / -xdev \( -perm -4000 -o -perm -2000 \) -type f 2>/dev/null | head -200 || true) if [ -n "$suid_files" ]; then local total total=$(echo "$suid_files" | wc -l | tr -d ' ') log_info "SUID/SGID 文件总数: $total" echo "$suid_files" | while read -r f; do local is_std=0 s for s in $suid_standard; do [ "$f" = "$s" ] && { is_std=1; break; } done [ "$is_std" -eq 1 ] && continue # 非白名单:属于发行版包的降级为信息(Kali 等发行版自带大量工具), # 不属于任何包的才是重点(可疑) if [ -n "$(file_owned_by_pkg "$f")" ]; then log_info "非白名单 SUID/SGID(包管理文件,供核对): $f" else log_warn "非包管理的 SUID/SGID(重点核对): $f" add_suspect "$f" "非包管理 SUID/SGID" fi done else log_info "未发现 SUID/SGID 文件(或权限不足)" fi log_sub "/tmp /var/tmp /dev/shm 可疑文件" for d in /tmp /var/tmp /dev/shm; do if [ -d "$d" ]; then local susp # 排除工具自身、本次输出目录、历史输出目录,避免自扫 susp=$(find "$d" -type f \( -perm -u+x -o -name '*.sh' -o -name '*.py' -o -name '*.pl' \) 2>/dev/null \ | grep -vF -e "$SCRIPT_PATH" \ | grep -vE '/hw_toolkit_[0-9]{8}_[0-9]{6}/' \ | head -30 || true) if [ -n "$susp" ]; then log_warn "$d 下发现可执行/脚本文件:" echo "$susp" | while read -r line; do log_raw " $line" add_suspect "$line" "临时目录可执行文件" done else log_ok "$d 无可疑文件" fi fi done log_sub "敏感目录近 3 天变更 (/etc /usr/bin /usr/sbin /bin /sbin)" if [ $QUICK -eq 0 ]; then local dir_recent_hit=0 for d in /etc /usr/bin /usr/sbin /bin /sbin; do [ -d "$d" ] || continue local recent # resolv.conf/ld.so.cache 由系统服务动态维护,mtime 更新属正常,不计入变更告警 recent=$(find "$d" -maxdepth 2 -type f -mtime -3 2>/dev/null \ | grep -vE '^/etc/resolv\.conf$|^/etc/ld\.so\.cache$' | head -20 || true) if [ -n "$recent" ]; then dir_recent_hit=1 log_warn "$d 近 3 天变更:" echo "$recent" | while read -r line; do log_raw " $line" done fi done [ "$dir_recent_hit" -eq 0 ] && log_info "近 3 天无敏感目录变更" else log_info "--quick 模式跳过深度扫描" fi log_sub "chattr +i 锁定文件检测(Rootkit 常用自我保护)" if has_cmd lsattr; then local locked # lsattr -R 输出含 "目录名:" 头行(NF=1),只取属性行(NF>=2)且第 1 段含 i locked=$(lsattr -R /etc /usr/bin /bin /usr/sbin /sbin 2>/dev/null | awk 'NF>=2 && $1 ~ /i/ {print $2}' | head -20 || true) if [ -n "$locked" ]; then log_warn "发现被 chattr +i 锁定的文件/目录(请确认为业务加固,正常极少需要):" echo "$locked" | while read -r line; do log_raw " $line" add_suspect "$line" "chattr +i 锁定" done else log_ok "未发现异常锁定文件" fi fi } #==================== 模块 09: 内核模块排查 ==================== mod_09_kernel_modules() { log_module "09" "内核模块排查" local mod_count mod_count=$(lsmod 2>/dev/null | tail -n +2 | wc -l | tr -d ' ') mod_count=${mod_count:-0} log_info "已加载内核模块数: $mod_count" log_sub "可疑模块名检测" local susp_mods susp_mods=$(lsmod 2>/dev/null | awk 'NR>1 {print $1}' | grep -iE '(rootkit|hook|hide|diamorphine|reptile|azazel|suterusu|kbeast|ipsecs|symbiote)' || true) if [ -n "$susp_mods" ]; then log_crit "发现疑似 Rootkit 内核模块:" echo "$susp_mods" | while read -r line; do log_raw " $line" add_suspect "kernel_module:$line" "疑似 Rootkit 内核模块" done else log_ok "未发现可疑模块名" fi log_sub "非标准路径加载的模块" if [ -r /proc/modules ]; then local nonstd_mod nonstd_mod=$(awk '{print $1, $NF}' /proc/modules 2>/dev/null | grep -E '/tmp/|/dev/shm/|/var/tmp/' | head -10 || true) if [ -n "$nonstd_mod" ]; then log_crit "发现从可疑路径加载的模块:" echo "$nonstd_mod" | while read -r line; do log_raw " $line"; done else log_ok "所有模块从标准路径加载" fi fi } #==================== 模块 10: 环境变量与库劫持 ==================== mod_10_env_hijack() { log_module "10" "环境变量与库劫持" log_sub "LD_PRELOAD 检测" if [ -n "${LD_PRELOAD:-}" ]; then log_crit "当前 shell 存在 LD_PRELOAD=$LD_PRELOAD" add_suspect "env:LD_PRELOAD" "$LD_PRELOAD" else log_ok "LD_PRELOAD 未设置" fi log_sub "/etc/ld.so.preload 检测" if [ -f /etc/ld.so.preload ]; then local content content=$(grep -vE '^\s*#|^\s*$' /etc/ld.so.preload 2>/dev/null || true) if [ -n "$content" ]; then log_crit "/etc/ld.so.preload 存在有效内容(Rootkit 常用):" echo "$content" | while read -r line; do log_raw " $line" add_suspect "/etc/ld.so.preload" "$line" done else log_info "/etc/ld.so.preload 存在但为空" fi else log_ok "/etc/ld.so.preload 不存在" fi log_sub "/etc/ld.so.conf.d/ 检查" if [ -d /etc/ld.so.conf.d ]; then local recent_conf recent_conf=$(find /etc/ld.so.conf.d -type f -mtime -30 2>/dev/null | head -10) if [ -n "$recent_conf" ]; then log_warn "近 30 天 ld.so.conf.d 变更:" echo "$recent_conf" | while read -r line; do log_raw " $line" grep -v '^#' "$line" 2>/dev/null | while read -r c; do [ -n "$c" ] && log_raw " 内容: $c" done done else log_ok "ld.so.conf.d 无近期变更" fi fi log_sub "PATH 安全检查" local path_val="${PATH:-}" if echo "$path_val" | grep -qE '(^|:)\.(:|$)|(^|:)/tmp(:|$)|(^|:)/var/tmp(:|$)'; then log_warn "PATH 包含当前目录或临时目录(可能被劫持): $path_val" else log_ok "PATH 无当前目录/临时目录劫持" fi log_info "当前 PATH: $path_val" } #==================== 模块 11: 日志完整性 ==================== mod_11_log_integrity() { log_module "11" "日志完整性" log_sub "关键日志文件检查(发行版感知)" local checked=0 missing=0 # 只在应该有 rsyslog 的发行版上检查文件 if [ $USE_JOURNAL -eq 0 ]; then for f in "$AUTH_LOG" "$SYS_LOG" /var/log/wtmp /var/log/btmp /var/log/lastlog; do [ -z "$f" ] && continue checked=$((checked+1)) if [ ! -f "$f" ]; then log_warn "$f 不存在" missing=$((missing+1)) elif [ ! -s "$f" ]; then log_warn "$f 存在但为空" missing=$((missing+1)) else local size mtime size=$(stat -c '%s' "$f" 2>/dev/null || echo "?") mtime=$(stat -c '%y' "$f" 2>/dev/null | cut -d. -f1 || echo "?") log_ok "$f 存在 (大小=$size B, mtime=$mtime)" fi done else log_info "本机使用 journald,跳过传统日志文件检查" fi log_sub "journald 日志状态" if has_cmd journalctl; then local j_size # 例: "Archived and active journals take up 88.5M in the file system." j_size=$(journalctl --disk-usage 2>/dev/null | grep -oE '[0-9.]+[KMGT]?i?B?' | head -1) if [ -n "$j_size" ]; then log_ok "journal 占用: $j_size" else log_info "journal 无日志或不可读(可能为 volatile 模式)" fi else log_info "journalctl 不可用" fi log_sub "日志清空/截断迹象" if [ $USE_JOURNAL -eq 0 ]; then for f in "$AUTH_LOG" "$SYS_LOG"; do [ -z "$f" ] && continue if [ -f "$f" ] && [ ! -s "$f" ]; then log_crit "$f 大小为 0,可能被清空" add_suspect "$f" "日志被清空" fi done fi # wtmp/btmp:空 btmp 很常见(从未记录失败登录时就是 0 字节),区分对待 for f in /var/log/wtmp /var/log/btmp; do [ -e "$f" ] || { log_info "$f 不存在(本机可能使用 journald/wtmpdb 记录登录)"; continue; } if [ ! -s "$f" ]; then if [ "$f" = "/var/log/btmp" ]; then log_info "$f 为空(从未记录失败登录,或已被清空,可结合 lastb 交叉确认)" else log_warn "$f 为空(登录历史异常丢失,建议排查)" fi fi done [ $missing -eq 0 ] && log_ok "关键日志完整" } #==================== 模块 12: Rootkit 痕迹检测 ==================== mod_12_rootkit() { log_module "12" "Rootkit 痕迹检测" log_sub "常见 Rootkit 路径扫描" local rk_paths="/usr/lib/.ice /usr/lib/.bugtraq /tmp/.ice /tmp/.bugtraq /dev/.udev /var/tmp/.cache /usr/include/.sys /etc/.systemd /root/.ssh/hidden /lib/.lib /lib64/.lib" local rk_hit=0 for p in $rk_paths; do if [ -e "$p" ]; then log_crit "发现可疑路径: $p" add_suspect "$p" "常见 Rootkit 路径" rk_hit=1 fi done [ $rk_hit -eq 0 ] && log_ok "未发现常见 Rootkit 路径" log_sub "关键命令完整性检查(静态编译 + 包校验)" local critical_bins="/bin/ls /bin/ps /bin/netstat /bin/ss /usr/bin/find /usr/bin/top /usr/sbin/lsof /bin/cat /bin/grep /usr/bin/whoami /usr/bin/id /bin/kill /usr/sbin/sshd" local verify_tool_ok=0 for bin in $critical_bins; do [ -f "$bin" ] || continue local file_info file_info=$(file "$bin" 2>/dev/null || echo "") if echo "$file_info" | grep -q "statically linked"; then log_warn "$bin 是静态编译(标准发行版通常为动态链接,请核对)" add_suspect "$bin" "关键命令静态编译" fi # 包校验(dpkg/rpm 双路径):内容与包记录不一致 = 疑似被替换 local vout vrc vout=$(verify_pkg_file "$bin") vrc=$? if [ "$vrc" -eq 1 ]; then log_crit "$bin 包校验失败(内容与包记录不一致,疑似被替换):" printf '%s\n' "$vout" | head -5 | while read -r l; do log_raw " $l"; done add_suspect "$bin" "包校验失败" elif [ "$vrc" -eq 0 ]; then verify_tool_ok=$((verify_tool_ok+1)) fi done if [ "$verify_tool_ok" -gt 0 ]; then log_ok "$verify_tool_ok 个关键命令通过包完整性校验" fi log_sub "可疑隐藏目录(点开头 + 系统路径,排除包管理文件)" local hidden_dirs hit=0 hidden_dirs=$(find /usr /lib /lib64 /etc /opt -maxdepth 3 -type d -name '.*' 2>/dev/null \ | grep -vE '/(\.cache|\.local|\.config|\.git|\.svn|\.npm|\.java|\.meta|\.bundle|\.mozilla|\.pki|\.dotnet|\.gnupg|\.themes|\.icons|\.systemPrefs|\.userPrefs|\.dist-info|\.eggs?)$' \ | head -40 || true) if [ -n "$hidden_dirs" ]; then while read -r line; do [ -n "$line" ] || continue if [ -n "$(file_owned_by_pkg "$line")" ]; then log_info "隐藏目录(包管理文件,供核对): $line" else log_warn "隐藏目录(非包管理文件,请人工确认): $line" add_suspect "$line" "非包管理隐藏目录" hit=1 fi done </dev/null \ | grep -vE '^[[:space:]]*$|timers? listed' | head -25 | while read -r line; do log_info "$line" done # 检查非标准 timer(先取 .timer 结尾单元名,再排除常见发行版组件) local nonstd_timers nonstd_timers=$(systemctl list-unit-files --type=timer --state=enabled --no-pager 2>/dev/null \ | awk '{print $1}' | grep -E '\.timer$' \ | grep -vE '^(apt|dpkg|fstrim|logrotate|man-db|systemd|e2scrub|phpsession|snapd|ureadahead|anacron|fwupd|motd|plymouth|raid|smartd|unattended|plocate|sysstat|mdadm|mlocate|btrfs|checkroot|backup|log2ram|dphys)' \ | head -20 || true) if [ -n "$nonstd_timers" ]; then log_warn "发现非标准 timer:" echo "$nonstd_timers" | while read -r line; do log_raw " $line" add_suspect "timer:$line" "非标准 systemd timer" done else log_ok "timer 均为常见发行版组件" fi fi log_sub "/etc/init.d 与 rc*.d 脚本" if [ -d /etc/init.d ]; then local recent_init recent_init=$(find /etc/init.d -type f -mtime -30 2>/dev/null | head -10 || true) if [ -n "$recent_init" ]; then log_warn "/etc/init.d 近 30 天变更:" echo "$recent_init" | while read -r line; do log_raw " $line"; done else log_ok "/etc/init.d 无近期变更" fi fi log_sub "Shell 启动文件后门(全用户)" local shell_files="/etc/profile /etc/bash.bashrc /etc/zsh/zshrc /root/.bashrc /root/.bash_profile /root/.profile /root/.zshrc" for u_home in /home/*; do [ -d "$u_home" ] || continue shell_files="$shell_files $u_home/.bashrc $u_home/.bash_profile $u_home/.profile $u_home/.zshrc" done local shell_hit=0 for f in $shell_files; do [ -r "$f" ] || continue if grep -qE '(curl.*\|\s*(ba)?sh|wget.*\|\s*(ba)?sh|nc\s+-e|base64\s+-d.*\|\s*(ba)?sh|/dev/tcp/|python.*pty\.spawn|bash\s+-i\s+>&)' "$f" 2>/dev/null; then log_crit "$f 包含疑似后门命令:" grep -nE '(curl.*\|\s*(ba)?sh|wget.*\|\s*(ba)?sh|nc\s+-e|base64\s+-d.*\|\s*(ba)?sh|/dev/tcp/|python.*pty\.spawn|bash\s+-i\s+>&)' "$f" 2>/dev/null | head -5 | while read -r line; do log_raw " $line" done add_suspect "$f" "shell 启动文件后门" shell_hit=1 fi done [ $shell_hit -eq 0 ] && log_ok "Shell 启动文件未发现后门" log_sub "/etc/hosts 与 /etc/resolv.conf 变更" for f in /etc/hosts /etc/resolv.conf /etc/nsswitch.conf; do if [ -f "$f" ]; then local m m=$(stat -c '%y' "$f" 2>/dev/null | cut -d. -f1) log_info "$f mtime: $m" case "$f" in /etc/resolv.conf) # 由 NetworkManager/DHCP 动态维护,mtime 频繁更新属正常;内容可疑性见模块 27 log_info "resolv.conf 由 NetworkManager/DHCP 动态维护,mtime 更新属正常(内容检查见模块 27 DNS 劫持)" ;; *) if [ -n "$(find "$f" -mtime -7 2>/dev/null)" ]; then log_warn "$f 近 7 天修改过,请核对" fi ;; esac fi done } #==================== 模块 14: Webshell 深度检测 ==================== mod_14_webshell() { log_module "14" "Webshell 深度检测" # 自动探测 Web 目录 local web_roots="" if [ -n "$WEBDIR" ]; then web_roots="$WEBDIR" else for d in /usr/share/nginx/html /var/www /var/www/html /www/wwwroot /home/www /opt/tomcat/webapps /var/lib/tomcat/webapps /srv/www /usr/share/apache2/default-site; do [ -d "$d" ] && web_roots="$web_roots $d" done fi if [ -z "$web_roots" ]; then log_info "未发现 Web 目录,跳过(可用 --webdir 指定)" return fi log_info "扫描目录: $web_roots" log_sub "已知 Webshell 文件名匹配" local known_shells="c99.php r57.php b374k.php wso.php cmd.php shell.php 1.php 2.php 404.php config.php.bak .shell.php alfa-shell.php indoxploit.php b374k-3.php" local ks_hit=0 for d in $web_roots; do for name in $known_shells; do local found found=$(find "$d" -type f -name "$name" 2>/dev/null | head -5) if [ -n "$found" ]; then log_crit "发现已知 Webshell 文件名: $found" add_suspect "$found" "已知 Webshell 文件名" ks_hit=1 fi done done [ $ks_hit -eq 0 ] && log_ok "未发现已知 Webshell 文件名" log_sub "PHP 高危函数特征" local php_hit=0 for d in $web_roots; do local php_susp php_susp=$(find "$d" -type f \( -name '*.php' -o -name '*.phtml' -o -name '*.php5' -o -name '*.php7' \) 2>/dev/null \ | head -500 \ | xargs grep -lE '(eval\s*\(\s*(base64_decode|gzinflate|str_rot13|\$_(POST|GET|REQUEST|COOKIE))|assert\s*\(\s*\$_|system\s*\(\s*\$_|exec\s*\(\s*\$_|passthru\s*\(\s*\$_|shell_exec\s*\(\s*\$_|popen\s*\(\s*\$_|proc_open\s*\(\s*\$_|preg_replace\s*\(.*/e|\bcreate_function\s*\()' 2>/dev/null | head -20 || true) if [ -n "$php_susp" ]; then php_hit=1 echo "$php_susp" | while read -r line; do log_crit "PHP 高危特征: $line" add_suspect "$line" "PHP Webshell 特征" done fi done [ $php_hit -eq 0 ] && log_ok "未发现 PHP 高危特征" log_sub "JSP 高危特征" local jsp_hit=0 for d in $web_roots; do local jsp_susp jsp_susp=$(find "$d" -type f \( -name '*.jsp' -o -name '*.jspx' -o -name '*.war' \) 2>/dev/null \ | head -300 \ | xargs grep -lE '(Runtime\.getRuntime\(\)\.exec|ProcessBuilder|ClassLoader.*defineClass|javax\.script\.ScriptEngine|new\s+File\s*\(\s*request\.)' 2>/dev/null | head -20 || true) if [ -n "$jsp_susp" ]; then jsp_hit=1 echo "$jsp_susp" | while read -r line; do log_crit "JSP 高危特征: $line" add_suspect "$line" "JSP Webshell 特征" done fi done [ $jsp_hit -eq 0 ] && log_ok "未发现 JSP 高危特征" log_sub "ASP/ASPX 高危特征" local asp_hit=0 for d in $web_roots; do local asp_susp asp_susp=$(find "$d" -type f \( -name '*.asp' -o -name '*.aspx' -o -name '*.asa' -o -name '*.cer' \) 2>/dev/null \ | head -200 \ | xargs grep -lE '(Execute\s*\(\s*Request|WScript\.Shell|Server\.CreateObject|powershell\s+-enc|eval\s*\(\s*Request)' 2>/dev/null | head -20 || true) if [ -n "$asp_susp" ]; then asp_hit=1 echo "$asp_susp" | while read -r line; do log_crit "ASP 高危特征: $line" add_suspect "$line" "ASP Webshell 特征" done fi done [ $asp_hit -eq 0 ] && log_ok "未发现 ASP 高危特征" log_sub "近 7 天修改的 Web 文件" local web_recent_hit=0 for d in $web_roots; do local recent_web recent_web=$(find "$d" -type f \( -name '*.php' -o -name '*.jsp' -o -name '*.asp' -o -name '*.aspx' \) -mtime -7 2>/dev/null | head -20 || true) if [ -n "$recent_web" ]; then web_recent_hit=1 log_warn "$d 近 7 天修改的脚本文件:" echo "$recent_web" | while read -r line; do log_raw " $line" add_suspect "$line" "近期修改的 Web 脚本" done fi done [ "$web_recent_hit" -eq 0 ] && log_info "近 7 天无 Web 脚本文件变更" log_sub "隐藏 Web 文件" local hidden_web_hit=0 for d in $web_roots; do local hidden_web hidden_web=$(find "$d" -type f -name '.*' \( -name '*.php' -o -name '*.jsp' -o -name '*.asp' -o -name '*.phtml' \) 2>/dev/null | head -10 || true) if [ -n "$hidden_web" ]; then hidden_web_hit=1 log_crit "发现隐藏 Web 脚本:" echo "$hidden_web" | while read -r line; do log_raw " $line" add_suspect "$line" "隐藏 Web 脚本" done fi done [ "$hidden_web_hit" -eq 0 ] && log_ok "未发现隐藏 Web 脚本" if [ $QUICK -eq 0 ] && has_cmd python3; then log_sub "高熵值文件检测(疑似加密/混淆 Webshell)" local ent_out="" for d in $web_roots; do # 用引号 heredoc 避免 bash 展开 Python f-string 里的 {} # $d 通过 argv 传给 Python;输出捕获后统一判定,避免空区块 ent_out="$ent_out$(python3 - "$d" <<'PYEOF' 2>/dev/null import os, math, sys root = sys.argv[1] if len(sys.argv) > 1 else '' if not root or not os.path.isdir(root): sys.exit(0) count = 0 for dp, dn, fn in os.walk(root): for f in fn: if not f.endswith(('.php', '.jsp', '.asp', '.aspx', '.phtml')): continue p = os.path.join(dp, f) try: sz = os.path.getsize(p) if sz > 500 * 1024 or sz < 100: continue with open(p, 'rb') as fh: data = fh.read() if not data: continue freq = {} for b in data: freq[b] = freq.get(b, 0) + 1 entropy = -sum((c / len(data)) * math.log2(c / len(data)) for c in freq.values()) if entropy > 7.0: print('[entropy {:.2f}] {}'.format(entropy, p)) count += 1 if count >= 20: sys.exit(0) except Exception: pass PYEOF )" done if [ -n "$ent_out" ]; then printf '%s\n' "$ent_out" | while read -r line; do [ -n "$line" ] && log_warn "高熵值文件(疑似加密/混淆): $line" done else log_ok "未发现高熵值可疑文件" fi else log_info "跳过熵值检测(--quick 或 python3 不可用)" fi log_sub ".htaccess 后门检查" local ht_hit=0 for d in $web_roots; do while read -r f; do [ -n "$f" ] || continue if grep -qE '(AddType\s+application/x-httpd-php|auto_prepend_file|auto_append_file|SetHandler\s+application)' "$f" 2>/dev/null; then ht_hit=1 log_crit ".htaccess 后门: $f" grep -nE '(AddType|auto_prepend_file|auto_append_file|SetHandler)' "$f" 2>/dev/null | head -5 | while read -r line; do log_raw " $line" done add_suspect "$f" ".htaccess 后门" fi done < <(find "$d" -type f -name '.htaccess' 2>/dev/null) done [ "$ht_hit" -eq 0 ] && log_ok "未发现 .htaccess 后门配置" log_sub "Webshell 工具特征(冰蝎/哥斯拉/蚁剑)" local tool_hit=0 for d in $web_roots; do local tool_susp tool_susp=$(find "$d" -type f \( -name '*.php' -o -name '*.jsp' -o -name '*.aspx' \) 2>/dev/null \ | head -500 \ | xargs grep -lE '(@eval\s*\(\s*\$_(POST|REQUEST)\s*\[\s*(pass|ant|x|cmd)|assert\s*\(\s*\$_POST|Behinder|Godzilla|antsword|reGeorg|neo-reGeorg)' 2>/dev/null | head -10 || true) if [ -n "$tool_susp" ]; then tool_hit=1 echo "$tool_susp" | while read -r line; do log_crit "Webshell 工具特征: $line" add_suspect "$line" "冰蝎/哥斯拉/蚁剑特征" done fi done [ "$tool_hit" -eq 0 ] && log_ok "未发现常见 Webshell 工具特征" } #==================== 模块 15: Web 日志攻击特征分析 ==================== mod_15_web_log() { log_module "15" "Web 日志攻击特征分析" local logs="" if [ -n "$LOGDIR" ]; then logs=$(find "$LOGDIR" -maxdepth 2 -type f -name '*access*log*' 2>/dev/null) else for f in /var/log/nginx/access.log /var/log/apache2/access.log /var/log/httpd/access_log /var/log/apache2/other_vhosts_access.log /usr/local/tomcat/logs/localhost_access_log.*.txt /opt/tomcat/logs/localhost_access_log.*.txt; do [ -f "$f" ] && logs="$logs $f" done fi if [ -z "$logs" ]; then log_info "未发现 Web 访问日志(可用 --logdir 指定)" return fi for logfile in $logs; do [ -f "$logfile" ] || continue log_sub "$logfile" # 修复:空文件保护 local lines lines=$(safe_wc_l "$logfile") lines=${lines:-0} log_info "日志行数: $lines" if [ "$lines" -eq 0 ]; then log_info "日志为空,跳过分析" continue fi # SQL 注入 local sqli sqli=$(grep -icE "(union.*select|select.*from|sleep\(|benchmark\(|' or 1=1|/\*|\.\./\.\./)" "$logfile" 2>/dev/null || echo 0) sqli=${sqli:-0} if [ "$sqli" -gt 0 ]; then log_warn "疑似 SQL 注入特征: $sqli 条" else log_ok "无 SQL 注入特征" fi # XSS local xss xss=$(grep -icE "(/dev/null || echo 0) xss=${xss:-0} if [ "$xss" -gt 0 ]; then log_warn "疑似 XSS 特征: $xss 条" else log_ok "无 XSS 特征" fi # 命令注入 local cmdi cmdi=$(grep -icE "(\|\s*cat|;\s*whoami|;\s*id|&&\s*wget|/etc/passwd|\`.*\`|\\\$\(.*\))" "$logfile" 2>/dev/null || echo 0) cmdi=${cmdi:-0} if [ "$cmdi" -gt 0 ]; then log_crit "疑似命令注入特征: $cmdi 条" else log_ok "无命令注入特征" fi # 目录扫描 local dirscan dirscan=$(grep -icE "(\.\./|\.\.\\\\|phpmyadmin|/wp-admin|/wp-login|/admin|/manager/html|\.git/config|\.env)" "$logfile" 2>/dev/null || echo 0) dirscan=${dirscan:-0} if [ "$dirscan" -gt 5 ]; then log_warn "疑似目录扫描: $dirscan 条" fi # 敏感文件访问 local sensitive sensitive=$(grep -cE "\.(env|git/config|svn/entries|DS_Store|bak|sql|zip|tar\.gz)" "$logfile" 2>/dev/null || echo 0) sensitive=${sensitive:-0} if [ "$sensitive" -gt 0 ]; then log_warn "敏感文件访问尝试: $sensitive 条" fi # 可疑 User-Agent local susp_ua susp_ua=$(grep -icE "(sqlmap|nikto|nmap|masscan|acunetix|nessus|burpsuite|dirbuster|gobuster|wpscan|zgrab)" "$logfile" 2>/dev/null || echo 0) susp_ua=${susp_ua:-0} if [ "$susp_ua" -gt 0 ]; then log_warn "扫描器 UA 出现: $susp_ua 条" grep -iE "(sqlmap|nikto|nmap|masscan|acunetix|nessus|burpsuite|dirbuster|gobuster|wpscan|zgrab)" "$logfile" 2>/dev/null | head -5 | while read -r line; do log_raw " $line" done fi # 4xx/5xx 分布 log_raw " HTTP 状态码分布:" awk '{for(i=1;i<=NF;i++) if ($i ~ /^[1-5][0-9][0-9]$/ && $(i-1) ~ /"$/) {print $i; break}}' "$logfile" 2>/dev/null \ | sort | uniq -c | sort -rn | head -10 | while read -r line; do log_raw " $line"; done # 暴力破解特征(同 IP 大量 401/403) local brute brute=$(awk '$9 ~ /^(401|403)$/ {print $1}' "$logfile" 2>/dev/null | sort | uniq -c | sort -rn | awk '$1>20' | head -10) if [ -n "$brute" ]; then log_warn "疑似暴力破解 IP:" echo "$brute" | while read -r line; do log_raw " $line"; done fi # 非标准 HTTP 方法 local nonstd_method nonstd_method=$(grep -oE '"(PUT|DELETE|TRACE|OPTIONS|PATCH|PROPFIND|MOVE|COPY|MKCOL)' "$logfile" 2>/dev/null | sort | uniq -c | head -10) if [ -n "$nonstd_method" ]; then log_info "非标准 HTTP 方法统计:" echo "$nonstd_method" | while read -r line; do log_raw " $line"; done fi done } #==================== 模块 16: 异常外联与 C2 通信 ==================== mod_16_c2() { log_module "16" "异常外联与 C2 通信检测" log_sub "DNS 隧道特征(长子域名 / 高频查询)" if [ $USE_JOURNAL -eq 1 ] && has_cmd journalctl; then local long_dns long_dns=$(journalctl --since "24 hours ago" 2>/dev/null \ | grep -oE '[a-zA-Z0-9-]{40,}\.[a-zA-Z0-9.-]+' | head -20 || true) if [ -n "$long_dns" ]; then log_warn "发现超长域名查询(疑似 DNS 隧道):" echo "$long_dns" | while read -r line; do log_raw " $line"; done else log_ok "无超长域名查询" fi elif [ -r /var/log/syslog ]; then local long_dns2 long_dns2=$(grep -oE '[a-zA-Z0-9-]{40,}\.[a-zA-Z0-9.-]+' /var/log/syslog 2>/dev/null | head -20 || true) [ -n "$long_dns2" ] && log_warn "syslog 中发现超长域名" || log_ok "无超长域名查询" fi log_sub "常见 C2 端口检测" local c2_ports="50050 50051 8443 443 8080 1234 4444 5555 6666 7777 9999 31337 12345 8000 8888 9001 9002" if has_cmd ss; then local c2_conn c2_conn=$(ss -tn state established 2>/dev/null | tail -n +2 | awk '{print $5}' | while read -r addr; do local port port=$(echo "$addr" | awk -F: '{print $NF}') for cp in $c2_ports; do [ "$port" = "$cp" ] && echo "$addr" && break done done | head -20) if [ -n "$c2_conn" ]; then log_warn "发现外连 C2 常用端口:" echo "$c2_conn" | while read -r line; do log_raw " $line"; done else log_ok "未发现 C2 常用端口外连" fi fi log_sub "代理隧道工具进程 (FRP/nps/Chisel/reGeorg/EW/lcx)" local tunnel_procs tunnel_procs=$(ps auxww 2>/dev/null | grep -iE '(frpc|frps|nps|npc|chisel|regeorg|neoregorg|ew\.|lcx|termite|proxychains|ss-local|ss-server|v2ray|xray|trojan|naiveproxy)' | grep -v grep | head -20 || true) if [ -n "$tunnel_procs" ]; then log_crit "发现代理隧道工具进程:" echo "$tunnel_procs" | while read -r line; do log_raw " $line" add_suspect "process" "$line" done else log_ok "未发现代理隧道工具进程" fi log_sub "隧道工具文件路径检测" local tunnel_files tunnel_files=$(find /tmp /var/tmp /dev/shm /root /home /opt /usr/local/bin -maxdepth 4 -type f \ \( -name 'frpc*' -o -name 'frps*' -o -name 'nps*' -o -name 'npc*' -o -name 'chisel*' -o -name 'reGeorg*' -o -name 'ew_*' -o -name 'lcx*' \) 2>/dev/null | head -20 || true) if [ -n "$tunnel_files" ]; then log_crit "发现隧道工具文件:" echo "$tunnel_files" | while read -r line; do log_raw " $line" add_suspect "$line" "隧道工具文件" done else log_ok "未发现隧道工具文件" fi log_sub "非工作时段 (00-06) 网络活动快照" local hour hour=$(date +%H) if [ "$hour" -ge 0 ] && [ "$hour" -lt 6 ]; then log_info "当前处于凌晨时段 ($hour:00),附当前已建立连接快照(仅为采集时点参考,非异常判定):" if has_cmd ss; then ss -tnp state established 2>/dev/null | tail -n +2 | head -20 | while read -r line; do log_raw " $line" done fi else log_info "当前为工作时段 ($hour:00),跳过凌晨快照采集" fi } #==================== 模块 17: 威胁情报 IP 检测 ==================== mod_17_threat_intel() { log_module "17" "威胁情报 IP 检测(微步在线)" # 提取外部 IP local ext_ips="" if has_cmd ss; then ext_ips=$(ss -tn state established 2>/dev/null | tail -n +2 | awk '{print $5}' \ | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' \ | grep -vE '^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|127\.|169\.254\.|0\.|255\.)' \ | sort -u | head -50) fi if [ -z "$ext_ips" ]; then log_info "无外部连接 IP" return fi local ip_count ip_count=$(echo "$ext_ips" | wc -l | tr -d ' ') log_info "外部 IP 数量: $ip_count" # 无 API Key:降级为列表输出 if [ -z "$API_KEY" ]; then log_info "未提供 --api-key,降级为 IP 列表输出(可复制到微步/奇安信/VirusTotal 手工查询):" echo "$ext_ips" | while read -r ip; do log_raw " $ip" done log_info "在线查询: https://x.threatbook.cn/ 或 https://ti.qianxin.com/" return fi # 有 API Key:调用微步 API if ! has_cmd curl; then log_warn "curl 不可用,无法调用 API" return fi log_sub "调用微步在线 API 查询" local malicious=0 suspicious=0 clean=0 unknown=0 while read -r ip; do [ -z "$ip" ] && continue local resp resp=$(curl -sS -m 10 "https://api.threatbook.cn/v3/scene/ip_reputation?apikey=${API_KEY}&resource=${ip}&lang=zh" 2>/dev/null || echo "") if [ -z "$resp" ]; then unknown=$((unknown+1)) continue fi local judgment judgment=$(echo "$resp" | grep -oE '"judgments":\[[^]]*\]' | head -1) if echo "$judgment" | grep -qiE '(恶意|Malicious|C2|僵尸网络|Botnet|扫描|Exploit)'; then log_crit "恶意 IP: $ip 判定: $judgment" add_suspect "$ip" "微步判定恶意 IP" malicious=$((malicious+1)) elif echo "$judgment" | grep -qiE '(可疑|Suspicious|IDC|代理|Proxy|Tor)'; then log_warn "可疑 IP: $ip 判定: $judgment" suspicious=$((suspicious+1)) else clean=$((clean+1)) fi sleep 0.3 # 限速保护 done <<< "$ext_ips" log_info "统计: 恶意=$malicious 可疑=$suspicious 正常=$clean 未知=$unknown" } #==================== 模块 18: 漏洞快速排查 ==================== mod_18_vuln() { log_module "18" "漏洞快速排查" log_sub "高危端口未授权服务探测(本地)" local svc_ports="6379:Redis 27017:MongoDB 9200:Elasticsearch 2181:ZooKeeper 11211:Memcached 5432:PostgreSQL 3306:MySQL 1433:MSSQL 1521:Oracle 5984:CouchDB 9000:MinIO/PHP-FPM 2375:DockerAPI 2376:DockerAPI-TLS 10250:Kubelet" local port_hit=0 for entry in $svc_ports; do local port svc port="${entry%%:*}" svc="${entry##*:}" local listening="" if has_cmd ss; then listening=$(ss -tuln 2>/dev/null | awk '{print $5}' | grep -E "[:.]$port\$" | head -1) fi if [ -n "$listening" ]; then port_hit=1 # 判断是否 0.0.0.0 暴露 if echo "$listening" | grep -qE '(0\.0\.0\.0|\*|\[::\]):'; then log_warn "$svc 端口 $port 全网卡监听: $listening(请核对是否需要)" else log_info "$svc 端口 $port 本地监听: $listening" fi fi done [ "$port_hit" -eq 0 ] && log_ok "未发现高危端口服务监听(本地探测)" log_sub "高危组件版本检测" # Shiro (通过 cookie rememberMe 特征判断,本地无法直接测) # Log4j / Fastjson 等常见高危组件 jar local jar_hit=0 local log4j_files log4j_files=$(find / -xdev \( -name 'log4j-core-*.jar' -o -name 'log4j-*.jar' \) -type f 2>/dev/null | head -10) if [ -n "$log4j_files" ]; then jar_hit=1 log_warn "发现 Log4j jar(请核对版本是否 >= 2.17.1 以修复 Log4Shell):" echo "$log4j_files" | while read -r line; do log_raw " $line" add_suspect "$line" "Log4j 待版本核对" done fi # Fastjson / Struts2 / Spring local fastjson fastjson=$(find / -xdev -name 'fastjson-*.jar' -type f 2>/dev/null | head -5) if [ -n "$fastjson" ]; then jar_hit=1 log_warn "发现 Fastjson jar(核对是否 >= 1.2.83)" echo "$fastjson" | while read -r l; do log_raw " $l"; done fi [ "$jar_hit" -eq 0 ] && log_ok "未发现 Log4j/Fastjson 等常见高危组件 jar" log_sub "SSL/TLS 证书过期检查" local cert_dirs="/etc/ssl/certs /etc/pki/tls/certs /etc/nginx/ssl /etc/letsencrypt/live" local cert_checked=0 cert_issue=0 for d in $cert_dirs; do [ -d "$d" ] || continue while read -r cert; do [ -n "$cert" ] || continue local end_date end_date=$(openssl x509 -in "$cert" -noout -enddate 2>/dev/null | cut -d= -f2) if [ -n "$end_date" ]; then cert_checked=$((cert_checked+1)) local end_ts now_ts diff_days end_ts=$(date -d "$end_date" +%s 2>/dev/null || echo 0) now_ts=$(date +%s) if [ "$end_ts" -gt 0 ]; then diff_days=$(( (end_ts - now_ts) / 86400 )) if [ "$diff_days" -lt 0 ]; then cert_issue=1 log_crit "证书已过期 ($diff_days 天): $cert" elif [ "$diff_days" -lt 30 ]; then cert_issue=1 log_warn "证书即将过期 ($diff_days 天): $cert" fi fi fi done < <(find "$d" -maxdepth 3 -type f \( -name '*.crt' -o -name '*.pem' \) 2>/dev/null | head -20) done if [ "$cert_checked" -eq 0 ]; then log_info "未发现待检查的 .crt/.pem 证书文件" elif [ "$cert_issue" -eq 0 ]; then log_ok "已检查 $cert_checked 个证书,均在有效期内(或到期 >30 天)" fi log_sub "敏感路径泄露探测(Web 目录)" local web_roots="" if [ -n "$WEBDIR" ]; then web_roots="$WEBDIR" else for d in /usr/share/nginx/html /var/www /var/www/html /www/wwwroot; do [ -d "$d" ] && web_roots="$web_roots $d" done fi local leak_hit=0 for d in $web_roots; do for leak in .git .svn .env .DS_Store backup.zip www.zip web.tar.gz phpinfo.php info.php test.php; do local found found=$(find "$d" -maxdepth 4 -name "$leak" 2>/dev/null | head -3) if [ -n "$found" ]; then leak_hit=1 log_crit "敏感路径泄露: $found" add_suspect "$found" "Web 敏感路径泄露" fi done done if [ -z "$web_roots" ]; then log_info "未发现 Web 目录(可用 --webdir 指定)" elif [ "$leak_hit" -eq 0 ]; then log_ok "未发现常见敏感路径泄露" fi } #==================== 模块 19: Docker 容器安全检查 ==================== mod_19_docker() { log_module "19" "Docker 容器安全检查" if ! has_cmd docker; then log_info "未安装 Docker,跳过" return fi if ! docker info >/dev/null 2>&1; then log_warn "Docker 已安装但无法连接 daemon(可能未启动或权限不足)" return fi log_sub "运行中容器列表" docker ps --format 'table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}' 2>/dev/null | head -30 | while read -r line; do log_info "$line" done log_sub "特权容器检测" local priv priv=$(docker ps -q 2>/dev/null | while read -r cid; do local is_priv is_priv=$(docker inspect "$cid" --format '{{.HostConfig.Privileged}}' 2>/dev/null) [ "$is_priv" = "true" ] && docker inspect "$cid" --format '{{.Name}} (privileged=true, image={{.Config.Image}})' done) if [ -n "$priv" ]; then log_crit "发现特权容器(存在逃逸风险):" echo "$priv" | while read -r line; do log_raw " $line" add_suspect "docker:$line" "特权容器" done else log_ok "无特权容器" fi log_sub "挂载敏感目录的容器" docker ps -q 2>/dev/null | while read -r cid; do local mounts mounts=$(docker inspect "$cid" --format '{{range .Mounts}}{{.Source}}:{{.Destination}} {{end}}' 2>/dev/null) if echo "$mounts" | grep -qE '(/etc:/|/root:/|/var/run/docker\.sock|/:/host|/proc:/)'; then local name name=$(docker inspect "$cid" --format '{{.Name}}' 2>/dev/null) log_crit "容器 $name 挂载敏感路径: $mounts" add_suspect "docker:$name" "敏感挂载 $mounts" fi done log_sub "Docker daemon 配置安全" if [ -f /etc/docker/daemon.json ]; then log_info "/etc/docker/daemon.json 内容:" cat /etc/docker/daemon.json 2>/dev/null | while read -r line; do log_raw " $line"; done if grep -qE '"hosts".*tcp://0\.0\.0\.0' /etc/docker/daemon.json 2>/dev/null; then log_crit "Docker API 通过 TCP 0.0.0.0 暴露(严重风险,可直接接管宿主机)" fi fi # 2375/2376 端口 if has_cmd ss; then local docker_tcp docker_tcp=$(ss -tuln 2>/dev/null | awk '{print $5}' | grep -E ':2375$' | head -1) if [ -n "$docker_tcp" ]; then log_crit "Docker daemon TCP 2375 端口开放: $docker_tcp" fi fi } #==================== 模块 20: Kubernetes 安全检查 ==================== mod_20_k8s() { log_module "20" "Kubernetes 安全检查" if ! has_cmd kubectl; then log_info "未安装 kubectl,跳过" return fi if ! kubectl cluster-info >/dev/null 2>&1; then log_info "kubectl 无法连接集群,跳过" return fi log_sub "集群可达性" kubectl cluster-info 2>/dev/null | head -5 | while read -r line; do log_info "$line"; done log_sub "特权 Pod 检测" local priv_pods priv_pods=$(kubectl get pods --all-namespaces -o json 2>/dev/null \ | python3 -c " import json, sys try: d = json.load(sys.stdin) for p in d.get('items', []): ns = p['metadata']['namespace'] name = p['metadata']['name'] for c in p['spec'].get('containers', []): sc = c.get('securityContext', {}) if sc.get('privileged'): print(f'{ns}/{name} container={c[\"name\"]} privileged=true') except Exception as e: pass " 2>/dev/null || true) if [ -n "$priv_pods" ]; then log_crit "发现特权 Pod:" echo "$priv_pods" | while read -r line; do log_raw " $line" add_suspect "k8s:$line" "特权 Pod" done else log_ok "无特权 Pod" fi log_sub "默认 ServiceAccount 绑定 cluster-admin" local sa_bind sa_bind=$(kubectl get clusterrolebindings -o json 2>/dev/null \ | python3 -c " import json, sys try: d = json.load(sys.stdin) for b in d.get('items', []): rn = b.get('roleRef', {}).get('name', '') if rn != 'cluster-admin': continue for s in b.get('subjects', []) or []: if s.get('name') == 'default': print(f\"{b['metadata']['name']} -> {s.get('namespace')}/default\") except: pass " 2>/dev/null || true) if [ -n "$sa_bind" ]; then log_crit "默认 SA 绑定 cluster-admin:" echo "$sa_bind" | while read -r line; do log_raw " $line" add_suspect "k8s:$line" "默认 SA 绑 cluster-admin" done else log_ok "默认 SA 未绑定 cluster-admin" fi log_sub "kubeconfig 权限" for kc in /root/.kube/config /etc/kubernetes/admin.conf "$HOME/.kube/config"; do if [ -f "$kc" ]; then local perm perm=$(stat -c '%a' "$kc" 2>/dev/null) if [ "$perm" != "600" ] && [ "$perm" != "400" ]; then log_warn "$kc 权限过宽: $perm(建议 600)" else log_ok "$kc 权限: $perm" fi fi done } #==================== 模块 21: 反弹 Shell 特征进程 ==================== mod_21_reverse_shell() { log_module "21" "反弹 Shell 特征进程检测" log_sub "进程命令行匹配" local rs_procs rs_procs=$(ps auxww 2>/dev/null | grep -iE \ '(bash\s+-i\s+>&|/dev/tcp/[0-9]|nc\s+(-e|-c)\s+/bin|ncat\s+(-e|-c)|socat.*EXEC|socat.*TCP|openssl\s+s_client.*\|\s*(ba)?sh|python[0-9.]*\s+-c\s+.*socket|perl\s+-e\s+.*socket|ruby\s+-rsocket|php\s+-r\s+.*fsockopen|mkfifo\s+/tmp/|telnet.*\|\s*/bin/sh)' \ | grep -v grep | grep -v "$SCRIPT_NAME" | head -20 || true) if [ -n "$rs_procs" ]; then log_crit "发现疑似反弹 shell 进程:" echo "$rs_procs" | while read -r line; do log_raw " $line" add_suspect "process" "疑似反弹 shell: $line" done else log_ok "未发现反弹 shell 特征进程" fi log_sub "已建立的可疑端口外连" if has_cmd ss; then local susp_out susp_out=$(ss -tnp state established 2>/dev/null | tail -n +2 \ | grep -vE '(127\.0\.0\.1|::1|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)' \ | awk '{print $4, $5, $NF}' | head -20) if [ -n "$susp_out" ]; then log_info "外部已建立连接(人工确认是否合法):" echo "$susp_out" | while read -r line; do log_raw " $line"; done else log_ok "未发现对外已建立连接(内网/回环已排除)" fi fi } #==================== 模块 22: PAM 完整性 ==================== mod_22_pam() { log_module "22" "PAM 完整性检查" log_sub "/etc/pam.d 近期修改(30 天)" if [ -d /etc/pam.d ]; then local recent_pam recent_pam=$(find /etc/pam.d -type f -mtime -30 2>/dev/null | head -20) if [ -n "$recent_pam" ]; then log_warn "近 30 天 PAM 配置变更(请核对是否合法):" echo "$recent_pam" | while read -r line; do log_raw " $line" local m m=$(stat -c '%y' "$line" 2>/dev/null | cut -d. -f1) log_raw " mtime: $m" done else log_ok "PAM 配置无近期变更" fi fi log_sub "pam_unix.so 完整性" local pam_libs="/lib/x86_64-linux-gnu/security/pam_unix.so /lib64/security/pam_unix.so /usr/lib/x86_64-linux-gnu/security/pam_unix.so /usr/lib64/security/pam_unix.so" local seen_libs="" for lib in $pam_libs; do [ -f "$lib" ] || continue local rl rl=$(readlink -f "$lib" 2>/dev/null || echo "$lib") # 去重:/lib/... 与 /usr/lib/... 在 usrmerge 系统上是同一文件 case " $seen_libs " in *" $rl "*) continue ;; esac seen_libs="$seen_libs $rl" local hash mtime hash=$(sha256sum "$rl" 2>/dev/null | awk '{print $1}') mtime=$(stat -c '%y' "$rl" 2>/dev/null | cut -d. -f1) log_info "$rl" log_raw " sha256: $hash" log_raw " mtime: $mtime" # 包管理器校验(dpkg/rpm 双路径,区分"不属于包"与"校验失败") local vout vrc vout=$(verify_pkg_file "$rl") vrc=$? if [ "$vrc" -eq 1 ]; then log_crit "$rl 包校验失败,疑似被替换:" printf '%s\n' "$vout" | head -5 | while read -r l; do log_raw " $l"; done add_suspect "$rl" "PAM 库包校验失败" elif [ "$vrc" -eq 0 ]; then log_ok "$rl 通过包完整性校验" fi # 静态编译检测 if file "$rl" 2>/dev/null | grep -q "statically linked"; then log_crit "$rl 是静态编译(正常应为动态库)" fi done log_sub "PAM 配置中的可疑模块" if [ -d /etc/pam.d ]; then local crit_hits warn_hits # 高危:引用 /tmp、/dev/shm、/var/tmp 下的 .so(过滤注释行) crit_hits=$(grep -rhE '^[^#]*\.so' /etc/pam.d/ 2>/dev/null \ | grep -E '(/tmp/|/dev/shm/|/var/tmp/)[^[:space:]]*\.so' | head -10 || true) # 需核对:pam_exec/pam_debug/pam_test(有合法业务用法,但属非常规) warn_hits=$(grep -rhE '^[^#]*pam_(exec|debug|test)\.so' /etc/pam.d/ 2>/dev/null \ | grep -vE '^[[:space:]]*#' | head -10 || true) if [ -n "$crit_hits" ]; then log_crit "PAM 配置引用高危路径模块:" echo "$crit_hits" | while read -r line; do log_raw " $line" add_suspect "/etc/pam.d" "引用高危路径 PAM 模块" done fi if [ -n "$warn_hits" ]; then log_warn "PAM 配置引用 pam_exec/pam_debug 类模块(请核对业务合法性):" echo "$warn_hits" | while read -r line; do log_raw " $line"; done fi if [ -z "$crit_hits" ] && [ -z "$warn_hits" ]; then log_ok "PAM 配置无可疑模块引用" fi fi } #==================== 模块 23: SSH 密钥与 authorized_keys 审计 ==================== mod_23_ssh_keys() { log_module "23" "SSH 密钥与 authorized_keys 审计" log_sub "authorized_keys 近期变更(30 天)" local ak_recent ak_recent=$(find /root /home -maxdepth 3 -name authorized_keys -type f -mtime -30 2>/dev/null | head -20) if [ -n "$ak_recent" ]; then log_warn "近 30 天 authorized_keys 变更:" echo "$ak_recent" | while read -r f; do log_raw " $f (mtime=$(stat -c '%y' "$f" 2>/dev/null | cut -d. -f1))" log_raw " 内容:" grep -v '^#' "$f" 2>/dev/null | while read -r line; do [ -n "$line" ] && log_raw " $line" done add_suspect "$f" "近期变更的 authorized_keys" done else log_ok "authorized_keys 无近期变更" fi log_sub "私钥文件权限与暴露检查" local priv_keys priv_keys=$(find /root /home -maxdepth 4 -type f \( -name 'id_rsa' -o -name 'id_dsa' -o -name 'id_ecdsa' -o -name 'id_ed25519' -o -name '*.pem' -o -name '*.key' \) 2>/dev/null | head -30) if [ -n "$priv_keys" ]; then echo "$priv_keys" | while read -r k; do local perm owner perm=$(stat -c '%a' "$k" 2>/dev/null) owner=$(stat -c '%U' "$k" 2>/dev/null) # 私钥应该 600 或 400 if [ "${perm: -2}" != "00" ]; then log_crit "私钥权限过宽: $k (perm=$perm, owner=$owner)" add_suspect "$k" "私钥权限过宽" else log_ok "$k (perm=$perm, owner=$owner)" fi done else log_info "未发现用户私钥文件" fi log_sub "SSH 主机密钥指纹" for hk in /etc/ssh/ssh_host_*_key.pub; do [ -f "$hk" ] || continue local fp fp=$(ssh-keygen -lf "$hk" 2>/dev/null) log_info "$hk: $fp" done log_sub "known_hosts 异常" local kh_hit=0 for kh in /root/.ssh/known_hosts /etc/ssh/known_hosts; do if [ -f "$kh" ]; then kh_hit=1 local m m=$(stat -c '%y' "$kh" 2>/dev/null | cut -d. -f1) log_info "$kh mtime: $m (条目 $(wc -l < "$kh" 2>/dev/null | tr -d ' ') 行)" fi done while read -r kh; do [ -n "$kh" ] || continue kh_hit=1 local m m=$(stat -c '%y' "$kh" 2>/dev/null | cut -d. -f1) log_info "$kh mtime: $m" done < <(find /home -maxdepth 3 -name known_hosts 2>/dev/null) [ "$kh_hit" -eq 0 ] && log_info "未发现 known_hosts 文件(无出站 SSH 连接记录)" } #==================== 模块 24: 敏感凭据文件搜索 ==================== mod_24_credentials() { log_module "24" "敏感凭据文件搜索" log_sub "配置文件中的明文凭据" # 分隔符覆盖 key: val / key=val / 'key' => 'val' / define('KEY', 'val') 四种常见写法 local patterns='(password|passwd|pwd|secret|api[_-]?key|access[_-]?key|token|credential)["'"'"']?[[:space:]]*(=>|[:=]|,)[[:space:]]*["'"'"']?[^[:space:]"'"'"']{6,}' if [ $QUICK -eq 1 ]; then log_info "--quick 模式跳过深度凭据搜索" return fi local cred_hit=0 # 用数组避免通配符在当前目录被 glob 展开 local -a SEARCH_DIRS=(/root /home /etc /opt /var/www /srv) for d in "${SEARCH_DIRS[@]}"; do [ -d "$d" ] || continue local files # 一次性 find(-o 组合)+ sort -u 去重,避免 application.yml 同时命中 *.yml files=$(find "$d" -maxdepth 5 -type f \( \ -name '*.conf' -o -name '*.cfg' -o -name '*.ini' -o -name '*.yaml' -o -name '*.yml' \ -o -name '*.json' -o -name '*.properties' -o -name '*.env' -o -name '.env*' \ -o -name 'config.php' -o -name 'wp-config.php' -o -name 'database.yml' -o -name 'settings.py' \ \) 2>/dev/null | sort -u | head -200) [ -z "$files" ] && continue echo "$files" | while read -r f; do [ -r "$f" ] || continue local hit # 过滤注释行:教程/示例里的 "#...password: xxx" 不算凭据泄露 hit=$(grep -inE "$patterns" "$f" 2>/dev/null \ | grep -vE '^[0-9]+:[[:space:]]*(#|;|//)' \ | grep -viE '(password_hash|hashed|encrypted|\*\*\*|xxx|placeholder|example|changeme|your_|<|dummy)' | head -3) if [ -n "$hit" ]; then log_warn "$f 疑似明文凭据:" echo "$hit" | while read -r line; do # 脱敏显示:保留 key 名,值替换为 ***MASKED*** # 注意:grep 检测用 -i 大小写不敏感,这里必须覆盖全小写/全大写/首字母大写 # 三种写法,否则 DB_PASSWORD 这类全大写 key 会被检出但值未脱敏(泄露) local masked masked=$(printf '%s' "$line" | sed -E "s/(PASSWORD|[Pp]assword|PASSWD|[Pp]asswd|PWD|[Pp]wd|SECRET|[Ss]ecret|TOKEN|[Tt]oken|CREDENTIAL|[Cc]redential|API[_-]?KEY|[Aa]pi[_-]?[Kk]ey|ACCESS[_-]?KEY|[Aa]ccess[_-]?[Kk]ey)([\"']?[[:space:]]*(=>|[:=]|,)[[:space:]]*)([\"'][^\"']*[\"']|[^[:space:]\"']+)/\\1\\2***MASKED***/g") if [ "$masked" = "$line" ]; then # 兜底:关键词大小写形态超出枚举范围时(如 passWord), # 掩码行尾最后一个分隔符之后的内容,宁可多遮不可泄露 masked=$(printf '%s' "$line" | sed -E 's/([=:]>?[[:space:]]*)[^=:[:space:]]+$/\1***MASKED***/') fi log_raw " $masked" done add_suspect "$f" "疑似明文凭据" fi done done log_sub "云凭据文件" local cf_hit=0 for cf in /root/.aws/credentials /root/.aws/config /home/*/.aws/credentials /root/.aliyun/config.json /root/.docker/config.json /home/*/.docker/config.json /root/.kube/config /root/.ossutilconfig /root/.cos.conf; do for f in $cf; do [ -f "$f" ] || continue cf_hit=1 local perm perm=$(stat -c '%a' "$f" 2>/dev/null) log_info "$f (perm=$perm)" if [ "${perm: -2}" != "00" ]; then log_warn "$f 权限过宽 ($perm)" fi done done [ "$cf_hit" -eq 0 ] && log_info "未发现常见云凭据文件(aws/aliyun/docker/kube 等)" log_sub "SSH 私钥文件全盘扫描" local all_keys all_keys=$(find / -xdev -type f \( -name 'id_rsa' -o -name 'id_ed25519' -o -name 'id_ecdsa' -o -name 'id_dsa' \) 2>/dev/null | head -20) if [ -n "$all_keys" ]; then echo "$all_keys" | while read -r k; do local perm owner perm=$(stat -c '%a' "$k" 2>/dev/null) owner=$(stat -c '%U' "$k" 2>/dev/null) log_info "$k (perm=$perm, owner=$owner)" done else log_info "未发现 RSA/Ed25519 等常见命名的私钥文件" fi } #==================== 模块 25: 时间戳异常 (Timestomping) ==================== mod_25_timestomping() { log_module "25" "时间戳异常检测 (Timestomping)" if [ $QUICK -eq 1 ]; then log_info "--quick 模式跳过 timestomping 扫描" return fi log_sub "mtime 早于 ctime 的可执行文件(时间戳伪造特征)" local ts_hit=0 # 检测 find 是否支持 -printf(GNU 扩展) local find_has_printf=0 if find /tmp -maxdepth 0 -printf '' 2>/dev/null; then find_has_printf=1 fi # 说明: # 包管理安装的文件 mtime(包内构建时间) < ctime(安装时间) 属正常 → 跳过包所属文件 # 近期(90 天)解压部署的 tar 包也会 mtime < ctime → 只取 ctime 在 90 天窗口内的 # 剩余命中才值得人工核对 local now_ts now_ts=$(date +%s) for d in /usr/bin /usr/sbin /bin /sbin /usr/local/bin /opt; do [ -d "$d" ] || continue local susp="" if [ $find_has_printf -eq 1 ]; then susp=$(find "$d" -maxdepth 3 -type f -executable -printf '%p|%T@|%C@\n' 2>/dev/null \ | awk -F'|' -v now="$now_ts" '$2 < $3 - 3600 && $3 > now - 7776000 {print $1"|"$2"|"$3}' \ | head -30) else # BusyBox/Alpine 兜底:用 stat 逐个检查 susp=$(find "$d" -maxdepth 3 -type f -executable 2>/dev/null | head -200 | while read -r f; do local mt ct mt=$(stat -c '%Y' "$f" 2>/dev/null || echo 0) ct=$(stat -c '%Z' "$f" 2>/dev/null || echo 0) if [ "$mt" -gt 0 ] && [ "$ct" -gt 0 ] && [ "$mt" -lt $((ct - 3600)) ] && [ "$ct" -gt $((now_ts - 7776000)) ]; then echo "$f|$mt|$ct" fi done | head -30) fi [ -z "$susp" ] && continue local d_hit=0 while IFS='|' read -r f mt ct; do [ -n "$f" ] || continue # 包管理文件跳过(模块 12 已用包校验覆盖此类) [ -n "$(file_owned_by_pkg "$f")" ] && continue local mt_h ct_h mt_h=$(date -d "@${mt%%.*}" '+%Y-%m-%d %H:%M' 2>/dev/null || echo "$mt") ct_h=$(date -d "@${ct%%.*}" '+%Y-%m-%d %H:%M' 2>/dev/null || echo "$ct") log_warn "疑似时间戳异常: $f (mtime=$mt_h 早于 ctime=$ct_h;安装器/解压部署也会产生该特征,请人工核对)" add_suspect "$f" "疑似 timestomping" d_hit=$((d_hit+1)) [ "$d_hit" -ge 5 ] && break done </dev/null || echo "") if [ -n "$ipt" ]; then echo "$ipt" | head -60 | while read -r line; do log_info "$line"; done # 检查 INPUT 链默认策略 local input_policy input_policy=$(echo "$ipt" | grep -E '^Chain INPUT' | grep -oE 'policy [A-Z]+' | awk '{print $2}') case "$input_policy" in ACCEPT) log_warn "INPUT 链默认策略为 ACCEPT(建议 DROP + 白名单)" ;; DROP|REJECT) log_ok "INPUT 链默认策略: $input_policy" ;; *) log_info "INPUT 链默认策略: ${input_policy:-未知}" ;; esac # 检查是否有 0.0.0.0/0 放行到高危端口(用词边界避免 22 误匹配 2200) local risky_accept risky_accept=$(echo "$ipt" | grep -E 'ACCEPT' | grep -E '0\.0\.0\.0/0' \ | grep -E 'dpt:(22|23|3389|6379|27017|9200|11211|2375|10250)(\s|$)' | head -10) if [ -n "$risky_accept" ]; then log_warn "发现对公网开放的高危端口 ACCEPT 规则:" echo "$risky_accept" | while read -r line; do log_raw " $line"; done fi else log_info "iptables 无规则或不可读" fi fi log_sub "nftables 规则" if has_cmd nft; then local nft_out nft_out=$(nft list ruleset 2>/dev/null | head -40 || echo "") if [ -n "$nft_out" ]; then echo "$nft_out" | while read -r line; do log_info "$line"; done else log_info "nftables 无规则" fi fi log_sub "firewalld 状态" if has_cmd firewall-cmd; then if firewall-cmd --state >/dev/null 2>&1; then log_info "firewalld 运行中" local fw_zone fw_svcs fw_ports fw_zone=$(firewall-cmd --get-default-zone 2>/dev/null) fw_svcs=$(firewall-cmd --list-services 2>/dev/null) fw_ports=$(firewall-cmd --list-ports 2>/dev/null) log_info "默认区域: $fw_zone" log_info "允许服务: $fw_svcs" log_info "允许端口: $fw_ports" else log_info "firewalld 未运行" fi else log_info "firewalld 未安装(常见于 Debian 系,使用 iptables/nftables)" fi log_sub "混杂模式网卡" local promisc promisc=$(ip link show 2>/dev/null | grep -i PROMISC | head -5 || true) if [ -n "$promisc" ]; then log_warn "发现混杂模式网卡(可能被嗅探):" echo "$promisc" | while read -r line; do log_raw " $line"; done else log_ok "无网卡处于混杂模式" fi } #==================== 模块 27: DNS/hosts 劫持检测 ==================== mod_27_dns_hijack() { log_module "27" "DNS 与 hosts 劫持检测" log_sub "/etc/resolv.conf" if [ -f /etc/resolv.conf ]; then local m nameservers m=$(stat -c '%y' /etc/resolv.conf 2>/dev/null | cut -d. -f1) log_info "mtime: $m(由 NetworkManager/DHCP 动态维护,更新属正常)" nameservers=$(grep -E '^nameserver' /etc/resolv.conf 2>/dev/null) echo "$nameservers" | while read -r line; do [ -z "$line" ] && continue log_info "$line" local ns_ip ns_ip=$(echo "$line" | awk '{print $2}') case "$ns_ip" in 8.8.8.8|8.8.4.4|1.1.1.1|1.0.0.4|9.9.9.9|114.114.114.114|223.5.5.5|119.29.29.29|180.76.76.76) log_ok "$ns_ip 是公共 DNS" ;; 127.0.0.53|127.0.0.1|::1) log_info "$ns_ip 是本机回环 DNS(systemd-resolved/dnsmasq 等本地服务)" ;; 10.*|192.168.*|172.1[6-9].*|172.2[0-9].*|172.3[01].*) log_info "$ns_ip 是内网 DNS" ;; *) log_warn "$ns_ip 非常见公共/内网 DNS,请核对是否合法" ;; esac done if [ -s /etc/resolv.conf ] && ! grep -qE '^nameserver' /etc/resolv.conf 2>/dev/null; then log_warn "/etc/resolv.conf 无 nameserver 配置(DNS 配置可能异常,请核对)" fi # 检测追加内容(正常由 NM 整体重写;手写 nameserver 出现在注释块之后需核对) if grep -nE '^nameserver' /etc/resolv.conf 2>/dev/null | awk -F: '{print $2}' | head -1 | grep -q .; then local first_ns_line first_ns_line=$(grep -nE '^nameserver' /etc/resolv.conf 2>/dev/null | head -1 | cut -d: -f1) if [ "${first_ns_line:-0}" -gt 10 ]; then log_warn "/etc/resolv.conf 中 nameserver 出现在第 $first_ns_line 行(位置靠后,建议人工核对是否有手工追加)" fi fi fi log_sub "/etc/hosts 劫持检测" if [ -f /etc/hosts ]; then local hosts_m hosts_lines hosts_m=$(stat -c '%y' /etc/hosts 2>/dev/null | cut -d. -f1) hosts_lines=$(grep -vE '^\s*#|^\s*$' /etc/hosts 2>/dev/null | wc -l | tr -d ' ') log_info "/etc/hosts mtime: $hosts_m, 有效条目: ${hosts_lines:-0}" # 常见被劫持的域名 local hijack_domains="(google|facebook|twitter|github|apple|amazon|microsoft|windowsupdate|aliyun|tencent|baidu|qq|taobao|jd)\.(com|cn|net|org)" local suspicious_hosts suspicious_hosts=$(grep -vE '^\s*#|^\s*$' /etc/hosts 2>/dev/null \ | grep -iE "$hijack_domains" \ | grep -vE '^\s*(127\.|::1|0\.0\.0\.0)' | head -10 || true) if [ -n "$suspicious_hosts" ]; then log_crit "/etc/hosts 存在可疑域名映射:" echo "$suspicious_hosts" | while read -r line; do log_raw " $line" add_suspect "/etc/hosts" "hosts 劫持: $line" done else log_ok "/etc/hosts 无可疑域名映射" fi # 显示所有非注释条目供人工审核 log_raw " 完整 hosts 条目:" grep -vE '^\s*#|^\s*$' /etc/hosts 2>/dev/null | while read -r line; do log_raw " $line" done fi log_sub "/etc/nsswitch.conf" if [ -f /etc/nsswitch.conf ]; then local nss_m nss_m=$(stat -c '%y' /etc/nsswitch.conf 2>/dev/null | cut -d. -f1) log_info "mtime: $nss_m" grep -E '^hosts:' /etc/nsswitch.conf 2>/dev/null | while read -r line; do log_info "$line"; done if [ -n "$(find /etc/nsswitch.conf -mtime -7 2>/dev/null)" ]; then log_warn "/etc/nsswitch.conf 近 7 天修改过" fi fi } #==================== 模块 28: Shell History 异常 ==================== mod_28_history() { log_module "28" "Shell History 异常检测" log_sub "HISTFILE 环境变量" if [ -n "${HISTFILE:-}" ]; then log_info "当前 HISTFILE: $HISTFILE" case "$HISTFILE" in /dev/null) log_crit "HISTFILE 指向 /dev/null(历史被丢弃)" ;; /tmp/*|/var/tmp/*|/dev/shm/*) log_warn "HISTFILE 指向临时目录: $HISTFILE" ;; *) log_ok "HISTFILE 路径正常" ;; esac else log_info "当前会话未设置 HISTFILE(非交互 shell 常见,请以各用户 history 文件为准)" fi log_sub "各用户 history 文件" for hf in /root/.bash_history /root/.zsh_history /home/*/.bash_history /home/*/.zsh_history; do [ -f "$hf" ] || continue local size mtime perm owner size=$(stat -c '%s' "$hf" 2>/dev/null) mtime=$(stat -c '%y' "$hf" 2>/dev/null | cut -d. -f1) perm=$(stat -c '%a' "$hf" 2>/dev/null) owner=$(stat -c '%U' "$hf" 2>/dev/null) log_info "$hf (owner=$owner, perm=$perm, size=${size:-0}B, mtime=$mtime)" if [ "${size:-0}" -eq 0 ]; then log_warn "$hf 大小为 0(历史为空:新账号或未使用交互式 shell 时属正常;若有使用记录则疑似被清空,请核对)" fi # 软链接到 /dev/null if [ -L "$hf" ]; then local target target=$(readlink "$hf") if [ "$target" = "/dev/null" ]; then log_crit "$hf 是软链接指向 /dev/null(永久丢弃历史)" add_suspect "$hf" "history 软链 /dev/null" fi fi # 检查是否包含危险命令 local danger danger=$(grep -icE '(curl.*\|\s*(ba)?sh|wget.*\|\s*(ba)?sh|nc\s+-e|bash\s+-i\s+>&|/dev/tcp/|rm\s+-rf\s+/|mkfs|dd\s+if=.*of=/dev/|chmod\s+777\s+/|chattr\s+\+i|history\s+-c|>\s*~/.bash_history|shred.*\.bash_history)' "$hf" 2>/dev/null || echo 0) danger=${danger:-0} if [ "$danger" -gt 0 ]; then log_warn "$hf 包含 $danger 条危险命令特征:" grep -inE '(curl.*\|\s*(ba)?sh|wget.*\|\s*(ba)?sh|nc\s+-e|bash\s+-i\s+>&|/dev/tcp/|rm\s+-rf\s+/|mkfs|dd\s+if=.*of=/dev/|chmod\s+777\s+/|chattr\s+\+i|history\s+-c)' "$hf" 2>/dev/null | tail -10 | while read -r line; do log_raw " $line" done fi done log_sub "auditd 状态" if has_cmd auditctl; then if auditctl -s >/dev/null 2>&1; then local audit_enabled audit_enabled=$(auditctl -s 2>/dev/null | grep -E '^enabled' | awk '{print $2}') log_info "auditd enabled=$audit_enabled" local rules rules=$(auditctl -l 2>/dev/null | head -20) if [ -n "$rules" ]; then log_info "auditd 规则:" echo "$rules" | while read -r line; do log_raw " $line"; done else log_warn "auditd 无规则(建议配置关键文件/命令监控)" fi else log_info "auditd 未运行" fi else log_info "auditd 未安装" fi } #==================== 简报生成 ==================== generate_summary() { local end_ts elapsed end_ts=$(date +%s) elapsed=$((end_ts - START_TS)) local elapsed_str elapsed_str="$((elapsed/60))分$((elapsed%60))秒" # 计数以字段文件为准(管道子 shell 中的计数不会丢失) CRIT_COUNT=$(safe_wc_l "$OUTPUT_DIR/.cnt_crit") WARN_COUNT=$(safe_wc_l "$OUTPUT_DIR/.cnt_warn") OK_COUNT=$(safe_wc_l "$OUTPUT_DIR/.cnt_ok") INFO_COUNT=$(safe_wc_l "$OUTPUT_DIR/.cnt_info") local hostname ip hostname=$(hostname 2>/dev/null) ip=$(ip -4 addr show 2>/dev/null | awk '/inet /{print $2}' | grep -v '127.0.0.1' | head -1) local status if [ $CRIT_COUNT -gt 0 ]; then status="存在高危告警 - 需立即处置" elif [ $WARN_COUNT -gt 5 ]; then status="存在较多警告 - 建议人工复核" elif [ $WARN_COUNT -gt 0 ]; then status="存在少量警告 - 关注即可" else status="未发现异常" fi local suspect_count=0 [ -f "$SUSPECT_FILES" ] && suspect_count=$(wc -l < "$SUSPECT_FILES" 2>/dev/null | tr -d ' ') suspect_count=${suspect_count:-0} local skipped="" [ $SKIP_WEB -eq 1 ] && skipped="$skipped - Web 层 (14-15)" [ $SKIP_LOG -eq 1 ] && skipped="$skipped - Web 日志 (15)" [ $SKIP_VULN -eq 1 ] && skipped="$skipped - 漏洞扫描 (18)" [ $SKIP_TI -eq 1 ] && skipped="$skipped - 威胁情报 (17)" [ $SKIP_CONTAINER -eq 1 ] && skipped="$skipped - 容器层 (19-20)" [ $SKIP_EXTRA -eq 1 ] && skipped="$skipped - 扩展模块 (21-28)" if [ -z "$skipped" ]; then skipped=" 无" else skipped="${skipped#$'\n'}" # 去掉拼接产生的首个换行 fi cat > "$SUMMARY_FILE" <