写点什么

升级过 log4j,却还没搞懂 log4j 漏洞的本质?

  • 2021 年 12 月 21 日
  • 本文字数:3542 字

    阅读完需:约 12 分钟

​​摘要:log4j 远程代码漏洞问题被大范围曝光后已经有一段时间了,今天完整讲清 JNDI 和 RMI 以及该漏洞的深层原因。


本文分享自华为云社区《升级过log4j,却还没搞懂log4j漏洞的本质?为你完整讲清jndi、rmi以及该漏洞的深层原因!》,作者:breakDraw。


log4j 远程代码漏洞问题被大范围曝光后已经有一段时间了。

很多人只能看到一个“弹出一个计算器”的演示,于是内心想着“哦,就是执行任意代码,启动个计算器”,却对这个漏洞的原理不甚了解。


而对于 java 开发应用不是非常深的同学来讲,jndi、rmi 更是很陌生的名词。

这里会以不断提问的方式,逐步推进这个问题的解答,一步步揭开这个漏洞的本质,并给出对这个漏洞的思考。


Q:log4j 里的”${}“符号是什么?有什么用?

A:可以通过 ${}的方式,打印一些特殊的值到日志中。

例如 ${hostName}就可以打印主机名

${java:vm}打印 jvm 信息

${thread:threadName}就可以打印线程名

当你把这个值作为日志的参数,就会打印出来这些值而非原参数名字。

可以理解为 log4j 的功能更强大了,不需要自己写 java 代码来打印这些信息,直接用一个字符串就能搞定这些打印。

上面这些都是要实现对应的 Lookup 类才能做的,即要么 log4j 内置,要么我们自己新增。


Q:上面这个打印本机信息的是漏洞的原因吗?看起来好象可以在机器里执行奇怪的命令?或者查看文件路径?

A:不是的。上面这些 lookup,都是事先定义好的一些 loopup 字符,并不能做任意的事情!而且就算你发了这些 ${java.vm}啥的,也只能在服务端打印和收集,你作为攻击者,是收集不到这些信息的

真正的原因,是因为 log4j 支持的 ${jndi:xxxx},即支持 jndi 进行 lookup 来寻找对象并打印。


Q:什么是 JNDI?

A:JavaNamingandDirectoryInterface(JAVA 命名和目录接口)

简单说就是可以通过 JNDI,在 java 环境中用一个名字,去 lookup 寻找一个东西使用。

例如可以直接在自己的 Java 环境中配置一个数据库连接,名字叫“java:MySqlDS”然后别的 java 进程通过 jndi 去查找”java:MysqlDs“,接着就会得到一个数据库连接。这样如果 1 个机器有多个进程,都要用同一个连接,完全可以修改整个 java 环境的 jndi 数据库对象,然后其他进程就能同时生效了。


Connection conn=null; 
// Context就是jdni的类Context ctx = new InitialContext(); // jndi关键方法,通过loopup找一个对象Object datasourceRef = ctx.lookup("java:MySqlDS"); //引用数据源 DataSource ds = (Datasource) datasourceRef; conn = ds.getConnection(); ...... c.close();
复制代码


除了数据库连接,他还支持 loopup 找 dns,可以弄一个 dnsContext 然后寻找”sun.com“对应的 dns 对象使用JNDI进行高级DNS查询

这样 log4j 里就可以通过 ${jndi:dns:huaweicloud.com}来获取当前机器中 huaweicloud.com 对应的域名对象进行打印,来确认网络请求失败时,是否是 dns 获取有问题。

这也就是 log4j 为啥要引入 jndi 的原因,可以更方便地获取一些可打印的对象进行日志统计。

然而,jndi 还支持通过 RMI/LDAP+url 字符串,来寻找并获取一个远程对象。这个寻找远程对象的操作,就是此次漏洞的核心问题所在。


这里只讲 RMI。LDAP 类似,就不再论述。


Q:RMI 是什么?

A:RMI,RemoteMethodInvocation。

具体含义:

  • 远程服务器实现具体的 Java 方法并提供接口

  • 客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法

在 RMI 中,实际上就是返回了一个 stub(桩)调用对象给客户端,然后客户都用这个 stub 对象去做远程调用。

这样客户端就不用关心背后网络怎么写的

甚至不用知道对方服务是什么端口或者 ip

因此也不需要写 sokect 的一堆方法搞半天了,也避免了总是修改访问的 url 啥的。

具体过程如下:




    1. Server 端监听一个端口,这个端口是 JVM 随机选择的;

    2. Client 端并不知道 Server 远程对象的通信地址和端口,但是 Stub 中包含了这些信息,并封装了底层网络操作;

    3. Client 端可以调用 Stub 上的方法;

    4. Stub 连接到 Server 端监听的通信端口并提交参数;

    5. 远程 Server 端上执行具体的方法,并返回结果给 Stub;

    6. Stub 返回执行结果给 Client 端,从 Client 看来就好像是 Stub 在本地执行了这个方法一样;


    Q:RMI 客户端不需要关心服务端的监听端口?那客户端从哪里拿到 stub 对象呢?总不可能凭空生成吧

    A:服务端那边可以启动一个 RMI 注册中心服务 RMIRegistry,端口设置为统一的 1099,ip 也是固定的。

    然后当客户端希望拿到某个服务例如订单服务 order 的 stub 对象时,就用”order“这个名字到 RMI 注册中心上去请求这个 stub 这样的话,客户端只需要知道 RMI 注册中心即可,不需要知道其他服务的 ip、端口,非常节省管理成本。


    服务端代码长这样:


    // 建立一个订单服务通信桩OrderServerStub stub = new OrderServerStub();// 启动一个RMI注册中心, 端口为1099LocateRegistry.createRegistry(1099);// 把OrderServer这个桩,注册到rmi://0.0.0.0:1099/order这个url上Naming.bind("rmi://0.0.0.0:1099/order", stub);
    复制代码

    客户端的代码长这样,可以看到一个 loopup 就把这个桩找过来了。然后就能直接调用 stub 里的 queryOrder 方法查询订单了!


    Registry registry = LocateRegistry.getRegistry("kingx_kali_host",1099);OrderServerStub stub = (OrderServerStub) registry.lookup("hello");stub.queryOrder("aaa");
    复制代码



    Q:那 JNDI 和 RMI 又是什么关系?怎么就联系到一起了

    A:上面的代码里,可以看到 RMI 需要自己写一段 Java 代码执行。如果以后你不用 RMI 来存这个通信对象了,而是用 LDAP 之类的,咋办?难道代码都要重新写然后部署一份吗?

    而如果能用 JNDI 的方式,通过一个小小的字符串,就能拿到,那就简单了。那么当我需要切换通信对象的获取方式时,切换 JDNI 里的设置即可。

    而 RMI 正好实现了 JNDI 的 spi 接口,以至于能支持用 JNDI+字符串去获取对象


    这里贴一下 SPI 的概念:


    SPI,全称为 ServiceProviderInterface,是一种服务发现机制。它通过在 ClassPath 路径下的 META-INF/services 文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如在 Dubbo、JDBC 中都使用到了 SPI 机制


    说人话,spi 就是框架方提供一个 interface 接口,然后只要有人在服务的 class 发现路径下写一个实现类,就能在代码里直接用上。


    而 log4j 里,正好就支持用 ${jndi:rmi:x.x.x.x:1099/path}的方式进行 RMI 对象的获取。

    log4j 开发者可能本意只是方便用 jndi 获取各种 java 容器内置对象,没想到忽略了 rmi 的获取方式。

    这就导致了我们的服务可能会访问黑客部署的 RMI 服务,获取到一个不可信的远程调用对象。


    Q:但是刚才提到,我们只会通过 RMI 去拿到一个 stub,stub 里的内容仅仅是通过特定的 ip+port 去做发送,代码是固定的。再怎么恶意的命令,也只会在 RMI 注册中心即黑客的服务器上执行,怎么就在我这边触发了攻击?

    而且这个 stub 对象的 class 文件在我们服务器本地并没有,难道不会报 classNotFind 异常吗?


    A:某个讲 RMI 注入的文章里这样说道:

    RMI 服务端除了直接绑定远程对象之外,还可以通过 References 引用类来绑定一个外部的远程对象(当前名称目录系统之外的对象)。绑定了 Reference 之后,服务端会先通过 Referenceable.getReference()获取绑定对象的引用,并且在目录中保存。当客户端在 lookup()查找这个远程对象时,客户端会获取相应的 objectfactory,最终通过 factory 类将 reference 转换为具体的对象实例。


    • 说人话,就是 RMI 允许客户端的 java 环境中没有这个 stub 对象

    • RMI 服务端(那个 1099 端口的服务)他会返回给你一个 factory(序列化传过来),让你调用这个 factory 做转换。而这个可被序列化生成的 factory 就是问题的根本原因。


    整个利用流程如下:

    1. 目标代码中调用了 InitialContext.lookup(URI),且 URI 为用户可控;

    2. 攻击者控制 URI 参数为恶意的 RMI 服务地址,如:rmi://hacker_rmi_server//name;

    3. 攻击者 RMI 服务器向目标返回一个 Reference 对象,Reference 对象中指定某个精心构造的 Factory 类;

    4. 目标在进行 lookup()操作时,会动态加载并实例化 Factory 类,接着调用 factory.getObjectInstance()获取外部远程对象实例;

    5. 攻击者可以在 Factory 类文件的构造方法、静态代码块、getObjectInstance()方法等处写入恶意代码,达到 RCE 的效果;


    Q:那么 log4j-core2.15 版本又是怎么改的呢?

    A:限定 jndi 使用的协议,禁止在 jndi 中用 ldap、rmi 去调用一些远端的服务。

    思考


    说实话,这个漏洞影响之所以这么大,就是因为原理太过简单,随便发一段 rmi 注册中心的 demo 和客户端调用 demo 给别人,他就能复现,甚至用这个方式去攻击。


    为什么 log4j 的设计者当时没有考虑到呢?很大概率可能是因为 jndi 的 spi 机制扩展性太强。也许最初,jndi 只支持 dns、数据库 driver 等对象的命名获取。


    但是后来随着版本更新,JNDP 通过 SPI 机制,支持了 RMI、LDAP 等实现,而这个是 log4j 开发者当时没考虑到的。


    换句话说,这是 java 高可扩展性和安全性的一次冲突,因此 JNDI 的调用方式,未来应该会被更加谨慎地使用了。


    点击关注,第一时间了解华为云新鲜技术~

    发布于: 40 分钟前阅读数: 5
    用户头像

    提供全面深入的云计算技术干货 2020.07.14 加入

    华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

    评论

    发布
    暂无评论
    升级过log4j,却还没搞懂log4j漏洞的本质?