目录
[① 函数声明与变量定义](#① 函数声明与变量定义)
[② 解析心跳包头部](#② 解析心跳包头部)
[③ 类型检查与内存分配](#③ 类型检查与内存分配)
[④ 构造响应头部](#④ 构造响应头部)
[⑤ 致命漏洞点:memcpy操作](#⑤ 致命漏洞点:memcpy操作)
[⑥ 发送响应](#⑥ 发送响应)
[① 正常逻辑](#① 正常逻辑)
[② 恶意逻辑](#② 恶意逻辑)
[1、确保系统已安装 Docker 和 Docker-Compose](#1、确保系统已安装 Docker 和 Docker-Compose)
[2、下载 Vulhub](#2、下载 Vulhub)
[(2)search heartbleed](#(2)search heartbleed)
[① 设置目标主机地址](#① 设置目标主机地址)
[③ 启用详细输出模式](#③ 启用详细输出模式)
心脏滴血漏洞(CVE-2014-0160)是OpenSSL中因未验证心跳包数据长度导致的缓冲区过读漏洞,攻击者可远程读取服务器内存中的敏感数据(如私钥、密码等)。漏洞原理在于process_heartbeat()函数未检查客户端声明的长度与实际数据是否匹配,导致memcpy操作可泄露内存内容。本文通过Vulhub搭建漏洞环境(Docker容器映射8443端口),利用Nmap和Metasploit(openssl_heartbleed模块)检测并利用该漏洞。
一、心脏滴血漏洞(CVE-2014-0160)
1、漏洞简介
CVE-2014-0160即"心脏滴血"漏洞,是2014年在OpenSSL中发现的一个灾难性安全缺陷。由于未对心跳扩展协议的数据长度进行验证,攻击者可远程读取服务器内存中最多64KB的敏感数据,包括私钥、用户密码等核心机密。该漏洞影响广泛、利用简单且不留痕迹,迫使全球数百万网站紧急修复并更换安全证书,成为推动互联网基础设施安全变革的关键事件。
| 项目 | 详细内容 |
|---|---|
| 漏洞名称 | Heartbleed(心脏滴血) |
| CVE编号 | CVE-2014-0160 |
| 公开日期 | 2014年4月7日 |
| 漏洞类型 | 缓冲区过读 |
| 影响协议 | TLS/DTLS(传输层安全协议) |
| 影响组件 | OpenSSL 库中的 心跳扩展 功能 |
| 影响版本 | OpenSSL 1.0.1 至 1.0.1f ,以及 1.0.2-beta |
| 安全版本 | OpenSSL 1.0.1g 及以上,1.0.2-beta2 及以上 |
| CVSS 3.x 评分 | 7.5 (高危) - 向量:AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| 攻击向量 | 网络远程攻击,无需认证 |
| 攻击复杂度 | 低(利用简单,已有公开的利用工具) |
| 所需权限 | 无 |
| 用户交互 | 无需用户交互 |
| 核心漏洞描述 | 处理TLS/DTLS心跳请求时,由于未验证客户端发送的数据长度字段是否与真实数据匹配,导致服务器会从内存中返回超出原有数据范围的内容。 |
| 造成影响 | 信息泄露 :攻击者每次可读取服务器内存中最多64KB的数据。这些数据可能包含: • 服务器的私钥 • 用户的敏感信息 (用户名、密码、会话Cookie、信用卡号等) • 其他进程的内存片段 |
| 攻击特点 | 1. 不留痕迹 :在服务器日志中通常显示为正常心跳请求。 2. 可反复进行 :可多次发起攻击,获取内存中不同部分的数据。 3. 危害巨大:获取私钥意味着可解密过往流量、进行中间人攻击。 |
| 修复措施 | 1. 立即升级 :将OpenSSL升级至安全版本。 2. 撤销与更换证书 :必须 吊销可能已泄露的旧SSL证书,重新生成密钥并申请新证书。 3. 轮换凭据 :用户应在确认服务修复后,更改相关密码。 4. 入侵检测:对服务器进行安全审计,排查是否已被入侵。 |
| 历史意义与影响 | 1. 影响范围极广 :当时全球约2/3的网站直接或间接受影响。 2. 开源生态转折点 :暴露了核心开源基础设施维护的脆弱性,催生了核心基础设施倡议 等资助计划。 3. 安全实践升级:推动了漏洞协同披露流程的成熟和"证书透明度"等技术的应用。 |
2、漏洞原理
漏洞的源头出在 OpenSSL 的 "心跳扩展" 功能的实现代码中。在处理心跳请求时,服务器会信任客户端发送的"数据长度"字段,并据此分配内存。但在从内存缓冲区复制数据返回时,没有检查客户端声明的长度是否与实际发送的数据长度匹配,导致 缓冲区过读。
(1)代码逻辑
在客户服务器通信模型中,客户端需要每隔一定时间向服务器发送数据包,以确定服务器是否掉线,服务器也能以此判断客户端是否存活,这种每隔固定时间发一次的数据包也称为心跳包。心跳包的内容没有什么特别的规定,一般都是很小的包。请求和应答两种类型的心跳包格式如下图所示。其中心跳包类型占1 个字节,主要是请求和响应两种类型;心跳包数据长度字段占2 个字节,表示后续数据或者负载的长度。接收端收到该心跳包后的处理函数是process_heartbeat(),其中参数p 指向心跳包的报文数据,s 是对应客户端的socket 网络通信套接字。

/* 处理心跳请求的函数 */
void process_heartbeat(unsigned char *p, SOCKET s)
{
unsigned short hbtype; // 心跳包类型(1字节)
unsigned int payload; // 客户端声明的心跳包数据长度
unsigned char *pl; // 指向心跳包数据的指针
hbtype = *p++; // 读取心跳包类型并移动指针
n2s(p, payload); // 从网络字节序读取2字节长度到payload变量
pl = p; // pl现在指向心跳包实际数据的位置
/*
* 关键漏洞点:这里只检查了心跳包类型,
* 但没有验证客户端声明的payload是否与实际数据长度匹配!
*/
if (hbtype == HB_REQUEST) { // 如果是心跳请求
unsigned char *buffer, *bp;
// 分配内存:1字节类型 + 2字节长度 + payload字节数据
buffer = malloc(1 + 2 + payload);
bp = buffer;
*bp++ = HB_RESPONSE; // 填充响应类型(1字节)
s2n(payload, bp); // 填充数据长度(2字节),转换为网络字节序
/*
* 致命漏洞发生在这里!
* memcpy从pl位置开始,复制payload字节到bp位置
* 但:如果客户端实际发送的数据小于它声明的payload值,
* 这里会从pl位置之后的内存区域读取额外数据!
*/
memcpy(bp, pl, payload); // 复制数据
// 将构造好的心跳响应包通过socket返回客户端
r = write_bytes(s, buffer, 3 + payload);
}
}
(2)攻击方法
// =========== 正常情况 ===========
客户端发送: [HB_REQUEST][length=5][data="hello"]
↑ ↑ ↑
hbtype=1 payload=5 pl指向这里
服务器处理:
1. 分配内存: 1 + 2 + 5 = 8字节
2. memcpy(bp, pl, 5) → 正常复制"hello"的5个字节
// =========== 恶意攻击 ===========
客户端发送: [HB_REQUEST][length=500][data="A"]
↑ ↑ ↑
hbtype=1 payload=500 pl指向这里
(但实际只有1字节'A')
服务器处理:
1. 分配内存: 1 + 2 + 500 = 503字节
2. memcpy(bp, pl, 500) → 从pl位置开始读取500字节!
- 第1字节: 'A' (客户端实际发送的)
- 第2-500字节: ❌ 从pl之后的内存地址读取!
这些内存可能包含私钥、密码等敏感数据
(3)功能分析
① 函数声明与变量定义
void process_heartbeat(unsigned char *p, SOCKET s)
{
unsigned short hbtype; // 心跳包类型:1字节
unsigned int payload; // 客户端声明的数据长度
unsigned char *pl; // 指向心跳包实际数据的指针
逻辑解释:
-
p: 指向接收到的网络数据缓冲区的指针 -
s: 网络socket描述符,用于发送响应 -
这三个变量都在栈上分配,不涉及堆内存
② 解析心跳包头部
hbtype = *p++; // 第1步:读取心跳包类型
n2s(p, payload); // 第2步:解析声明的数据长度
pl = p; // 第3步:指针指向实际数据开始位置
详细过程:
假设网络数据缓冲区内容:
偏移: 0 1 2 3 4 5 ...
数据: [0x01][0x01][0xF4][0x41][...][...]...
解释: |类型|长度高位|长度低位|数据'A'|...
执行过程:
1. hbtype = *p++ → 读取p[0]=0x01,然后p指针移动到位置1
(p现在指向0x01,即长度的高字节)
2. n2s(p, payload) → 宏展开:
payload = (p[0] << 8) | p[1] = (0x01 << 8) | 0xF4 = 0x01F4 = 500
然后p += 2,p现在指向位置3(0x41)
3. pl = p → pl现在指向数据'A'(0x41)
③ 类型检查与内存分配
if (hbtype == HB_REQUEST) { // 检查是否是心跳请求
unsigned char *buffer, *bp;
buffer = malloc(1 + 2 + payload); // 分配响应缓冲区
bp = buffer;
逻辑分析:
-
HB_REQUEST通常定义为1 -
分配的内存大小 =
1(类型) +2(长度) +payload(数据) -
对于恶意payload=500,分配503字节
④ 构造响应头部
*bp++ = HB_RESPONSE; // 写入响应类型
s2n(payload, bp); // 写入数据长度(网络字节序)
内存布局变化:
分配后的buffer内存:
开始: [未初始化]...[未初始化] (总共503字节)
↑
bp初始指向这里
执行*bp++ = HB_RESPONSE后:
buffer: [0x02][?][?]...
↑ ↑
写入 bp现在指向这里
执行s2n(payload, bp)后:
buffer: [0x02][0x01][0xF4][?]...
↑ ↑
长度高 长度低 bp现在指向第4字节
⑤ 致命漏洞点:memcpy操作
memcpy(bp, pl, payload); // 复制数据到响应缓冲区
这是漏洞核心:
-
bp: 目标地址,指向buffer中数据区开始位置 -
pl: 源地址,指向接收缓冲区中数据开始位置 -
payload: 要复制的字节数(客户端声明的值)
漏洞触发场景:
实际网络数据只有: [0x01][0x01][0xF4][0x41]
↑
pl指向这里,只有1字节'A'
但payload = 500,所以memcpy会:
1. 从pl位置读取1字节'A' ✅(合法的)
2. 继续读取pl+1位置的499字节 ❌(非法的!)
- 这些内存不属于这次心跳请求
- 可能包含其他连接的敏感数据
- 可能包含SSL私钥片段
⑥ 发送响应
r = write_bytes(s, buffer, 3 + payload);
}
}
攻击完成:
-
发送 3 + 500 = 503 字节的响应包
-
其中包含:1字节类型 + 2字节长度 + 500字节"数据"
-
但500字节中,只有第1字节是客户端实际发送的,其余499字节是服务器内存中的随机数据
(4)形象比喻
你和服务器在进行加密对话。为了保持连接活跃,你可以定期向服务器发送一个"心跳"信号,说:"我还活着,请回复我发给你的数据。"
-
你正常应该说:"心跳,数据长度=5,数据='你好吗'"。
-
服务器会检查长度(5),然后准确地读取5个字符('你好吗'),并原样回复给你。
漏洞在于: 攻击者可以恶意地说:"心跳,数据长度=500,数据='A'。"
-
有漏洞的服务器 不会验证数据长度是否真实。
-
它会读取你发送的1个字符'A',然后从自己内存中紧接着的位置,再读取接下来的499个字符,并将这总共500个字符一起发回给攻击者。
| 正常流程 | 存在漏洞的流程 |
|---|---|
1. 客户端发送心跳包:(数据="Hi", 长度=2) |
1. 恶意客户端发送心跳包:(数据="H", 长度=500) |
| 2. 服务器检查长度=2,从内存中读取2字节数据"Hi"。 | 2. 服务器不验证,仅根据声明的长度=500分配内存。 |
| 3. 服务器将"Hi"(2字节)发回客户端。✅ | 3. 服务器从内存中读取1字节"H"后,继续读取紧随其后的499字节内存内容。 |
| 4. 通信完成。 | 4. 服务器将"H"+499字节敏感内存数据一起发回攻击者。❌ |
① 正常逻辑
// 客户端发送: [HB_REQUEST][length=5][data="hello"]
// 内存: 0x01 0x00 0x05 'h' 'e' 'l' 'l' 'o'
process_heartbeat():
hbtype = 0x01 (HB_REQUEST)
payload = 0x0005 = 5
pl指向'h'
malloc(1+2+5=8字节)
memcpy(bp, pl, 5) → 正常复制"hello"
② 恶意逻辑
// 客户端发送: [HB_REQUEST][length=500][data="A"]
// 内存: 0x01 0x01 0xF4 'A'
process_heartbeat():
hbtype = 0x01 (HB_REQUEST)
payload = 0x01F4 = 500 // ⚠️ 但实际数据只有1字节!
pl指向'A'
malloc(1+2+500=503字节)
memcpy(bp, pl, 500) → ⚠️ 缓冲区过读!
(5)攻击时序图

sequenceDiagram
participant C as 客户端(攻击者)
participant S as 服务器内存
participant P as process_heartbeat函数
C->>S: 发送: [type=1][length=500][data="A"]
Note over S: 网络缓冲区: |0x01|0x01|0xF4|0x41|?|?|...
P->>P: hbtype=0x01, payload=500
P->>P: pl指向0x41位置
P->>S: malloc(503字节)
Note over S: 堆内存分配
P->>S: buffer[0]=0x02 (响应类型)
P->>S: buffer[1]=0x01, buffer[2]=0xF4 (长度)
P->>S: memcpy(bp, pl, 500)
Note over S: 从pl开始读500字节<br/>实际只有1字节有效数据
S-->>P: 读取到: 'A' + 499字节其他内存
P->>C: 发送503字节响应包
Note over C: 获得服务器内存泄露数据
3、漏洞利用的随机性
心脏滴血每次读取的数据不同,主要是因为服务器内存是动态变化的共享资源池。每次攻击读取的是从指定位置开始的一段连续内存,而这块内存中的内容取决于当时系统的运行状态:不同进程的数据、之前连接残留的信息、SSL会话缓存、堆分配器的元数据等都会随机出现在该区域。由于内存分配和释放的时序性,以及多线程/多进程环境下的并发操作,即使对同一服务器发起完全相同的攻击请求,每次读取的内存地址内容也会自然不同,就像从流动的河流中不同位置取水一样,每次获得的水样成分都有差异。
// 为什么每次攻击获取的数据不同?
for (int i = 0; i < 10; i++) {
// 每次连接,内存布局可能不同
process_heartbeat(evil_packet, socket);
// 可能获取的数据:
// 第1次: 用户A的密码
// 第2次: SSL私钥片段
// 第3次: 随机内存(无价值)
// 第4次: HTTP会话cookie
// ...
}
服务器内存是动态变化的:
+------------------------+
| 当前连接1的数据 | ← 第1次攻击读取这里
+------------------------+
| 已被释放的内存 | ← 可能包含之前连接的敏感数据
+------------------------+
| OpenSSL内部结构 | ← 第2次攻击读取这里(可能含私钥)
+------------------------+
| 堆管理信息 | ← 第3次攻击读取这里
+------------------------+
4、漏洞修复
从代码逻辑角度看,这个漏洞是典型的信任边界失效:
-
错误假设:客户端声明的payload长度 == 客户端实际发送的数据长度
-
正确假设:客户端可能撒谎,必须验证
(1)有漏洞的源码
// ❌ 错误:没有验证payload长度
n2s(p, payload);
pl = p;
if (hbtype == HB_REQUEST) {
// 直接信任客户端提供的payload
memcpy(bp, pl, payload); // ⚠️ 可能读取无效内存
}
(2)官方修复方案
// ✅ 正确:添加长度验证
n2s(p, payload);
pl = p;
// 关键修复:验证数据是否足够
if (1 + 2 + payload + 16 > s->s3->rrec.length) {
return 0; // 丢弃异常请求
}
if (hbtype == HB_REQUEST) {
// 现在可以安全使用payload
memcpy(bp, pl, payload);
}
二、vulhub渗透环境搭建
1、确保系统已安装 Docker 和 Docker-Compose
本文使用Vulhub复现心脏滴血漏洞,由于Vulhub 依赖于 Docker 环境,需要确保系统中已经安装并启动了 Docker 服务,命令如下所示。
# 检查 Docker 是否安装
docker --version
docker-compose --version
# 检查 Docker 服务状态
sudo systemctl status docker
2、下载 Vulhub
将 Vulhub 项目克隆到本地,具体命令如下所示。
git clone https://github.com/vulhub/vulhub.git
cd vulhub-master
3、进入漏洞环境
Vulhub 已经准备好现成的漏洞环境,我们只需进入对应目录。注意:docker需要管理员权限运行,故而注意需要切换到root执行后续的docker命令。
cd openssl
cd CVE-2014-0160

4、启动漏洞环境
在CVE-2014-0160目录下,使用docker-compose up -d命令启动环境。Vulhub 的脚本会自动从 Docker Hub 拉取预先构建好的镜像并启动容器
docker-compose build && docker-compose up -d

命令执行后,Docker 会完成拉取一个包含openssl(受影响版本)的镜像。
5、查看环境状态
使用 docker ps 命令确认容器启动状态,说明由Vulhub 项目提供的心脏滴血漏洞镜像容器已正常运行 ,通过宿主机器的8443 端口可访问容器内的443端口,可用于测试CVE-2014-0160漏洞的利用。
ac6b1f74b4f5 vulhub/openssl:1.0.1c-with-nginx "/usr/local/nginx/sb..." 20 minutes ago Up 20 minutes 0.0.0.0:8080->80/tcp, :::8080->80/tcp, 0.0.0.0:8443->443/tcp, :::8443->443/tcp cve-2014-0160_nginx_1
-
ac6b1f74b4f5- 含义 : Docker 容器的唯一 容器ID(缩写)。
-
vulhub/openssl:1.0.1c-with-nginx-
含义 :容器所使用的 Docker 镜像名称和标签。
-
vulhub: 镜像的发布者或组织(Vulhub 是一个著名的漏洞靶场合集项目)。 -
openssl: 镜像的名称,表明其与 OpenSSL 相关。 -
1.0.1c-with-nginx: 镜像的标签,明确指出这个镜像包含了:-
有漏洞的 OpenSSL 版本
1.0.1c(这正是受"心脏滴血"漏洞影响的版本)。 -
同时集成了 Nginx Web 服务器。
-
-
-
"/usr/local/nginx/sb..."- 含义 :这是容器启动时运行的 命令 (这里显示不完整)。它很可能是启动了 Nginx 服务,例如
/usr/local/nginx/sbin/nginx。
- 含义 :这是容器启动时运行的 命令 (这里显示不完整)。它很可能是启动了 Nginx 服务,例如
-
20 minutes ago- 含义 :容器创建于 20 分钟前。
-
Up 20 minutes- 含义 :容器已经持续运行了 20 分钟。
-
端口映射
0.0.0.0:8080->80/tcp, :::8080->80/tcp, 0.0.0.0:8443->443/tcp, :::8443->443/tcp-
这是最关键的部分,说明了如何访问这个漏洞环境:
-
0.0.0.0:8080->80/tcp: 将宿主机的 8080 端口 (IPv4)映射到容器内部的 80 端口(HTTP 服务)。- 访问方式 :在浏览器或攻击工具中,访问
http://你的宿主机IP:8080即可连接到容器内的 Nginx 网页。
- 访问方式 :在浏览器或攻击工具中,访问
-
0.0.0.0:8443->443/tcp: 将宿主机的 8443 端口 (IPv4)映射到容器内部的 443 端口(HTTPS 服务)。- 访问方式 :访问
https://你的宿主机IP:8443即可连接到容器内启用了 SSL/TLS 的网站。心脏滴血漏洞正是通过这个 HTTPS 端口进行利用的。
- 访问方式 :访问
-
:::开头的映射是 IPv6 的对应配置。
-
-
cve-2014-0160_nginx_1-
含义 :这是您为这个容器指定的或 Docker Compose 自动生成的 容器名称。
-
这个名字清楚地表明了它的用途:一个用于 CVE-2014-0160 漏洞的 Nginx 服务容器(可能是由
docker-compose启动的第一个服务实例)。
-

这说明已经成功启动了一个包含漏洞(CVE-2014-0160)的 Nginx 服务器容器。 该容器使用了存在漏洞的 OpenSSL 1.0.1c 版本,并已通过端口映射对外提供服务:
-
HTTP 服务 可通过
http://localhost:8080访问。 -
HTTPS 服务(攻击目标) 可通过
https://localhost:8443访问。
6、获取靶机ip
使用ifconfig获取靶机ip地址,我这里直接安装到kali中,故而ip地址即为宿主机的ip,如下所示ip地址为192.168.59.128。

三、渗透实战
1、nmap探测漏洞
根据vulhub搭建环境心脏滴血容器中的0.0.0.0:8443->443/tcp(通过docker ps胡去哦去) 可知本机的 8443 端口映射靶机的 443端口)。
nmap -sV -p 8443 --script ssl-heartbleed.nse 127.0.0.1
-
-sV:服务版本探测,用于识别目标端口运行的服务类型和版本 -
-p 8443:指定扫描8443端口(这正是之前Docker映射的HTTPS端口) -
--script ssl-heartbleed.nse:加载并执行专门检测心脏滴血漏洞的Nmap脚本 -
127.0.0.1:目标地址(本地主机)

(1)端口分析
8443/tcp open ssl/http nginx 1.11.13
-
端口状态:open(开放且可访问)
-
识别到的服务:ssl/http(HTTP over SSL/TLS)
-
Web服务器:nginx 1.11.13
-
从HTTP头确认:
nginx/1.11.13
(2)漏洞分析
ssl-heartbleed:
VULNERABLE:
The Heartbleed Bug is a serious vulnerability...
-
明确标记为"VULNERABLE"(易受攻击)
-
风险等级:High(高风险)
-
确认OpenSSL版本1.0.1和1.0.2-beta受影响
-
漏洞影响描述准确:可读取受保护系统的内存,泄露加密信息和密钥
2、MSF渗透
msfconsole # 启动Metasploit框架
use auxiliary/scanner/ssl/openssl_heartbleed
# 使用心脏滴血漏洞扫描模块
set RHOST 192.168.59.128
set RPORT 8443
set verbose true
# 设置目标参数
show options
# 查看当前配置(确认所有参数)
(1)msfconsole

(2)search heartbleed

(3)心脏滴血利用模块
use auxiliary/scanner/ssl/openssl_heartbleed
使用Metasploit框架的检测心脏滴血漏洞的辅助扫描模块,它通过向目标SSL端口发送恶意构造的心跳包,根据服务器响应判断是否存在缓冲区过读漏洞,并能提取泄露的内存数据如私钥片段和会话信息
use # Metasploit命令:加载/使用指定模块
└── auxiliary/ # 模块类型:辅助模块(非直接攻击)
└── scanner/ # 子类别:扫描器模块
└── ssl/ # 目标协议:SSL/TLS相关
└── openssl_heartbleed # 具体模块:心脏滴血漏洞检测器

(4)配置靶机ip和端口
set RHOST 192.168.59.128
set RPORT 8443
set verbose true
① 设置目标主机地址
set RHOST 192.168.59.128
-
RHOST = Remote Host(远程主机)
-
目标IP:
192.168.59.128 -
这应该是运行Vulhub漏洞容器的宿主机IP地址(不同于之前的127.0.0.1本地测试)
②设置目标端口
set RPORT 8443
-
RPORT = Remote Port(远程端口)
-
端口:
8443- 这是之前Docker映射的HTTPS端口
③ 启用详细输出模式
set verbose true
-
verbose = 详细模式
-
启用后,Metasploit会显示更详细的执行过程和返回数据

(5)确认配置无误

(6)开启攻击
如下所示,利用 Kali 的 MSF 框架对 OpenSSL"心脏滴血"漏洞 的靶机攻击成功。
