不完美的条件竞争JNDI漏洞利用链发现过程

浅蓝 2022-01-17 10:07:00

0x00 前言

自我发布 《探索高版本 JDK 下 JNDI 漏洞的利用方法》这篇文章后就开始找基于org.apache.catalina.users.MemoryUserDatabaseFactory“创建目录”环节的利用链,因为那篇文章我用的是 h2 数据库的类(对这个背景不了解的可以先看一下那篇文章),不是特别完美。所以我打算再找一个更通用的。我从4ra1n发给我的含有public无参构造扫描结果中挨个看了一遍,然后从中找到了一个非常有趣的链,但实践测试时发现JDK的deploy.jar不会被 Tomcat 加到 classpath。这也就让这条链降低了实战价值,不过我挖掘它的过程非常具有分享的价值,能够在一定程度上对不了解这类问题的人有所启发,所以写了这篇文章供参考。

0x01 发现过程

image-20220115123203080.png

当我在看 com.sun.deploy.cache.Cache 这个类的时候顺手搜了一下 mkdir关键字,发现它的 reset() 方法有去创建目录,cachePath,是从 com.sun.deploy.config.Config#getCacheDirectory获取的路径和 /6.0 拼接组成文件名。

如果我想修改 cachePath 那就要看看com.sun.deploy.config.Config#getCacheDirectory是怎么获取的,还要想办法修改它。

image-20220115123807733.png

跟进该方法,发现它首先调用了一个 get()方法,然后再调用get()返回对象的getProperty(String)方法去查询deployment.user.cachedir

image-20220115125054594.png

跟进get()它去调用了com.sun.deploy.config.DefaultConfig#getDefaultConfig(顺便提一下com.sun.deploy.config.DefaultConfigcom.sun.deploy.config.Config是继承关系),这里有一个Java基础的知识点,它用的是一个线程安全的Lazy(俗称懒汉式)单例模式获取对象,也就是说通过 com.sun.deploy.config.DefaultConfig#getDefaultConfig获取的对象,他们实际上都是同一个对象,如果我修改了这个对象的某个值,那么其他地方调用的这个单例对象获取相应值时也会发生改变。

虽然这个地方看起来没有什么问题,但写过 Java 单例模式的人应该知道,通常单例模式的构造方法要改成 private 修饰的,而DefaultConfig类的构造方法不但是无参构造,还是 public 修饰的,也就意味着我可以不通过 getDefaultConfig() 来实例化DefaultConfig对象,这里是一个重要的知识点,后面会串联起来。

前面讲了deployment.user.cachedir是怎么获取的,因为我需要创建指定的目录,那我就必须修改它。

image-20220115130329920.png

刚好 com.sun.deploy.config.Config类就提供了一个setCacheDirectory(String)方法修改它。同样是先调用了get()方法来获取一个单例的com.sun.deploy.config.DefaultConfig对象,然后调用它的setProperty(String,String)方法来修改deployment.user.cachedir

所以应跟进com.sun.deploy.config.DefaultConfig#setProperty(String,String)

image-20220115131649976.png

这里并没有直接去修改 Key Value,而是加了一个条件判断 _inInit必须是true

再来看看 _inInit是如何赋值的。

image-20220115133934358.png

重点我都标记在了图片上,也就是说当 DefaultConfig对象被实例化的那一瞬间,会让_inInit=true当实例化结束_inInit就变成了 false


让我们把前面的重点都串联起来,整理一下结论。

首先我的目的是调用DefaultConfig.setCacheDirectory("/tmp/test/blueDirectory/"),以修改deployment.user.cachedir的值为/tmp/test/blueDirectory/

∵因为_inInit在正常情况下等于false无法成功修改deployment.user.cachedir

∵又因为_inInit是类静态变量没有做线程安全保护

∵又因为实例化DefaultConfig的一瞬间 _inInit=true

∴所以分别开启多个线程在实例化DefaultConfig对象的同时调用DefaultConfig.setCacheDirectory("/tmp/test/blueDirectory/")就能够让修改deployment.user.cachedir时通过_inInit==true的条件,达成”条件竞争“攻击。

综上所述,已经能够解决修改deployment.user.cachedir问题了。那么在前面com.sun.deploy.cache.Cache#reset方法调用com.sun.deploy.config.Config#getCacheDirectory的返回结果也就会发生改变。

现在只需要最后一个点来触发com.sun.deploy.cache.Cache#reset方法。

让我们再次回到com.sun.deploy.cache.Cache类,去寻找reset()方法的触发点。

image-20220115141937726.png

通过关键词搜索,可以看到reset()方法是在类加载的过程中在 static 代码块里被调用的。

所以要想触发创建目录的操作,就需要 Class.forName 加载com.sun.deploy.cache.Cache类,这个类基本不会被用到,所以一般是没有被加载过的。

0x02 场景模拟

image-20220115142827315.png

public class MakeDir {

    static ExecutorService pool = Executors.newCachedThreadPool();
    static String orginCacheDirectory = Config.getCacheDirectory();
    public static void main(String[] args) throws Exception {
        File file = new File("/tmp/test/");
        File crack = new File(file, "/crack/");
        System.out.println("file count="+file.list().length+" filename="+String.join(",",file.list()));
        System.out.println(crack+" exists="+crack.exists());
        System.out.println("orgin CacheDir="+orginCacheDirectory);
        Runnable runnable = new Runnable(){
            public void run() {
                DefaultConfig defaultConfig = new DefaultConfig();
            }
        };
        Runnable runnable2 = new Runnable(){
            public void run() {
                DefaultConfig.setCacheDirectory("/tmp/test/crack");
            }
        };
        pool.submit(runnable);
        pool.submit(runnable2);
        while (true){
            String newCacheDirectory = Config.getCacheDirectory();
            if (!newCacheDirectory.equals(orginCacheDirectory)){
                System.out.println("changed CacheDir: "+newCacheDirectory);
                break;
            }
        }
        Class<?> cacheClass = Class.forName("com.sun.deploy.cache.Cache");
        System.out.println(cacheClass);
        System.out.println(crack + " exists="+crack.exists());
    }


}

我通过用这段代码来模拟从修改 _inInit到修改CacheDirectory,再到最后的触发目录创建。

因为他们都是按照 JNDI 利用链条件找的,所以 JNDI Reference 对象可以这么写。

image-20220115145703510.png

最后把代码改成 JNDI 请求再模拟一次同样具有效果。

image-20220115150344991.png

当目录创建这条链打通了,就可以去打跨目录写文件的链了

0x03 总结

image-20220115150521982.png

我在 IDE 里能够成功的原因是 classpath 会把 deploy.jar 带上,而 Tomcat 默认不会,所以这条链就没用了,也许只有当某些特殊系统的场景下可能会把 deploy.jar 加到 classpath 里这个链才能体现价值。

另外不管是在挖gadget还是在找漏洞时都可以多关注一些线程安全或是条件竞争类的问题,这类问题通常都不会被注意到。

评论

浅蓝

乌云最帅 没有之一

随机分类

软件安全 文章:17 篇
密码学 文章:13 篇
Ruby安全 文章:2 篇
安全管理 文章:7 篇
事件分析 文章:223 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Article_kelp

因为这里的静态目录访功能应该理解为绑定在static路径下的内置路由,你需要用s

N

Nas

师傅您好!_static_url_path那 flag在当前目录下 通过原型链污

Z

zhangy

你好,为什么我也是用windows2016和win10,但是流量是smb3,加密

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

目录