使用污点分析检查 log4j 问题
本文分享自华为云社区《使用污点分析检查log4j问题》,作者: Uncle_Tom。
JNDI 注入
这次 log4j 的问题主要是由于 JNDI 问题造成的,先介绍下 JNDI。
1.1. JNDI
JNDI 是接口服务
Java Naming and Directory Interface(JNDI) 是一个应用程序编程接口 (API),它为使用 Java 编程语言编写的应用程序提供命名和目录功能。各种目录服务都可以以一种通用的、统一的接口方式访问。
JNDI 架构
JNDI 架构由 API 和服务提供者接口 (service provider interface(SPI))组成。 Java 应用程序使用 JNDI API 来访问各种命名和目录服务。 SPI 可以透明地插入各种命名和目录服务,从而允许使用 JNDI API 的 Java 应用程序访问它们的服务。见下图:
JNDI 提供的服务
JNDI 包含在 Java SE 平台中。要使用 JNDI,必须使用 JNDI 类和一个或多个服务提供者。 JDK 包括以下命名/目录服务的服务提供者:
轻量级目录访问协议 (Lightweight Directory Access Protocol (LDAP));
域名服务 (Domain Name Service (DNS));
网络信息服务(Network Information Service(NIS));
名称服务 Java 远程方法调用(Java Remote Method Invocation (RMI));
通用对象请求代理体系结构 (Common Object Request Broker Architecture (CORBA)) 通用对象服务 (Object Services (COS))。
JNDI 的 Java 实现
JNDI 程序包:
javax.naming:包含了访问命名服务的类和接口。例如,它定义了 Context 接口,这是命名服务执行查询的入口。
javax.naming.directory:对命名包的扩充,提供了访问目录服务的类和接口。例如,它为属性增加了新的类,提供了表示目录上下文的 DirContext 接口,定义了检查和更新目录对象的属性的方法。
javax.naming.event:提供了对访问命名和目录服务时的事件通知的支持。例如,定义了 NamingEvent 类,这个类用来表示命名/目录服务产生的事件,定义了侦听 NamingEvents 的 NamingListener 接口。
javax.naming.ldap:这个包提供了对 LDAP 版本 3 扩充的操作和控制的支持,通用包 javax.naming.directory 没有包含这些操作和控制。
javax.naming.spi:这个包提供了一个方法,通过 javax.naming 和有关包动态增加对访问命名和目录服务的支持。这个包是为有兴趣创建服务提供者的开发者提供的。
JNDI 上下文(javax.naming.Context)
javax.naming 包定义了一个 Context 接口,它是查找、绑定/解除绑定、重命名对象以及创建和销毁子上下文的核心接口。
lookup: 最常用的操作是 lookup()。通过 lookup()查找对象的名称,它返回绑定到该名称的对象。
Bindings:listBindings() 返回名称到对象绑定的枚举。绑定是一个元组,包含绑定对象的名称、对象类的名称和对象本身。
list: list() 与 listBindings() 类似,不同之处在于它返回包含对象名称和对象类名称的名称枚举。 list() 对于诸如浏览器之类的应用程序很有用,这些应用程序希望发现有关绑定在上下文中的对象的信息,但不需要所有实际对象。尽管 listBindings() 提供了所有相同的信息,但它可能是一个更昂贵的操作。
Name: Name 是一个表示通用名称的接口, 包含零个或多个组件的有序序列。命名系统使用此接口来定义遵循其约定的名称。
JNDI 初始化
在 JNDI 中,所有命名和目录操作都是相对于上下文执行的,没有绝对的根路径。因此,JNDI 定义了一个 InitialContext,它为命名和目录操作提供了一个起点。通过初始的上下文,就可以使用它来查找其他上下文和对象。
1.2. JNDI 注入
JNDI 通过 InitialContext.lookup(String name)一个字符串参数进行初始化,如果该参数来自不受信任的源,则可能通过远程类加载导致远程代码执行。当请求对象的名称由攻击者控制时,可能会将受害者 Java 应用程序指向恶意 RMI/LDAP/CORBA 服务器,并使用任意对象进行响应。
1.3. log4j 的 JDNI 注入
本次 log4j 的 JNDI 注入问题,就是因为当 log4j 的日志输出为"${xxx}"的时候,log4j 会认为这是个 JNDI 调用的标志,并将 xxx 解析为 JNDI 资源后,加载运行。如果 xxx 为恶意服务地址的时候,危害也就发生了。
污点分析
污点分析(Taint analysis)可以被看作是信息流分析(Information Flow Analysis)的一种,主要是追踪数据在程序中的走向。
在漏洞分析中,使用污点分析技术将所感兴趣的数据(通常来自程序的外部输入)标记为污点数据,然后通过跟踪和污点数据相关的信息的流向,可以知道它们是否会影响某些关键的程序操作,进而挖掘程序漏洞。即将程序是否存在外部输入导致漏洞的问题,转化为污点信息是否会被 Sink 点上的操作所使用的问题。
污点分析技术目前主要有静态和动态分析两种方式。本文主要讲述通过静态分析工具所采用的静态分析方法。
2.1. 污点分析的主要概念
通常外部输入数据都被认为是一种可能引起安全风险的源头,被称为污染源。这些信息被程序的接收或处理函数收到后,在程序内部通过程序内部的各个功能模块被加工、处理或调用某些外部接口执行。这些信息如果未做有效的检验就可能造成各类安全风险。
下图是污染分析在分析污染传播的过程中所经历的主要过程,这个过程被划分为:污染源、污染传播、污染清理、污染爆发。
2.1.1. 污染场景
数据流入系统
外部污染通常由信任域意外传入信任域内,会导致系统运行的问题。主要有:
外部输入:用户界面的输入、外部发来的请求电文、数据库读取的信息;
环境和设置信息:在不安全的环境中运行时,需要从环境中读入的信息。例如:环境变量、配置文件等;
数据内部流转
在程序内部,会有一些因为设计或编码过程中引入的数据范围不当设置或判断,最终导致安全问题。例如除零、数组越界等。
数据流出系统
信任区域内的关键或需要保密的信息,被泄露到信任域外的场景一样可以通过污点分析技术进行分析。例如:
系统内部信息:系统内部的信息往往会暴漏系统的内部结构,这些信息给外部攻击提供了明确的嗅探目标。例如:日志的输出的包含堆栈信息的异常信息等;
敏感信息:敏感信息是需要严格保护的,这些信息的泄露也会给系统带来巨大的损失。例如:个人身份信息、个人财产信息、个人健康信息等。
2.1.2. 污染源
污染的信息会通过应用软件特定的函数被引入到应用系统中,这也是污染分析的起点。静态分析工具中,通常采用下面的方法完成污染源的定义:
通过配置文件完成不同外部污染源的定义,方便静态分析工具在分析时,可配置的加载和分析;
配置文件通过正则表达式或直接指定具体函数的方式定义污染源,通过这种方式匹配程序中使用的函数;
污染源函数的匹配主要包括:命名空间、类名、函数名;
污染源通过函数向下传播的具体变量。通常被定义为匹配到函数的某个参数或返回值;
对于面向对象的程序,需要考虑是否适用于这个匹配函数的继承或派生函数;
通常还会给每个定义加上不同的污染标记,以便在污染清理时,做出不同类型污染的清理判断。
2.1.3. 污染传播
污染传播是污染分析中最复杂的一个阶段,主要分为显示分析和隐式分析。
显示分析:显式分析就是分析污点标记如何随程序中变量之间的数据依赖关系传播。这个分析主要是通过依赖图完成污染的在赋值操作、表达式处理、数组/结构体赋值等函数内的传递分析;再依托函数调用图完成函数间的传播。
隐式分析:是分析污点标记如何随程序中变量之间的控制依赖关系传播,污点标记如何从条件指令传播到其所控制的语句,即污点通过控制运行的分支,达到污染传播的目的。
在显示分析和隐式分析中,都不可避免的会遇到:内置函数、第三方函数,以及应用框架的问题。
内置函数:由于内置函数没有代码,当污染进入某个参数是,分析程序无法知道这个函数的行为,污染是被另一个参数传递出去了?还是被返回值传递出去了?
第三方函数:应用程序通常是以模块的方式完成应用系统组合,特别是目前的应用系统大量引用第三方的模块。当第三方函数是以包的方式引入的时候,就会遇到和内置函数一样的问题,分析程序不知道这些函数的行为。
应用框架:为了降低应用系统开发的难度,通常我们会采用一些成熟的开发框架。这些框架为了应用的灵活性,往往也不直接实现代码间的调用,而是通过配置的方式。例如 spring 框架中事件的驱动方式,是通过配置完成了不同模块间的衔接,这些对于依赖源码的静态分析来说,无法适配彼此间的调用关系。
内置函数、第三方函数,以及应用框架都会造成分析中断的问题,从而使污染分析不能有效的进行。为了解决这个问题,静态分析软件需要定义这些函数,帮助分析程序完成污染传播的继续分析。
这些函数的定义方式基本和污染源的定义方式相同,采用配置的方式匹配到这些函数,同时还需要明确这些函数污染传入的参数和传出的参数。以方便静态分析软件在碰到这些函数时,根据传出参数继续分析。
2.1.4. 污染清理
在编程的过程中,如果对于不安全的外部输入,已经做了相应的处理,但如果静态分析工具无法知道这些检查和处理,让然给出告警,将成为误报,反而会增加分析的成本。所以在程序分析的过程中需要对污染清理做出分析,以便减少不必要的误报,同时还可以提高分析效率。
污染清理主要分为两种处理方式:
有效的条件判断:对输入值的范围做了条件判断,以便剔除不安全的输入;
清理函数:对于一些复杂的判断通过一些函数来完成。这包括:加密函数、转义函数等;
对于清理函数的处理,同样可以采用配置的方式完成。只要对适配到的函数,做污染标记清楚就可以了,使静态分析工具在经过这些函数不再带有继续分析的标记,终止后续的分析。
2.1.5. 污染爆发
当污染经过传播仍然进入到一些特定的函数或语句时,就会引发安全漏洞的发生。这些安全漏洞主要有:
输入验证不当 - (20)
例如:输入中指定数量的不正确验证 - (1284); 输入中指定索引、位置或偏移量的不正确验证 - (1285)等。
可索引资源的错误访问(“范围错误”) - (118)
例如:不检查输入大小的缓冲区复制(“经典缓冲区溢出”) - (120); 整数溢出导致缓冲区溢出 - (680)等。
下游组件使用的输出中特殊元素的转义处理不恰当(“注入”) - (74)
例如:在命令中使用的特殊元素转义处理不恰当(“命令注入”) - (77); SQL 命令中使用的特殊元素转义处理不恰当(“SQL 注入”) - (89)等。
动态管理代码资源控制不当 - (913)
例如:使用外部控制输入来选择类或代码(“不安全反射”) - (470);不可信数据的反序列化 - (502), 这次的 log4j JNDI 问题被 NVD 定义的 CWE。
信息泄露将敏感信息暴露给未经授权的使用者 - (200)
敏感信息的不安全存储 - (922)
log4j 的问题分析
3.1. 测试代码
下载代码
一个测试代码
log4j 的 debug、info、warn、error、fatal 都是污染源
3.2. 调用链
污染分析设置:
污染源: org.apache.logging.log4j.Logger.error 函数,且第一个参数中包含"${" 和 “}”,加这个约束的目的是降低工具的分析难度;
爆发点: JNDI 主要的的调用函数:javax.naming.Context.lookup()函数,且污染进入这个函数的第一个参数,同时查找继承和派生这个函数的函数;
污染传播:java 的内置函数,例如这个问题里用到的 String.substring() 等等;
静态分析工具完整的分析链如下:
图很长,并做了节选,拆成两段:
图 1
图 2
思考
从分析链路来看,log4j 问题经过了非常复杂的链路才传到最后的污染爆发点,一共涉及了 10 几个文件之间的调用,这里还没有放入更详细的类图之间的关系,可见是一个非常复杂的问题;
污染传播的链路中,任何的调用(跨函数、类继承等)或内置函数的不适配,都将导致无法检查出这个问题;
通常情况下,工具不会将 log 做为外部输入,将其设置成污染源;如果从外部输入传入 log,将会使分析链路更长,分析的难度也会更大;
第三方的软件通常以包的形式引入开发工程,做静态检查的时候不会以源码的方式链接到检查代码中。对于静态分析,也就很难发现其中的问题;
这样复杂和深度的检查,是开源静态检查工具能力所无法到达的地方,即使是商用工具也都非常困难;
随着大家对软件安全的重视,比较浅层的安全问题相对比较容易发现,剩下深层次的安全问题,就需要更加强大的静态分析工具,特别需要加大在安全检查能力上的布局。
参考
log4j 网站:
https://logging.apache.org/log4j
Exploiting JNDI Injections in
Java: https://www.veracode.com/blog/research/exploiting-jndi-injections-java
Lesson: Overview of
JNDI: https://docs.oracle.com/javase/tutorial/jndi/overview/index.html
版权声明: 本文为 InfoQ 作者【华为云开发者社区】的原创文章。
原文链接:【http://xie.infoq.cn/article/499e508515fc7a7e367261884】。文章转载请联系作者。
评论