XWiki Platform 路径遍历漏洞分析 | CVE-2025-55747CVE-2025-55748
0x0 背景介绍
XWiki Platform
是一个通用的wiki平台,为构建在其之上的应用程序提供运行时服务XWiki Platform
在特定版本中存在安全漏洞:
CVE-2025-55747
->攻击者可以通过构造特定的URL,利用webjars API
访问并读取系统中的配置文件,泄露敏感信息。
CVE-2025-55748
->配置文件可以通过jsx
和 sx
端点访问,可以访问和读取配置文件。
0x1 环境搭建
1. Ubuntu24+Docker环境复现
- 另存为install.sh并赋予执行权限chmod +x install.sh
#!/bin/bash
echo "部署 Xwiki CVE-2025-55747&CVE-2025-55748"
echo -e "\n======= 重建阶段 ======="
echo "[*] 创建新目录..."
mkdir -p /root/xwiki-Dir && cd /root/xwiki-Dir || exit# 检查端口占用
if lsof -i :8080 >/dev/null; thenecho "[-] 错误:8080端口已被占用"exit 1
fi
# 创建 docker-compose.yml
cat > docker-compose.yml << 'EOF'
version: '2'
networks:bridge:driver: bridge
services:web:image: "xwiki:17.2.2-postgres-tomcat"container_name: xwiki-postgres-tomcat-webdepends_on:- dbports:- "8080:8080"environment:- XWIKI_VERSION=17.2.2- DB_USER=xwiki- DB_PASSWORD=xwiki- DB_HOST=xwiki-postgres-dbvolumes:- xwiki-data:/usr/local/xwikinetworks:- bridgedb:image: "postgres:17"container_name: xwiki-postgres-dbvolumes:- postgres-data:/var/lib/postgresql/dataenvironment:- POSTGRES_ROOT_PASSWORD=xwiki- POSTGRES_PASSWORD=xwiki- POSTGRES_USER=xwiki- POSTGRES_DB=xwiki- POSTGRES_INITDB_ARGS=--encoding=UTF8 --locale-provider=builtin --locale=C.UTF-8networks:- bridge
volumes:postgres-data: {}xwiki-data: {}
EOFecho "[+] 启动环境..."
docker compose up -d
sleep 20
echo -e "\n[√] 服务已就绪"echo -e "\n======= 完成 ======="
echo "访问地址: http://localhost:8080"
echo "默认安装即可,我这装了一个默认主题(比较慢)"
2. 成功页面
0x2 漏洞复现
1) CVE-2025-55747
- YML检测
https://gitee.com/songKl992/cve-series-update/blob/master/CVE/2025/Xwiki-CVE-2025-55747.yml
2) CVE-2025-55748
- YML检测
https://gitee.com/songKl992/cve-series-update/blob/master/CVE/2025/Xwiki-CVE-2025-55748.yml
3) 信息收集脚本
- 同理,对于我这个脚本小子来说,更喜欢一些自动化收集,如下:
https://github.com/Kai-One001/Xwiki--/blob/main/Scan-Xwiki.py
PS:一些解答和遇到的疑惑:
- Q1:为什么不能读取系统文件呢?例如
etc/
这些
答:因为漏洞用的是ClassLoader.getResourceAsStream()
,它只能读classpath
内的资源(如WEB-INF/classes/
- Q2:为什么公开是
xwiki
读取,而这里是直接webjars
答:部署路径不同,部署在/xwiki
路径下 和docker
中部署在/
根下是不同路径
2、复现流量特征 (PACP)
1) CVE-2025-55747
- 读取
xwiki.cfg
文件
2) CVE-2025-55748
- 读取
xwiki.cfg
文件
0x3 漏洞原理分析
1、CVE-2025-55747分析
1) URL 入口
- 文件位置:
org.xwiki.url.ExtendedURL.java
- 关键代码位置:
ExtendedURL.java (第236行)
:URL解码处理 - 找到路径分割与解码方法:
extractPathSegments(String rawPath)
private List<String> extractPathSegments(String rawPath)
{List<String> urlSegments = new ArrayList<>();if (StringUtils.isEmpty(rawPath)) {return urlSegments;}for (String pathSegment : rawPath.split(URL_SEPARATOR, -1)) {// 去除路径参数,如 ;jsessionid=...String normalizedPathSegment = pathSegment.split(";", 2)[0];// 现在开始解码String decodedPathSegment;try {decodedPathSegment = URLDecoder.decode(normalizedPathSegment, UTF8);} catch (UnsupportedEncodingException e) {throw new RuntimeException("Failed to URL decode...", e);}urlSegments.add(decodedPathSegment);}WebJarsResourceReference.javareturn urlSegments;
}
2) 路径拼接
- 文件位置:
org.xwiki.webjars.internal.WebJarsResourceReference.java
- 作用:把解析后的路径段,包装成一个“资源引用”对象,用于后续查找
WebJar
文件 - 关键代码段分析 构造函数:接收
namespace
和resourceSegments
public WebJarsResourceReference(String namespace, List<String> resourceSegments)
{setType(TYPE); // 设置资源类型为 "webjars"this.namespace = namespace;this.resourceSegments = new ArrayList<>(resourceSegments); // ❌ 直接复制,无检查
}
方法 getResourceName():拼接路径public String getResourceName(){return StringUtils.join(getResourceSegments(), RESOURCE_PATH_SEPARATOR);}
3) 资源访问
- 文件位置:
org.xwiki.webjars.internal.WebJarsResourceReferenceHandler.java
- 作用:根据
WebJarsResourceReference
提供的路径,从类加载器中读取文件并返回给用户
@Override
protected InputStream getResourceStream(WebJarsResourceReference resourceReference)
{String resourcePath = String.format("%s%s", WEBJARS_RESOURCE_PREFIX, getResourceName(resourceReference));return getClassLoader(resourceReference.getNamespace()).getResourceAsStream(resourcePath);
}
4) 漏洞流程
URL解析阶段:%2F被解码为/,%3A被解码为 ↓
资源引用创建:解码后的段被直接使用,没有安全检查 ↓
路径组装:使用StringUtils.join()直接拼接,没有重新编码 👇
资源访问:ClassLoader解析相对路径,允许访问WebJars目录之外的文件
2、CVE-2025-55748分析
1) SsxAction.java(入口)
- 文件位置:
com.xpn.xwiki.web.SsxAction
- 作用:处理
/bin/ssx/
请求的入口类 - 关键点:继承
AbstractSxAction
@Component
@Named("ssx")
@Singleton
public class SsxAction extends AbstractSxAction
{/** The extension type of this action. */public static final CssExtension CSSX = new CssExtension();/** Logging helper. */private static final Logger LOGGER = LoggerFactory.getLogger(SsxAction.class);@Overridepublic Extension getExtensionType(){return CSSX;}@Overrideprotected Logger getLogger(){return LOGGER;}
}
@Named("ssx")
→ 对应 URL 路由/bin/ssx/
- 继承自
AbstractSxAction
→ 使用其通用渲染逻辑 getExtensionType()
返回CssExtension
→ 表示这是用于加载.css
类型的资源扩展
2) AbstractSxAction.java(参数获取)- 文件位置:
com.xpn.xwiki.web.sx.AbstractSxAction
- 作用:处理
.css 或 .js
资源的通用逻辑 - 关键方法:
render()
@Overridepublic String render(XWikiContext context) throws XWikiException{SxSource sxSource;if (context.getRequest().getParameter(JAR_RESOURCE_REQUEST_PARAMETER) != null) {sxSource = new SxResourceSource(context.getRequest().getParameter(JAR_RESOURCE_REQUEST_PARAMETER));} else {if (context.getDoc().isNew()) {context.getResponse().setStatus(HttpServletResponse.SC_NOT_FOUND);return "docdoesnotexist";}sxSource = new SxDocumentSource(context, getExtensionType());}try {renderExtension(sxSource, getExtensionType(), context);} catch (IllegalArgumentException e) {// Simply set a 404 status code and return null, so that no unneeded bytes are transferedcontext.getResponse().setStatus(HttpServletResponse.SC_NOT_FOUND);}return null;}
JAR_RESOURCE_REQUEST_PARAMETER
是什么?通常定义为:
public static final String JAR_RESOURCE_REQUEST_PARAMETER = "resource";
- 所以
resourceName
直接来自用户输入 - 没有任何校验:没检查
..、
没解码、没白名单 - 直接传给
SxResourceSource
3) SxResourceSource.java(最终文件读)
- 文件位置:
com.xpn.xwiki.web.sx.SxResourceSource
- 作用:根据路径从
ClassLoader
中读取资源 - 关键方法:
getContent()
@Overridepublic String getContent(){try {// Load from the current context class loader to allow extensions to contribute skin extensions.ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();try (InputStream in = contextClassLoader.getResourceAsStream(this.resourceName)) {return IOUtils.toString(in, StandardCharsets.UTF_8);}} catch (NullPointerException e) {// This happens when the file was not found. Forward an IAE so that the sx action returns 404throw new IllegalArgumentException(e);} catch (IOException e) {return "";}}
1、this.resourceName 就是用户传的…/…/WEB-INF/xwiki.cfg
getResourceAsStream
会解析../
this.resourceName
是用户输入的resource
参数ClassLoader.getResourceAsStream()
会解析相对路径:../../WEB-INF/xwiki.cfg
→ 跳出jar
包,读取webapp
目录下的文件(但也不会抛出异常,只是返回null
)
2、IOUtils.toString(in, UTF_8)
- 使用
Apache Commons IO
将InputStream
转为字符串 - 假设文件是文本格式(如
.cfg, .xml, .properties
) - 如果文件是二进制(如
.jar, .class
),可能会乱码,但依然能传输
0x4 修复建议
修复方案
- 升级到最新版本:目前厂商已发布升级补丁以修复漏洞,补丁获取链接
- 临时缓解措施:
- Tomcat 拦截 :使用
security-constraint
通过web.xml
添加安全约束,禁止对敏感路径的GET
请求; - Tomcat拦截:使用
RewriteValve
基于规则拦截请求; - 使用
Nginx / Apache
反向代理层拦截:制定规则,过滤敏感请求。
- Tomcat 拦截 :使用
免责声明:本文仅用于安全研究目的,未经授权不得用于非法渗透测试活动。