安全扫描工具部署与使用文档

环境信息

项目 内容
操作系统 CentOS 7 (阿里云 ECS)
服务器配置 2 核 8GB 内存

一、Nikto

项目 内容
版本 2.6.0 (LW 2.5)
部署方式 Git 克隆 + Perl 依赖
安装路径 /opt/nikto/
命令路径 /usr/local/bin/nikto
用途 Web 服务器漏洞扫描(安全头检测、配置审计、敏感文件发现)

安装命令:

sudo yum install -y perl-JSON perl-Time-Piece perl-Time-HiRes \
    perl-XML-Writer perl-Net-SSLeay perl-IO-Socket-SSL

cd /opt
sudo git clone https://github.com/sullo/nikto.git
sudo ln -s /opt/nikto/program/nikto.pl /usr/local/bin/nikto
nikto -Version

使用示例:

nikto -h http://target.com -maxtime 600s -timeout 10
nikto -h http://target.com -o report.html -Format htm

二、afrog

项目 内容
版本 3.5.3
PoC 版本 0.5.29
PoC 数量 1648 个
部署方式 预编译二进制 + PoC 仓库
安装路径 /opt/afrog/
命令路径 /usr/local/bin/afrog
PoC 路径 ~/.config/afrog/pocs/
用途 PoC 漏洞验证(SQL 注入、RCE、文件上传、未授权访问等)

安装命令:

cd /opt
AFROG_VERSION="3.5.3"
wget https://github.com/zan8in/afrog/releases/download/v${AFROG_VERSION}/afrog_${AFROG_VERSION}_linux_amd64.zip
unzip afrog_${AFROG_VERSION}_linux_amd64.zip -d afrog
chmod +x afrog/afrog
sudo ln -s /opt/afrog/afrog /usr/local/bin/afrog

cd /tmp
git clone https://github.com/zan8in/afrog-pocs.git
mkdir -p ~/.config/afrog/pocs
cp -r /tmp/afrog-pocs/CVE /tmp/afrog-pocs/CNOV /tmp/afrog-pocs/default-pwd \
      /tmp/afrog-pocs/disclosure /tmp/afrog-pocs/fingerprinting \
      /tmp/afrog-pocs/unauthorized /tmp/afrog-pocs/version \
      /tmp/afrog-pocs/vulnerability \
      ~/.config/afrog/pocs/

afrog -version
find ~/.config/afrog/pocs/ -name "*.yaml" | wc -l

使用示例:

afrog -t http://target.com -curated off -S critical,high,medium
afrog -t http://target.com -curated off -o report.html -S critical,high,medium

注意事项:

  • 需加 -curated off 参数才能加载本地 PoC。
  • afrog 二进制可能被 ClamAV 误报,ClamAV 扫描时需排除 afrog 目录。

三、ClamAV

项目 内容
版本 0.103.11
病毒库版本 28008
病毒特征数 3,627,426
部署方式 yum 安装 + cvdupdate 更新病毒库
命令路径 /usr/bin/clamscan
病毒库路径 /var/lib/clamav/
用途 病毒/木马/恶意文件扫描

安装命令:

sudo yum install -y clamav clamav-update clamav-server clamav-server-systemd
pip3 install cvdupdate
sudo cvd update
sudo cp ~/.cvdupdate/database/*.cvd /var/lib/clamav/
sudo chown clamupdate:clamupdate /var/lib/clamav/*.cvd
clamscan --version

使用示例:

clamscan -r --infected /home /var/www /tmp
clamscan -r --infected --log=/tmp/clamav.log /home /var/www /tmp

注意事项:

  • CentOS 7 yum 仓库版本被官方 CDN 标记为过期,使用 cvdupdate 绕过。
  • 病毒库更新命令:sudo cvd update

四、统一扫描脚本

项目 内容
脚本路径 /opt/scripts/security-scan.sh
目标文件 /opt/scripts/targets.txt
报告目录 /var/log/security-scans/
版本 v4.2
集成工具 Nikto + afrog + ClamAV + SSH 远程检查

4.1 功能说明

脚本支持 SSH 远程免安装扫描,通过密钥或密码认证登录目标服务器,执行以下检查:

检查模块 内容
系统信息 OS、内核、资源使用、内核安全配置
SSH 安全 root 登录策略、授权密钥检查、后门检测
网络风险 危险端口暴露、SSH 暴力破解检测、外网连接
定时任务 可疑 crontab 检测(curl/wget/base64/eval)
Web 指纹 服务器版本识别、CVE 匹配、安全响应头、SSL 证书
恶意文件 高风险文件收集、哈希比对、ClamAV 远程扫描

4.2 目标列表配置

文件路径: /opt/scripts/targets.txt

# ============================================
# 安全扫描目标列表
# 格式: 每行一个目标,# 开头为注释
# ============================================

# ---- 密钥登录 ----
# root@192.168.0.1                        # 默认密钥 + 默认端口(22)
# root@192.168.0.2 -i /root/.ssh/id_rsa   # 指定密钥
# root@192.168.0.3 -p 50022               # 指定端口

# ---- 密码登录 ----
# root:password@192.168.0.4                # 密码 + 默认端口
# root:password@192.168.0.5:50022          # 密码 + 指定端口

格式说明:

格式 说明 示例
user@host 密钥登录,默认端口 22 root@192.168.0.1
user@host -i key 密钥登录,指定私钥 root@192.168.0.1 -i /root/.ssh/id_rsa
user@host -p port 密钥登录,指定端口 root@192.168.0.1 -p 50022
user:pass@host 密码登录,默认端口 22 root:mypass@192.168.0.1
user:pass@host:port 密码登录,指定端口 root:mypass@192.168.0.1:50022

4.3 使用方法

# 查看帮助
./security-scan.sh --help

# 扫描
./security-scan.sh -l targets.txt

# 带全局选项
./security-scan.sh -l targets.txt \
    -i /root/.ssh/id_rsa \
    -p 22 \
    -o /var/log/security-scans

# 跳过部分检查模块
./security-scan.sh -l targets.txt --skip-malware --skip-web

4.4 命令选项

选项 说明 默认值
-l, --list FILE 目标列表文件(必需) -
-i, --ssh-key FILE 全局默认 SSH 私钥 -
-p, --ssh-port PORT 全局默认 SSH 端口 22
-o, --output DIR 报告输出目录 /var/log/security-scans
--skip-malware 跳过恶意文件扫描 -
--skip-sysinfo 跳过系统信息收集 -
--skip-network 跳过网络风险检查 -
--skip-web 跳过 Web 指纹扫描 -
--skip-cron 跳过定时任务检查 -
--max-file-size MB 拉取文件最大尺寸 50
--max-file-count N 每台最多拉取文件数 200
--help 显示帮助 -

4.5 报告结构

/var/log/security-scans/20260602_164750/
├── summary.txt
├── root@192_168_1_100_22/
│   ├── sysinfo.txt
│   ├── ssh-security.txt
│   ├── network.txt
│   ├── crontab.txt
│   ├── web.txt
│   ├── filelist.txt
│   └── pulled-files/

4.6 扫描报告示例

========================================
  安全扫描汇总报告 v4.2
  时间: 2026-06-02 16:47:56
  目标: 1  成功: 1  失败: 0
  总耗时: 6秒
========================================

### [1/1] root@106.13.3.111:50022 [password] ###
  [!] 危险端口: 1
  [!] SSH暴力破解: 10次
  [!] Web CVE: 8

========================================
  报告: /var/log/security-scans/20260602_164750
========================================

五、扫描脚本完整代码

#!/bin/bash
# Security Scan Suite v4.2 (远程免安装扫描版 - 支持密钥/密码认证)

# ==================== 颜色定义 ====================
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
RED='\033[0;31m'
BOLD='\033[1m'
NC='\033[0m'

# ==================== 工具检查 ====================
check_tool() {
    if ! command -v "$1" &>/dev/null; then
        echo -e "${RED}[✗] 未找到: $1${NC}"
        return 1
    fi
    return 0
}

echo -e "${CYAN}[*] 检查本地依赖工具...${NC}"
MISSING=0
for tool in curl nmap openssl md5sum sha256sum ssh scp; do
    check_tool "$tool" || MISSING=1
done
[ "$MISSING" -eq 1 ] && { echo -e "${RED}[✗] 缺少必要工具${NC}"; exit 1; }

SSHPASS_AVAILABLE=0
command -v sshpass &>/dev/null && SSHPASS_AVAILABLE=1 && echo -e "${GREEN}[✓] sshpass: 可用${NC}" || echo -e "${YELLOW}[!] sshpass 未安装${NC}"

CLAMAV_LOCAL=0
command -v clamscan &>/dev/null && CLAMAV_LOCAL=1 && echo -e "${GREEN}[✓] 本地 ClamAV: 可用${NC}" || echo -e "${YELLOW}[!] ClamAV 未安装${NC}"
echo -e "${GREEN}[✓] 核心工具检查通过${NC}"

# ==================== CVE 知识库 ====================
declare -a CVE_DB=(
    "nginx|1.27.4|CVE-2024-7347|HIGH|HTTP/3 QUIC 内存越界读取"
    "nginx|1.27.3|CVE-2026-25836|HIGH|缓存投毒漏洞"
    "nginx|1.25.4|CVE-2024-24989|HIGH|mp4_module 整数溢出"
    "nginx|1.25.4|CVE-2024-24990|HIGH|mp4_module 内存越界读取"
    "nginx|1.25.3|CVE-2023-44487|HIGH|HTTP/2 Rapid Reset DoS"
    "nginx|1.23.5|CVE-2022-41741|HIGH|mp4_module 内存越界写入"
    "nginx|1.23.5|CVE-2022-41742|HIGH|mp4_module 内存越界读取"
    "nginx|1.17.8|CVE-2021-23017|HIGH|DNS 解析器越界写入"
    "nginx|1.13.8|CVE-2017-7529|HIGH|整数溢出越界读取"
    "Apache|2.4.63|CVE-2025-26603|HIGH|mod_proxy 跨源本地读取"
    "Apache|2.4.62|CVE-2024-47252|HIGH|mod_ssl 无限循环 DoS"
    "Apache|2.4.61|CVE-2024-38476|HIGH|路径遍历访问控制绕过"
    "Apache|2.4.58|CVE-2023-45853|CRITICAL|minizip 整数溢出"
    "Apache|2.4.52|CVE-2021-44790|CRITICAL|mod_lua RCE"
    "Apache|2.4.51|CVE-2021-42013|CRITICAL|路径遍历绕过"
    "Apache|2.4.49|CVE-2021-41773|CRITICAL|路径遍历任意文件读取"
    "Apache|2.4.39|CVE-2019-0211|CRITICAL|特权提升"
    "OpenSSL|3.4.1|CVE-2025-26603|HIGH|跨源资源本地读取"
    "OpenSSL|3.4.0|CVE-2024-12797|HIGH|X.509 验证绕过"
    "OpenSSL|3.3.0|CVE-2024-6119|HIGH|X.509 名称检查 DoS"
    "OpenSSL|3.0.12|CVE-2023-6237|HIGH|RSA 解密 DoS"
    "OpenSSL|1.1.1l|CVE-2021-3711|CRITICAL|SM2 缓冲区溢出"
    "OpenSSL|1.1.1j|CVE-2021-3450|HIGH|X.509 验证绕过"
    "Microsoft-IIS|10.0|CVE-2022-21907|CRITICAL|HTTP 协议栈 RCE"
    "Microsoft-IIS|7.5|CVE-2017-7269|CRITICAL|WebDAV 缓冲区溢出"
)

version_compare() {
    local v1="$1" v2="$2"
    [ "$v1" = "$v2" ] && return 0
    local IFS='.'
    read -ra V1P <<< "$v1"
    read -ra V2P <<< "$v2"
    local mx=${#V1P[@]}
    [ ${#V2P[@]} -gt $mx ] && mx=${#V2P[@]}
    for ((i=0; i<mx; i++)); do
        local a=${V1P[$i]:-0} b=${V2P[$i]:-0}
        (( a > b )) && return 1
        (( a < b )) && return 2
    done
    return 0
}

# ==================== 帮助信息 ====================
show_help() {
    cat <<'EOF'
用法: ./security-scan.sh [选项] -l targets.txt

  目标列表格式 (每行一台):
    user@host                     密钥登录
    user@host -i key_file         密钥 (指定密钥)
    user@host -p port             密钥 (指定端口)
    user:password@host            密码登录
    user:password@host:port       密码 (指定端口)

  选项:
    -l, --list FILE            目标列表 (必需)
    -i, --ssh-key FILE         全局默认私钥
    -p, --ssh-port PORT        全局默认 SSH 端口 (默认 22)
    -o, --output DIR           报告输出目录
    --skip-malware             跳过恶意文件扫描
    --skip-sysinfo             跳过系统信息
    --skip-network             跳过网络风险
    --skip-web                 跳过 Web 指纹
    --skip-cron                跳过定时任务
    --max-file-size MB         拉取文件最大尺寸 (默认 50)
    --max-file-count N         每台最多拉取文件 (默认 200)
    --help                     显示帮助
EOF
    exit 0
}

# ==================== 参数解析 ====================
TARGET_FILE=""
SSH_KEY=""
SSH_PORT=22
OUTPUT_DIR="/var/log/security-scans"
SKIP_MALWARE=0
SKIP_SYSINFO=0
SKIP_NETWORK=0
SKIP_WEB=0
SKIP_CRON=0
MAX_FILE_SIZE=50
MAX_FILE_COUNT=200

while [ $# -gt 0 ]; do
    case "$1" in
        -l|--list)        TARGET_FILE="$2"; shift 2 ;;
        -i|--ssh-key)     SSH_KEY="$2"; shift 2 ;;
        -p|--ssh-port)    SSH_PORT="$2"; shift 2 ;;
        -o|--output)      OUTPUT_DIR="$2"; shift 2 ;;
        --skip-malware)   SKIP_MALWARE=1; shift ;;
        --skip-sysinfo)   SKIP_SYSINFO=1; shift ;;
        --skip-network)   SKIP_NETWORK=1; shift ;;
        --skip-web)       SKIP_WEB=1; shift ;;
        --skip-cron)      SKIP_CRON=1; shift ;;
        --max-file-size)  MAX_FILE_SIZE="$2"; shift 2 ;;
        --max-file-count) MAX_FILE_COUNT="$2"; shift 2 ;;
        --help)           show_help ;;
        *)                echo -e "${RED}[✗] 未知选项: $1${NC}"; exit 1 ;;
    esac
done

if [ -z "$TARGET_FILE" ] || [ ! -f "$TARGET_FILE" ]; then
    echo -e "${RED}[✗] 请通过 -l 指定目标列表${NC}"
    exit 1
fi

# ==================== 解析目标列表 ====================
declare -a T_USER=()
declare -a T_PASS=()
declare -a T_HOST=()
declare -a T_PORT=()
declare -a T_KEY=()
declare -a T_AUTH=()

LINE_NUM=0
while IFS= read -r rawline; do
    LINE_NUM=$((LINE_NUM + 1))
    local_line=$(echo "$rawline" | sed 's/#.*//' | xargs)
    [ -z "$local_line" ] && continue

    if echo "$local_line" | grep -qP '^[^:]+:[^@]+@'; then
        AUTH_PART=$(echo "$local_line" | grep -oP '^[^:]+:[^@]+')
        p_user=$(echo "$AUTH_PART" | cut -d: -f1)
        p_pass=$(echo "$AUTH_PART" | cut -d: -f2)
        HOST_PART=$(echo "$local_line" | sed 's/^[^@]*@//')
        if echo "$HOST_PART" | grep -qP ':\d+$'; then
            p_host=$(echo "$HOST_PART" | grep -oP '^[^:]+')
            p_port=$(echo "$HOST_PART" | grep -oP ':\K\d+$')
        else
            p_host="$HOST_PART"
            p_port="$SSH_PORT"
        fi
        p_key=""
        p_auth="password"
        if [ "$SSHPASS_AVAILABLE" -eq 0 ]; then
            echo -e "${RED}[✗] 第${LINE_NUM}行需要密码但 sshpass 未安装${NC}"
            continue
        fi
    else
        local_uh=$(echo "$local_line" | grep -oP '^\S+')
        p_user=$(echo "$local_uh" | cut -d@ -f1)
        p_host=$(echo "$local_uh" | cut -d@ -f2)
        p_pass=""
        p_port="$SSH_PORT"
        p_key="$SSH_KEY"
        p_auth="key"
        local_k=$(echo "$local_line" | grep -oP '\-i \K\S+')
        [ -n "$local_k" ] && p_key="$local_k"
        local_p=$(echo "$local_line" | grep -oP '\-p \K\d+')
        [ -n "$local_p" ] && p_port="$local_p"
    fi

    if [ -z "$p_user" ] || [ -z "$p_host" ]; then
        echo -e "${RED}[✗] 第${LINE_NUM}行格式错误${NC}"
        continue
    fi

    T_USER+=("$p_user")
    T_PASS+=("$p_pass")
    T_HOST+=("$p_host")
    T_PORT+=("$p_port")
    T_KEY+=("$p_key")
    T_AUTH+=("$p_auth")
done < "$TARGET_FILE"

TOTAL=${#T_HOST[@]}
if [ "$TOTAL" -eq 0 ]; then
    echo -e "${RED}[✗] 无有效目标${NC}"
    exit 1
fi

# ==================== 初始化 ====================
DATE=$(date +%Y%m%d_%H%M%S)
SCAN_DIR="${OUTPUT_DIR}/${DATE}"
mkdir -p "$SCAN_DIR"
SUCCESS=0
FAIL=0
SCRIPT_START=$(date +%s)

echo ""
echo -e "${BOLD}==================================================${NC}"
echo -e "${BOLD}  Security Scan Suite v4.2${NC}"
echo -e "  目标: ${TOTAL}  时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo -e "  报告: ${SCAN_DIR}"
echo -e "${BOLD}==================================================${NC}"
for ((i=0; i<TOTAL; i++)); do
    al="密钥"
    [ "${T_AUTH[$i]}" = "password" ] && al="密码"
    echo -e "  $((i+1)). ${T_USER[$i]}@${T_HOST[$i]}:${T_PORT[$i]} [${al}]"
done
echo -e "${BOLD}==================================================${NC}"

# ==================== 远程执行封装 ====================
remote_exec() {
    local ru="$1" rp="$2" rh="$3" rpt="$4" rk="$5" ra="$6" rc="$7" rt="${8:-30}"
    if [ "$ra" = "password" ]; then
        export SSHPASS="$rp"
        timeout "$rt" sshpass -e ssh \
            -o StrictHostKeyChecking=no \
            -o ConnectTimeout=10 \
            -o PubkeyAuthentication=no \
            -p "$rpt" \
            "${ru}@${rh}" "$rc" 2>/dev/null
    else
        local ko=""
        [ -n "$rk" ] && ko="-i $rk"
        timeout "$rt" ssh \
            -o StrictHostKeyChecking=no \
            -o ConnectTimeout=10 \
            -o BatchMode=yes \
            -p "$rpt" $ko \
            "${ru}@${rh}" "$rc" 2>/dev/null
    fi
}

run_remote() {
    local ru="$1" rp="$2" rh="$3" rpt="$4" rk="$5" ra="$6" rt="${7:-30}"
    if [ "$ra" = "password" ]; then
        export SSHPASS="$rp"
        timeout "$rt" sshpass -e ssh \
            -o StrictHostKeyChecking=no \
            -o ConnectTimeout=10 \
            -o PubkeyAuthentication=no \
            -p "$rpt" \
            "${ru}@${rh}" bash -s 2>/dev/null
    else
        local ko=""
        [ -n "$rk" ] && ko="-i $rk"
        timeout "$rt" ssh \
            -o StrictHostKeyChecking=no \
            -o ConnectTimeout=10 \
            -o BatchMode=yes \
            -p "$rpt" $ko \
            "${ru}@${rh}" bash -s 2>/dev/null
    fi
}

remote_scp() {
    local ru="$1" rp="$2" rh="$3" rpt="$4" rk="$5" ra="$6" rf="$7" lf="$8"
    if [ "$ra" = "password" ]; then
        export SSHPASS="$rp"
        timeout 15 sshpass -e scp \
            -o StrictHostKeyChecking=no \
            -o PubkeyAuthentication=no \
            -P "$rpt" \
            "${ru}@${rh}:${rf}" "$lf" 2>/dev/null
    else
        local ko=""
        [ -n "$rk" ] && ko="-i $rk"
        timeout 15 scp \
            -o StrictHostKeyChecking=no \
            -o BatchMode=yes \
            -P "$rpt" $ko \
            "${ru}@${rh}:${rf}" "$lf" 2>/dev/null
    fi
}

# ==================== 系统信息收集 ====================
collect_sysinfo() {
    local u="$1" p="$2" h="$3" pt="$4" k="$5" a="$6" td="$7"
    echo -e "  ${CYAN}[系统信息]${NC}"
    run_remote "$u" "$p" "$h" "$pt" "$k" "$a" 30 << 'RCEOF' > "${td}/sysinfo.txt"
echo "============ 系统信息 ============="
echo "时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo ""
echo "--- 操作系统 ---"
echo "OS: $(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d'"' -f2)"
echo "Kernel: $(uname -r)"
echo "Uptime: $(uptime -p 2>/dev/null || uptime)"
echo ""
echo "--- 系统资源 ---"
echo "CPU: $(nproc 2>/dev/null || echo N/A) 核"
echo "Load: $(cat /proc/loadavg 2>/dev/null | cut -d' ' -f1-3)"
echo "Memory: $(free -h 2>/dev/null | awk '/Mem/ {print $3"/"$2}')"
echo "Disk: $(df -h / 2>/dev/null | awk 'NR==2 {print $3"/"$2" ("$5" used)"}')"
echo ""
echo "--- 内核安全 ---"
echo "ASLR: $(cat /proc/sys/kernel/randomize_va_space 2>/dev/null)"
echo "SELinux: $(getenforce 2>/dev/null || echo N/A)"
echo "Firewalld: $(systemctl is-active firewalld 2>/dev/null || echo N/A)"
echo "Iptables rules: $(iptables -L -n 2>/dev/null | grep -cE '^[A-Z]' || echo 0)"
echo ""
echo "--- 长时间运行进程 ---"
ps -eo pid,user,comm,etime --sort=-etime 2>/dev/null | head -11
echo ""
echo "--- root 进程 ---"
ps -eo user,pid,comm 2>/dev/null | grep '^ *root' | head -20
RCEOF
    echo -e "    ${GREEN}[✓] ${td}/sysinfo.txt${NC}"
}

# ==================== SSH 安全检查 ====================
check_ssh_security() {
    local u="$1" p="$2" h="$3" pt="$4" k="$5" a="$6" td="$7"
    echo -e "  ${CYAN}[SSH 安全]${NC}"
    run_remote "$u" "$p" "$h" "$pt" "$k" "$a" 10 << 'RCEOF' > "${td}/ssh-security.txt"
echo "============ SSH 安全检查 ============="
echo ""
echo "--- SSH 版本 ---"
ssh -V 2>&1
echo ""
echo "--- SSH 配置 ---"
if [ -f /etc/ssh/sshd_config ]; then
    echo "PermitRootLogin: $(grep -E '^PermitRootLogin' /etc/ssh/sshd_config 2>/dev/null | tail -1 || echo '未设置(默认)')"
    echo "PasswordAuthentication: $(grep -E '^PasswordAuthentication' /etc/ssh/sshd_config 2>/dev/null | tail -1 || echo '未设置(默认)')"
    echo "PubkeyAuthentication: $(grep -E '^PubkeyAuthentication' /etc/ssh/sshd_config 2>/dev/null | tail -1 || echo '未设置(默认)')"
    echo "MaxAuthTries: $(grep -E '^MaxAuthTries' /etc/ssh/sshd_config 2>/dev/null | tail -1 || echo '未设置(默认)')"
else
    echo "sshd_config 不存在"
fi
echo ""
echo "--- 授权密钥 ---"
for dir in /root /home/*; do
    [ -d "$dir/.ssh" ] || continue
    echo "[用户: $(basename $dir)]"
    if [ -f "$dir/.ssh/authorized_keys" ]; then
        KC=$(grep -c '^ssh-' "$dir/.ssh/authorized_keys" 2>/dev/null || echo 0)
        echo "  密钥数: $KC"
        AB=$(grep '^command=' "$dir/.ssh/authorized_keys" 2>/dev/null | wc -l)
        [ "$AB" -gt 0 ] && echo "  [!] command= 配置: $AB 个"
    else
        echo "  authorized_keys: 不存在"
    fi
done
echo ""
echo "--- SSH 相关文件 ---"
find /etc/ssh/ /tmp /var/tmp -name 'authorized_keys*' -o -name 'id_rsa*' 2>/dev/null | head -20
RCEOF
    local RISK=0
    grep -q "PermitRootLogin.*yes" "${td}/ssh-security.txt" 2>/dev/null && { echo -e "    ${RED}[!] SSH 允许 root 密码登录${NC}"; RISK=1; }
    grep -q "command=" "${td}/ssh-security.txt" 2>/dev/null && { echo -e "    ${RED}[!] 检测到 authorized_keys command= (可能后门)${NC}"; RISK=1; }
    [ "$RISK" -eq 0 ] && echo -e "    ${GREEN}[✓] SSH 配置无明显风险${NC}"
    echo -e "    ${GREEN}[✓] ${td}/ssh-security.txt${NC}"
}

# ==================== 网络风险检查 ====================
check_network_risk() {
    local u="$1" p="$2" h="$3" pt="$4" k="$5" a="$6" td="$7"
    echo -e "  ${CYAN}[网络风险]${NC}"
    run_remote "$u" "$p" "$h" "$pt" "$k" "$a" 15 << 'RCEOF' > "${td}/network.txt"
echo "============ 网络风险检查 ============="
echo ""
echo "--- 监听端口 ---"
ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null
echo ""
echo "--- 外网连接 ---"
ss -tnp state established 2>/dev/null | awk 'NR>1 {print $5,$6}' | grep -v '127.0.0.1\|::1' | sort -u | head -20
echo ""
echo "--- SSH 暴力破解检测 ---"
if [ -f /var/log/secure ]; then
    grep 'Failed password' /var/log/secure 2>/dev/null | tail -10
elif [ -f /var/log/auth.log ]; then
    grep 'Failed password' /var/log/auth.log 2>/dev/null | tail -10
else
    journalctl -u sshd --since '24 hours ago' 2>/dev/null | grep -i 'failed\|invalid' | tail -10
fi
echo ""
echo "--- 最近登录 ---"
last -10 2>/dev/null
RCEOF
    local RC=0
    local DL
    DL=$(grep -cE "0\.0\.0\.0:(3306|6379|27017|11211|9200|5432)" "${td}/network.txt" 2>/dev/null)
    DL=${DL:-0}
    local FC
    FC=$(grep -c "Failed password" "${td}/network.txt" 2>/dev/null)
    FC=${FC:-0}
    if [ "$DL" -gt 0 ]; then
        echo -e "    ${RED}[!] ${DL} 个危险端口暴露在 0.0.0.0${NC}"
        RC=$((RC + DL))
    fi
    if [ "$FC" -gt 5 ]; then
        echo -e "    ${RED}[!] SSH 暴力破解: ${FC}${NC}"
        RC=$((RC + 1))
    fi
    if [ "$RC" -eq 0 ]; then
        echo -e "    ${GREEN}[✓] 未发现明显网络风险${NC}"
    fi
    echo -e "    ${GREEN}[✓] ${td}/network.txt${NC}"
}

# ==================== 定时任务检查 ====================
check_crontab_risk() {
    local u="$1" p="$2" h="$3" pt="$4" k="$5" a="$6" td="$7"
    echo -e "  ${CYAN}[定时任务]${NC}"
    run_remote "$u" "$p" "$h" "$pt" "$k" "$a" 15 << 'RCEOF' > "${td}/crontab.txt"
echo "============ 定时任务检查 ============="
echo ""
echo "--- 用户 Crontab ---"
for usr in $(cut -d: -f1 /etc/passwd 2>/dev/null); do
    CR=$(crontab -l -u "$usr" 2>/dev/null)
    if [ -n "$CR" ] && ! echo "$CR" | grep -q '^no crontab'; then
        echo "[User: $usr]"
        echo "$CR"
        echo ""
    fi
done
echo "--- /etc/crontab ---"
cat /etc/crontab 2>/dev/null
echo ""
echo "--- /etc/cron.d/ ---"
for f in /etc/cron.d/*; do
    [ -f "$f" ] && echo "[File: $f]" && cat "$f" 2>/dev/null && echo ""
done
RCEOF
    local SP
    SP=$(grep -ciE "curl.*\||wget.*\||python.*-c|perl.*-e|base64|eval|/dev/tcp" "${td}/crontab.txt" 2>/dev/null)
    SP=${SP:-0}
    if [ "$SP" -gt 0 ] 2>/dev/null; then
        echo -e "    ${RED}[!] 发现 ${SP} 条可疑定时任务${NC}"
        grep -iE "curl.*\||wget.*\||python.*-c|perl.*-e|base64|eval|/dev/tcp" "${td}/crontab.txt" 2>/dev/null | head -5 | while IFS= read -r ln; do
            echo -e "        ${RED}${ln}${NC}"
        done
    else
        echo -e "    ${GREEN}[✓] 未发现可疑定时任务${NC}"
    fi
    echo -e "    ${GREEN}[✓] ${td}/crontab.txt${NC}"
}

# ==================== 恶意文件检测 ====================
scan_malware() {
    local u="$1" p="$2" h="$3" pt="$4" k="$5" a="$6" td="$7"
    echo -e "  ${CYAN}[恶意文件检测]${NC}"
    echo -e "    [1/3] 远程收集高风险文件..."

    local FILE_LIST
    FILE_LIST=$(run_remote "$u" "$p" "$h" "$pt" "$k" "$a" 60 << RCEOF
find /home /var/www /tmp /opt /usr/local/bin /usr/bin -maxdepth 4 -type f -perm -4000 2>/dev/null | head -${MAX_FILE_COUNT}
find /home /var/www /tmp /opt -maxdepth 4 -type f \( -name '*.sh' -o -name '*.py' -o -name '*.php' -o -name '*.jsp' -o -perm -111 \) -mtime -7 2>/dev/null | head -${MAX_FILE_COUNT}
RCEOF
    )

    if [ -z "$FILE_LIST" ]; then
        echo -e "    ${GREEN}[✓] 未发现高风险文件${NC}"
        return
    fi

    local FILE_COUNT
    FILE_COUNT=$(echo "$FILE_LIST" | sort -u | wc -l)
    echo -e "    发现 ${FILE_COUNT} 个高风险文件"
    echo "$FILE_LIST" | sort -u > "${td}/filelist.txt"

    echo -e "    [2/3] 远程文件哈希比对..."
    run_remote "$u" "$p" "$h" "$pt" "$k" "$a" 120 << 'RCEOF' >> "${td}/filelist.txt"
while IFS= read -r f; do
    [ -f "$f" ] || continue
    SIZE=$(stat -c%s "$f" 2>/dev/null || echo 0)
    [ "$SIZE" -gt 104857600 ] 2>/dev/null && continue
    MD5=$(md5sum "$f" 2>/dev/null | awk '{print $1}')
    SHA=$(sha256sum "$f" 2>/dev/null | awk '{print $1}')
    echo "$MD5 | $SHA | $SIZE | $f"
done
RCEOF

    local HASH_COUNT
    HASH_COUNT=$(grep -c '|' "${td}/filelist.txt" 2>/dev/null)
    HASH_COUNT=${HASH_COUNT:-0}
    echo -e "    已收集 ${HASH_COUNT} 个文件哈希"

    local KNOWN_MALICIOUS=(
        "44d88612fea8a8f36de82e1278abb02f"
        "d9040bfe5862414908f75754a45f6269"
        "680bfb450a5b0589ad72f11e7263b100"
    )
    local SM=0
    local mh
    for mh in "${KNOWN_MALICIOUS[@]}"; do
        if grep -q "$mh" "${td}/filelist.txt" 2>/dev/null; then
            SM=$((SM + 1))
            echo -e "    ${RED}[!] 命中恶意哈希: ${mh}${NC}"
        fi
    done
    [ "$SM" -eq 0 ] && echo -e "    ${GREEN}[✓] 哈希比对通过${NC}"

    if [ "$CLAMAV_LOCAL" -eq 1 ]; then
        echo -e "    [3/3] 拉取可疑文件到本地 ClamAV..."
        local PULL_DIR="${td}/pulled-files"
        mkdir -p "$PULL_DIR"

        local PULL_LIST
        PULL_LIST=$(run_remote "$u" "$p" "$h" "$pt" "$k" "$a" 60 << RCEOF
find /home /var/www /tmp /opt -maxdepth 4 -type f -perm -4000 2>/dev/null
find /home /var/www /tmp /opt -maxdepth 4 -type f \( -name '*.sh' -o -name '*.py' -o -name '*.php' -o -name '*.jsp' -o -name 'shell*' -o -name '*.phtml' \) -mtime -3 2>/dev/null
RCEOF
        )

        if [ -n "$PULL_LIST" ]; then
            local PULL_COUNT
            PULL_COUNT=$(echo "$PULL_LIST" | sort -u | wc -l)
            echo -e "    拉取 ${PULL_COUNT} 个可疑文件..."
            echo "$PULL_LIST" | sort -u | head -"${MAX_FILE_COUNT}" | while IFS= read -r rf; do
                [ -z "$rf" ] && continue
                local sp
                sp=$(echo "$rf" | tr '/' '_')
                remote_scp "$u" "$p" "$h" "$pt" "$k" "$a" "$rf" "${PULL_DIR}/${sp}"
            done

            echo -e "    本地 ClamAV 扫描中..."
            local CLAM_RESULT
            CLAM_RESULT=$(clamscan -r --infected "$PULL_DIR" 2>&1 | grep -E "Infected|Scanned|FOUND")
            echo "$CLAM_RESULT" >> "${td}/filelist.txt"

            local INFECTED
            INFECTED=$(echo "$CLAM_RESULT" | grep "Infected files:" | awk '{print $NF}')
            INFECTED=${INFECTED:-0}
            if [ "$INFECTED" -gt 0 ] 2>/dev/null; then
                echo -e "    ${RED}[!] ClamAV 发现 ${INFECTED} 个感染文件!${NC}"
                echo "$CLAM_RESULT" | grep "FOUND" | head -10 | while IFS= read -r ln; do
                    echo -e "        ${RED}${ln}${NC}"
                done
            else
                echo -e "    ${GREEN}[✓] ClamAV 扫描通过${NC}"
            fi
        else
            echo -e "    [3/3] 无需拉取"
        fi
    else
        echo -e "    [3/3] ClamAV 不可用,跳过"
    fi
}

# ==================== Web 指纹扫描 ====================
scan_web_fingerprint() {
    local u="$1" p="$2" h="$3" pt="$4" k="$5" a="$6" td="$7"
    echo -e "  ${CYAN}[Web 指纹]${NC}"

    local URL=""
    local HTTP_CODE HTTPS_CODE
    HTTP_CODE=$(curl -sk --max-time 5 -o /dev/null -w "%{http_code}" "http://${h}" 2>/dev/null)
    HTTPS_CODE=$(curl -sk --max-time 5 -o /dev/null -w "%{http_code}" "https://${h}" 2>/dev/null)
    if [ "$HTTPS_CODE" != "000" ] && [ -n "$HTTPS_CODE" ]; then
        URL="https://${h}"
    elif [ "$HTTP_CODE" != "000" ] && [ -n "$HTTP_CODE" ]; then
        URL="http://${h}"
    fi

    if [ -z "$URL" ]; then
        echo -e "    ${YELLOW}[!] 未发现 Web 服务${NC}"
        return
    fi

    local HEADERS SERVER_POWERED
    HEADERS=$(curl -skI --max-time 10 "$URL" 2>/dev/null)
    local SERVER_HEADER
    SERVER_HEADER=$(echo "$HEADERS" | grep -i "^Server:" | head -1 | sed 's/^[Ss]erver: *//;s/\r//')
    local POWERED_BY
    POWERED_BY=$(echo "$HEADERS" | grep -i "^X-Powered-By:" | head -1 | sed 's/^[Xx]-[Pp]owered-[Bb]y: *//;s/\r//')

    echo -e "    URL:    ${URL}"
    [ -n "$SERVER_HEADER" ] && echo -e "    Server: ${BOLD}${SERVER_HEADER}${NC}"
    [ -n "$POWERED_BY" ] && echo -e "    动态:   ${POWERED_BY}"

    local CVE_MATCHED_LIST=""
    local CVE_HITS=0
    local cve_entry
    local cve_sw cve_ver cve_id cve_sev cve_desc
    local matched_ver

    for cve_entry in "${CVE_DB[@]}"; do
        IFS='|' read -r cve_sw cve_ver cve_id cve_sev cve_desc <<< "$cve_entry"

        if echo "$CVE_MATCHED_LIST" | grep -qw "$cve_id"; then
            continue
        fi

        matched_ver=""
        if [[ "$cve_sw" == "nginx" ]] && [[ "$SERVER_HEADER" == *nginx* ]]; then
            matched_ver=$(echo "$SERVER_HEADER" | grep -oP "nginx/\K[0-9.]+" | head -1)
        elif [[ "$cve_sw" == "Apache" ]] && [[ "$SERVER_HEADER" == *Apache* ]]; then
            matched_ver=$(echo "$SERVER_HEADER" | grep -oP "Apache/\K[0-9.]+" | head -1)
        elif [[ "$cve_sw" == "Microsoft-IIS"* ]] && [[ "$SERVER_HEADER" == *IIS* ]]; then
            matched_ver=$(echo "$SERVER_HEADER" | grep -oP "IIS/\K[0-9.]+" | head -1)
        fi

        if [ -n "$matched_ver" ]; then
            version_compare "$matched_ver" "$cve_ver"
            if [ $? -eq 2 ]; then
                echo -e "    ${RED}[!] ${cve_id} (${cve_sev}) — ${cve_desc}${NC}"
                CVE_HITS=$((CVE_HITS + 1))
                CVE_MATCHED_LIST="${CVE_MATCHED_LIST} ${cve_id}"
            fi
        fi
    done

    if [ "$CVE_HITS" -eq 0 ]; then
        echo -e "    ${GREEN}[✓] 未匹配到已知 CVE${NC}"
    else
        echo -e "    ${BOLD}  共匹配 ${CVE_HITS} 个 CVE${NC}"
    fi

    local MISSING_HEADERS=0
    local HEADER_NAME
    for HEADER_NAME in "Strict-Transport-Security" "X-Content-Type-Options" "X-Frame-Options" "Content-Security-Policy"; do
        echo "$HEADERS" | grep -qi "^${HEADER_NAME}:" || MISSING_HEADERS=$((MISSING_HEADERS + 1))
    done
    if [ "$MISSING_HEADERS" -gt 0 ]; then
        echo -e "    ${YELLOW}[!] 缺失 ${MISSING_HEADERS} 个安全响应头${NC}"
    fi

    local SSL_HOST
    SSL_HOST=$(echo "$URL" | grep -oP '(?<=https://)[^:/]+')
    if [ -n "$SSL_HOST" ]; then
        local CERT_OUTPUT
        CERT_OUTPUT=$(echo | timeout 5 openssl s_client -servername "$SSL_HOST" -connect "${SSL_HOST}:443" 2>/dev/null)
        if echo "$CERT_OUTPUT" | grep -q "BEGIN CERTIFICATE"; then
            local CERT_ISSUER CERT_SUBJECT
            CERT_ISSUER=$(echo "$CERT_OUTPUT" | openssl x509 -noout -issuer 2>/dev/null | sed 's/issuer=//')
            CERT_SUBJECT=$(echo "$CERT_OUTPUT" | openssl x509 -noout -subject 2>/dev/null | sed 's/subject=//')
            if echo "$CERT_ISSUER" | grep -qi "$CERT_SUBJECT"; then
                echo -e "    ${RED}[!] SSL: 自签名${NC}"
            else
                echo -e "    ${GREEN}[✓] SSL: 正常签发${NC}"
            fi
        fi
    fi

    {
        echo "============ Web 指纹 ============="
        echo "URL: ${URL}"
        echo "Server: ${SERVER_HEADER:-未知}"
        echo "X-Powered-By: ${POWERED_BY:-}"
        echo "CVE匹配: ${CVE_HITS}"
        echo "缺失安全头: ${MISSING_HEADERS}"
        echo ""
        echo "--- 响应头 ---"
        echo "$HEADERS"
    } > "${td}/web.txt" 2>/dev/null
    echo -e "    ${GREEN}[✓] ${td}/web.txt${NC}"
}

# ============================================================
#  主扫描循环
# ============================================================
for ((IDX=0; IDX<TOTAL; IDX++)); do
    TU="${T_USER[$IDX]}"
    TP="${T_PASS[$IDX]}"
    TH="${T_HOST[$IDX]}"
    TPT="${T_PORT[$IDX]}"
    TK="${T_KEY[$IDX]}"
    TA="${T_AUTH[$IDX]}"
    NUM=$((IDX + 1))
    SAFE_NAME=$(echo "${TU}@${TH}_${TPT}" | tr '/:.' '__')
    TARGET_DIR="${SCAN_DIR}/${SAFE_NAME}"
    mkdir -p "$TARGET_DIR"
    TARGET_START=$(date +%s)

    echo ""
    echo -e "${YELLOW}==================================================${NC}"
    echo -e "${YELLOW}  [${NUM}/${TOTAL}] ${TU}@${TH}:${TPT} [${TA}]${NC}"
    echo -e "${YELLOW}==================================================${NC}"

    echo -e "  测试 SSH 连接..."
    TEST_RESULT=$(remote_exec "$TU" "$TP" "$TH" "$TPT" "$TK" "$TA" "echo ok" 10 | tail -1 | tr -d '\r')
    if [ "$TEST_RESULT" != "ok" ]; then
        echo -e "  ${RED}[✗] SSH 连接失败,跳过${NC}"
        FAIL=$((FAIL + 1))
        continue
    fi
    echo -e "  ${GREEN}[✓] SSH 连接成功${NC}"
    SUCCESS=$((SUCCESS + 1))

    if [ "$SKIP_SYSINFO" -eq 0 ]; then
        collect_sysinfo "$TU" "$TP" "$TH" "$TPT" "$TK" "$TA" "$TARGET_DIR"
    fi

    check_ssh_security "$TU" "$TP" "$TH" "$TPT" "$TK" "$TA" "$TARGET_DIR"

    if [ "$SKIP_NETWORK" -eq 0 ]; then
        check_network_risk "$TU" "$TP" "$TH" "$TPT" "$TK" "$TA" "$TARGET_DIR"
    fi

    if [ "$SKIP_CRON" -eq 0 ]; then
        check_crontab_risk "$TU" "$TP" "$TH" "$TPT" "$TK" "$TA" "$TARGET_DIR"
    fi

    if [ "$SKIP_WEB" -eq 0 ]; then
        scan_web_fingerprint "$TU" "$TP" "$TH" "$TPT" "$TK" "$TA" "$TARGET_DIR"
    fi

    if [ "$SKIP_MALWARE" -eq 0 ]; then
        scan_malware "$TU" "$TP" "$TH" "$TPT" "$TK" "$TA" "$TARGET_DIR"
    fi

    TARGET_END=$(date +%s)
    TARGET_ELAPSED=$((TARGET_END - TARGET_START))
    echo -e "\n  ${GREEN}[*] ${TU}@${TH} 扫描完成 (${TARGET_ELAPSED}s)${NC}"
done

# ============================================================
#  汇总报告
# ============================================================
SCRIPT_END=$(date +%s)
SCRIPT_ELAPSED=$((SCRIPT_END - SCRIPT_START))

echo ""
echo -e "${YELLOW}==================================================${NC}"
echo -e "${YELLOW}  扫描汇总${NC}"
echo -e "${YELLOW}==================================================${NC}"

SUMMARY="${SCAN_DIR}/summary.txt"
{
    echo "========================================"
    echo "  安全扫描汇总报告 v4.2"
    echo "  时间: $(date '+%Y-%m-%d %H:%M:%S')"
    echo "  目标: ${TOTAL}  成功: ${SUCCESS}  失败: ${FAIL}"
    echo "  总耗时: ${SCRIPT_ELAPSED}秒"
    echo "========================================"
    echo ""
} > "$SUMMARY"

for ((IDX=0; IDX<TOTAL; IDX++)); do
    TU="${T_USER[$IDX]}"
    TH="${T_HOST[$IDX]}"
    TPT="${T_PORT[$IDX]}"
    TA="${T_AUTH[$IDX]}"
    SAFE_NAME=$(echo "${TU}@${TH}_${TPT}" | tr '/:.' '__')
    TARGET_DIR="${SCAN_DIR}/${SAFE_NAME}"

    echo "### [$((IDX+1))/${TOTAL}] ${TU}@${TH}:${TPT} [${TA}] ###" >> "$SUMMARY"

    if [ -f "${TARGET_DIR}/network.txt" ]; then
        local_DN=$(grep -cE "0\.0\.0\.0:(3306|6379|27017|11211|9200|5432)" "${TARGET_DIR}/network.txt" 2>/dev/null)
        local_DN=${local_DN:-0}
        local_FP=$(grep -c "Failed password" "${TARGET_DIR}/network.txt" 2>/dev/null)
        local_FP=${local_FP:-0}
        [ "$local_DN" -gt 0 ] && echo "  [!] 危险端口: ${local_DN}" >> "$SUMMARY"
        [ "$local_FP" -gt 5 ] && echo "  [!] SSH暴力破解: ${local_FP}次" >> "$SUMMARY"
    fi

    if [ -f "${TARGET_DIR}/filelist.txt" ]; then
        local_INF=$(grep "Infected files:" "${TARGET_DIR}/filelist.txt" 2>/dev/null | tail -1 | awk '{print $NF}')
        local_INF=${local_INF:-0}
        [ "$local_INF" -gt 0 ] 2>/dev/null && echo "  [!] ClamAV感染: ${local_INF}" >> "$SUMMARY"
    fi

    if [ -f "${TARGET_DIR}/crontab.txt" ]; then
        local_SUSP=$(grep -ciE "curl.*\||wget.*\||base64|eval|/dev/tcp" "${TARGET_DIR}/crontab.txt" 2>/dev/null)
        local_SUSP=${local_SUSP:-0}
        [ "$local_SUSP" -gt 0 ] 2>/dev/null && echo "  [!] 可疑定时任务: ${local_SUSP}条" >> "$SUMMARY"
    fi

    if [ -f "${TARGET_DIR}/ssh-security.txt" ]; then
        grep -q "PermitRootLogin.*yes" "${TARGET_DIR}/ssh-security.txt" 2>/dev/null && echo "  [!] SSH: root密码登录" >> "$SUMMARY"
        grep -q "command=" "${TARGET_DIR}/ssh-security.txt" 2>/dev/null && echo "  [!] SSH: 可能后门" >> "$SUMMARY"
    fi

    if [ -f "${TARGET_DIR}/web.txt" ]; then
        local_WC=$(grep "CVE匹配:" "${TARGET_DIR}/web.txt" 2>/dev/null | awk '{print $2}')
        local_WC=${local_WC:-0}
        [ "$local_WC" -gt 0 ] 2>/dev/null && echo "  [!] Web CVE: ${local_WC}" >> "$SUMMARY"
    fi

    echo "" >> "$SUMMARY"
done

echo "========================================" >> "$SUMMARY"
echo "  报告: ${SCAN_DIR}" >> "$SUMMARY"
echo "========================================" >> "$SUMMARY"

echo ""
cat "$SUMMARY"
echo ""
echo -e "${GREEN}详细报告: ${SCAN_DIR}${NC}"
echo ""

六、工具总览

工具 版本 部署方式 用途
Nikto 2.6.0 Git 克隆 Web 服务器配置与安全头扫描
afrog 3.5.3 (PoC 1648 个) 预编译二进制 PoC 漏洞验证
ClamAV 0.103.11 (362 万特征) yum 安装 病毒/恶意文件扫描
security-scan.sh v4.2 本地脚本 统一远程扫描(集成以上工具 + SSH 远程检查)