apache log4j2任意代码执行漏洞POC EXP修复解决方案

正愁这个周没文章可写,结果两天前就曝了一个核弹级的漏洞“log4j RCE”,这两天官方的修补方案也逐渐完善。所以本篇就拿 log4j 作为主题讲一下我的发现。

RCE

log4j RCE 原理已经有挺多人发过了,本文不过多赘述。简单说就是日志在打印时遇到 ${ 后 Interpolator 类按照 : 分割出第一部分作为 prefix 第二部分作为 key。通过 prefix 去找对应的 lookup,再通过对应的 lookup 实例调用 lookup 方法传入 key 作为参数。log4j-core 自带的 lookup 有很多实例,其中就包括了此次存在漏洞的 JndiLookup 实例。JndiLookup 则是直接把传进来的 key 当做 JNDI URL 用 InitialContext.lookup 去访问,从而造成了 JNDI 代码执行漏洞。

WAF bypass

该漏洞曝光后各安全厂商也纷纷推出了解决方案,WAF、RASP、改源码、改配置文件、更新到rc2等。在此次漏洞中最没有防御效果的就是 WAF 了。有提出 ${ 、jndi、ldap、rmi 等关键词规则的防护。但研究后发现都会存在被绕过问题。首先是 jndi、ldap 简直太容易被绕过,只要用 lowerCase upperCase 就可以把关键词分割开。如果是用了正则的话那还可以使用 upper 把 jndı 转成 jndi。注意:这里的 ı(\u0131) 不是 i(\x69)和I(\x49),经过 toUpperCase 就会转变成 I。从而绕过了 jndi 关键词的拦截。


再就是 ${ 关键词的拦截了,虽然这个范围有点大可能会产生一些误报,但鉴于漏洞的严重性还是有很多人建议拦截 ${但这样也未必能够真正的解决,因为漏洞的触发点是在打印日志的时候把可控内容携带进去了。那么可控内容从哪里来?Header、URL、键值对参数、JSON参数、XML参数 ...现在随着 JSON 数据格式的流行,很多系统都在使用 JSON 处理参数,JSON 处理库用的最多的就数 Jackson和fastjson。而 Jackson 和 fastjson 又有 unicode 和 hex 的编码特性。例如:

{"key":"\u0024\u007b"}

{"key":"\x24\u007b"}

这样就避开了数据包中有 ${ 的条件,所以 WAF 的防护规则还要多考虑几种编码。


信息泄露,sys、env 这两个 lookup 的 payload 也在讨论中被频繁提起,实际上他们分别对应的是 System.getProperty() 和 System.getenv(),能够获取一些环境变量和系统属性。部分内容是可以被携带在 dnslog 传出去的。除了 sys、env 以外我还发现 ResourceBundleLookup 也可以获取敏感信息,但没有看到有人讨论 Bundle,所以重点讲一下。

public String lookup(final LogEvent event, final String key) {

    if (key == null) {

        return null;

    }

    final String[] keys = key.split(":");

    final int keyLen = keys.length;

    if (keyLen != 2) {

        LOGGER.warn(LOOKUP, "Bad ResourceBundle key format [{}]. Expected format is BundleName:KeyName.", key);

        return null;

    }

    final String bundleName = keys[0];

    final String bundleKey = keys[1];

    try {

        // The ResourceBundle class caches bundles, no need to cache here.

        return ResourceBundle.getBundle(bundleName).getString(bundleKey);

    } catch (final MissingResourceException e) {

        LOGGER.warn(LOOKUP, "Error looking up ResourceBundle [{}].", bundleName, e);

        return null;

    }

}

从代码上来看就很好理解,把 key 按照 : 分割成两份,第一个是 bundleName 获取 ResourceBundle,第二个是 bundleKey 获取 Properties ValueResourceBundle 在 Java 应用开发中经常被用来做国际化,网站通常会给一段表述的内容翻译成多种语言,比如中文简体、中文繁体、英文。那开发者可能就会使用 ResourceBundle 来分别加载 classpath 下的 zh_CN.properties、en_US.properties。并按照唯一的 key 取出对应的那段文字。例如: zh_CN.properties  LOGIN_SUCCESS=登录成功

那 ResourceBundle.getBundle("zh_CN").getString("LOGIN_SUCCESS") 获取到的就是 登录成功,如果系统是 springboot 的话,它会有一个 application.properties 配置文件。里面存放着这个系统的各项配置,其中有可能就包含 redis、mysql 的配置项。当然也不止 springboot,很多其他类型的系统也会写一些类似 jdbc.properties 的文件来存放配置。这些 properties 文件都可以通过 ResourceBundle 来获取到里面的配置项。所以在 log4j 中 Bundle 是比sys和env更严重的存在。

642.jpg



在不出网的环境下可以通过 dnslog 的方式来外带信息。除了dnslog以外还可以通过这两种方法来获取信息。

640.jpg

641.jpg

分享: