执行无代理漏洞扫描(Performing Agentless Vulnerability Scanning)
概述
无代理漏洞扫描(Agentless vulnerability scanning)无需在端点安装 Agent 即可评估系统安全弱点。该方法利用现有网络协议(Linux 使用 SSH、Windows 使用 WMI)、云服务商 API 进行基于快照的分析,以及经身份验证的远程检查。Microsoft Defender for Cloud、Wiz、Datadog 和 Tenable 等现代云平台通过获取磁盘快照并离线检查操作系统配置和已安装软件包,实现带外分析(out-of-band analysis)。开源工具 Vuls 基于 NVD 和 OVAL 数据为 Linux/FreeBSD 系统提供无代理扫描。本技能涵盖在本地、云端和容器化环境中配置无代理扫描。
前置条件
- 在 Linux/Unix 目标上配置基于 SSH 密钥的身份验证
- 在 Windows 目标上具有适当凭据的 WMI/WinRM 访问权限
- 云服务商 API 凭据(AWS IAM、Azure RBAC、GCP IAM)
- 扫描器到目标系统所需端口的网络访问权限
- 具有目标系统配置只读访问权限的服务账户
- Python 3.8+(用于自定义扫描自动化)
核心概念
无代理扫描 vs 基于 Agent 扫描
| 对比维度 |
无代理扫描 |
基于 Agent 扫描 |
| 部署方式 |
无需安装软件 |
需在每个端点安装 Agent |
| 网络依赖 |
需要网络连接 |
可离线运行并通过云同步 |
| 性能影响 |
对目标系统影响极小 |
持续轻量级开销 |
| 覆盖深度 |
取决于协议/凭据 |
深度本地访问 |
| 云快照分析 |
原生支持 |
不适用 |
| 适用场景 |
云虚拟机、IoT、遗留系统、OT |
受管端点、笔记本电脑 |
无代理扫描方法
| 方法 |
协议 |
目标操作系统 |
端口 |
适用场景 |
| SSH 远程命令 |
SSH |
Linux/Unix |
22 |
软件包枚举、配置审计 |
| WMI 远程查询 |
WMI/DCOM |
Windows |
135, 445 |
热补丁枚举、注册表检查 |
| WinRM PowerShell |
WS-Man |
Windows |
5985/5986 |
远程命令执行 |
| SNMP 团体字 |
SNMP v2c/v3 |
网络设备 |
161 |
设备指纹识别、固件检查 |
| 云快照 |
云服务商 API |
云虚拟机 |
N/A |
磁盘镜像分析 |
| 容器镜像仓库 |
HTTPS |
容器镜像 |
443 |
镜像漏洞扫描 |
| 基于 API |
REST/HTTPS |
SaaS/Cloud |
443 |
配置评估 |
云快照分析流程
1. 扫描器通过云 API 请求磁盘快照
2. 云服务商为虚拟机根磁盘和数据磁盘创建快照
3. 扫描器在隔离分析环境中挂载快照
4. 扫描器检查操作系统软件包、配置和文件系统
5. 分析完成后删除快照(不保留持久副本)
6. 结果发送到中央管理控制台
实施步骤
步骤 1:基于 SSH 的无代理扫描(Linux)
# 创建专用扫描 SSH 密钥对
ssh-keygen -t ed25519 -f /opt/scanner/.ssh/scan_key -N "" \
-C "vuln-scanner@security.local"
# 通过 Ansible 将公钥部署到目标
# ansible-playbook deploy_scan_key.yml
# 测试与目标的连通性
ssh -i /opt/scanner/.ssh/scan_key -o ConnectTimeout=10 \
scanner@target-host "cat /etc/os-release && dpkg -l 2>/dev/null || rpm -qa"
import paramiko
import json
class AgentlessLinuxScanner:
"""基于 SSH 的 Linux 系统无代理漏洞扫描器。"""
def __init__(self, key_path):
self.key_path = key_path
def connect(self, hostname, username="scanner", port=22):
"""与目标建立 SSH 连接。"""
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
key = paramiko.Ed25519Key.from_private_key_file(self.key_path)
client.connect(hostname, port=port, username=username, pkey=key,
timeout=30, banner_timeout=30)
return client
def get_os_info(self, client):
"""检测操作系统类型和版本。"""
_, stdout, _ = client.exec_command("cat /etc/os-release", timeout=10)
os_release = stdout.read().decode()
info = {}
for line in os_release.strip().split("\n"):
if "=" in line:
key, val = line.split("=", 1)
info[key] = val.strip('"')
return info
def get_installed_packages(self, client):
"""枚举已安装的软件包。"""
# 尝试 dpkg(Debian/Ubuntu)
_, stdout, _ = client.exec_command(
"dpkg-query -W -f='${Package}|${Version}|${Architecture}\\n'",
timeout=30
)
output = stdout.read().decode().strip()
if output:
packages = []
for line in output.split("\n"):
parts = line.split("|")
if len(parts) >= 2:
packages.append({
"name": parts[0],
"version": parts[1],
"arch": parts[2] if len(parts) > 2 else "",
"manager": "dpkg"
})
return packages
# 尝试 rpm(RHEL/CentOS/Fedora)
_, stdout, _ = client.exec_command(
"rpm -qa --queryformat '%{NAME}|%{VERSION}-%{RELEASE}|%{ARCH}\\n'",
timeout=30
)
output = stdout.read().decode().strip()
packages = []
for line in output.split("\n"):
parts = line.split("|")
if len(parts) >= 2:
packages.append({
"name": parts[0],
"version": parts[1],
"arch": parts[2] if len(parts) > 2 else "",
"manager": "rpm"
})
return packages
def check_kernel_version(self, client):
"""获取运行中的内核版本。"""
_, stdout, _ = client.exec_command("uname -r", timeout=10)
return stdout.read().decode().strip()
def check_listening_ports(self, client):
"""枚举正在监听的网络服务。"""
_, stdout, _ = client.exec_command(
"ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null",
timeout=10
)
return stdout.read().decode().strip()
def scan_host(self, hostname, username="scanner"):
"""对主机执行完整的无代理扫描。"""
print(f"[*] 正在扫描 {hostname}...")
client = self.connect(hostname, username)
result = {
"hostname": hostname,
"os_info": self.get_os_info(client),
"kernel": self.check_kernel_version(client),
"packages": self.get_installed_packages(client),
"listening_ports": self.check_listening_ports(client),
}
client.close()
print(f" [+] 在 {hostname} 上发现 {len(result['packages'])} 个软件包")
return result
步骤 2:基于 WinRM 的无代理扫描(Windows)
import winrm
class AgentlessWindowsScanner:
"""基于 WinRM 的 Windows 系统无代理漏洞扫描器。"""
def __init__(self, username, password, domain=None):
self.username = username
self.password = password
self.domain = domain
def connect(self, hostname, use_ssl=True):
"""创建 WinRM 会话。"""
port = 5986 if use_ssl else 5985
transport = "ntlm"
user = f"{self.domain}\\{self.username}" if self.domain else self.username
session = winrm.Session(
f"{'https' if use_ssl else 'http'}://{hostname}:{port}/wsman",
auth=(user, self.password),
transport=transport,
server_cert_validation="ignore"
)
return session
def get_installed_hotfixes(self, session):
"""获取已安装的 Windows 更新/热补丁。"""
cmd = "Get-HotFix | Select-Object HotFixID,InstalledOn,Description | ConvertTo-Json"
result = session.run_ps(cmd)
if result.status_code == 0:
return json.loads(result.std_out.decode())
return []
def get_installed_software(self, session):
"""从注册表枚举已安装的软件。"""
cmd = """
$paths = @(
'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',
'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'
)
Get-ItemProperty $paths -ErrorAction SilentlyContinue |
Where-Object {$_.DisplayName} |
Select-Object DisplayName, DisplayVersion, Publisher |
ConvertTo-Json
"""
result = session.run_ps(cmd)
if result.status_code == 0:
return json.loads(result.std_out.decode())
return []
def get_os_info(self, session):
"""获取 Windows 操作系统详情。"""
cmd = "Get-CimInstance Win32_OperatingSystem | Select-Object Caption,Version,BuildNumber,OSArchitecture | ConvertTo-Json"
result = session.run_ps(cmd)
if result.status_code == 0:
return json.loads(result.std_out.decode())
return {}
def scan_host(self, hostname):
"""对 Windows 主机执行完整的无代理扫描。"""
print(f"[*] 通过 WinRM 扫描 {hostname}...")
session = self.connect(hostname)
result = {
"hostname": hostname,
"os_info": self.get_os_info(session),
"hotfixes": self.get_installed_hotfixes(session),
"software": self.get_installed_software(session),
}
print(f" [+] 发现 {len(result['hotfixes'])} 个热补丁,"
f"{len(result['software'])} 个软件条目")
return result
步骤 3:云快照扫描(AWS)
import boto3
import time
class AWSSnapshotScanner:
"""基于 AWS EC2 EBS 快照的无代理漏洞扫描器。"""
def __init__(self, region="us-east-1"):
self.ec2 = boto3.client("ec2", region_name=region)
def create_snapshot(self, volume_id, description="安全扫描快照"):
"""创建用于分析的 EBS 快照。"""
snapshot = self.ec2.create_snapshot(
VolumeId=volume_id,
Description=description,
TagSpecifications=[{
"ResourceType": "snapshot",
"Tags": [
{"Key": "Purpose", "Value": "VulnScan"},
{"Key": "AutoDelete", "Value": "true"},
]
}]
)
snapshot_id = snapshot["SnapshotId"]
print(f" [*] 正在从 {volume_id} 创建快照 {snapshot_id}...")
waiter = self.ec2.get_waiter("snapshot_completed")
waiter.wait(SnapshotIds=[snapshot_id])
print(f" [+] 快照 {snapshot_id} 已就绪")
return snapshot_id
def delete_snapshot(self, snapshot_id):
"""分析完成后清理快照。"""
self.ec2.delete_snapshot(SnapshotId=snapshot_id)
print(f" [+] 已删除快照 {snapshot_id}")
def scan_instance(self, instance_id):
"""通过快照分析扫描 EC2 实例。"""
print(f"[*] 对实例 {instance_id} 执行无代理扫描")
instance = self.ec2.describe_instances(
InstanceIds=[instance_id]
)["Reservations"][0]["Instances"][0]
root_volume = None
for bdm in instance.get("BlockDeviceMappings", []):
if bdm["DeviceName"] == instance.get("RootDeviceName"):
root_volume = bdm["Ebs"]["VolumeId"]
break
if not root_volume:
print(" [!] 未找到根卷")
return None
snapshot_id = self.create_snapshot(root_volume)
try:
# 此处执行分析
# 挂载快照,检查软件包,核查配置
result = {
"instance_id": instance_id,
"snapshot_id": snapshot_id,
"root_volume": root_volume,
"platform": instance.get("Platform", "linux"),
"state": instance["State"]["Name"],
}
return result
finally:
self.delete_snapshot(snapshot_id)
步骤 4:Vuls 开源无代理扫描器
# /etc/vuls/config.toml - 无代理扫描的 Vuls 配置
[servers]
[servers.web-server-01]
host = "192.168.1.10"
port = "22"
user = "vuls"
keyPath = "/opt/vuls/.ssh/scan_key"
scanMode = ["fast"]
[servers.db-server-01]
host = "192.168.1.20"
port = "22"
user = "vuls"
keyPath = "/opt/vuls/.ssh/scan_key"
scanMode = ["fast-root"]
[servers.db-server-01.optional]
[servers.db-server-01.optional.sudo]
password = ""
[servers.container-host-01]
host = "192.168.1.30"
port = "22"
user = "vuls"
keyPath = "/opt/vuls/.ssh/scan_key"
scanMode = ["fast"]
containersIncluded = ["${running}"]
# 运行 Vuls 无代理扫描
vuls scan
# 生成报告
vuls report -format-json -to-localfile
# 查看结果
vuls tui
最佳实践
- 对 Linux 扫描使用基于 SSH 密钥的身份验证,而非密码
- 为扫描创建具有最小只读权限的专用服务账户
- 分析完成后始终清理云快照,以避免存储成本和数据暴露
- 结合无代理扫描与 Agent 扫描以实现全面覆盖
- 在低活动期安排扫描,以最小化性能影响
- 定期轮换扫描凭据并存储在 Secrets Vault 中
- 在安排生产扫描前测试扫描器连通性
- 对网络设备扫描使用带身份验证和加密的 SNMPv3
常见陷阱
- 在未正确隔离的多个环境中使用共享凭据
- 未清理云环境中的临时快照
- 假设无代理扫描对性能完全无影响(网络和 CPU 仍会被使用)
- 新部署上缺少 WinRM/SSH 防火墙规则导致扫描失败
- 未考虑 SSH 主机密钥变更导致身份验证失败
- 使用 OT/ICS 设备无法安全处理的协议扫描这些设备
相关技能
- implementing-rapid7-insightvm-for-scanning
- implementing-wazuh-for-vulnerability-detection
- deploying-osquery-for-endpoint-monitoring
- performing-remediation-validation-scanning