lightless blog - unserialization
type: channel
link: https://www.lightless.me/tag/unserialization/
items: [Java unserialization] Apache Shiro <= 1.2.4 反序列化漏洞分析
items: [Java unserialization] fastjson <= 1.2.24 反序列化漏洞分析
items: [Java unserialization] JRE8u20 反序列化漏洞分析
items: [Java unserialization] JDK7u21 反序列化漏洞分析
items: [Java unserialization] ApacheCommonsCollections 反序列化漏洞分析
items: [Java unserialization] Spring-tx.jar反序列化漏洞分析

[Java unserialization] Apache Shiro <= 1.2.4 反序列化漏洞分析
type: item
date: 2018-07-20T06:48:54+00:00
description: 0x00 Apache Shiro 这个组件的漏洞应该算是很久很久之前就爆出来了，但是最近工作中又遇到了，刚好最近也在看Java反序列化的东西，所以决定拿出来再分析一下，期间也遇到了一些奇怪的问题。 网上的分析文章中大部分都是手动添加了commons-collections4-4.0的依赖，目的是为了使用ysoserial生成的CommonsCollections2这个payload，然而我遇到的情况是使用了CommonsBeanutils1就可以直接打成功，所以这里我们不再重复网上对CommonsCollections2的分析了。 0x01 调试分析 调试环境： JDK 1.8.0_72 Tomcat 8.0.30 首先我们把shiro的源码clone回来，并切到有问题的分支上去。 git clone https://github.com/apache/shiro.git shiro-root cd shiro-root git checkout 1.2.0 为了能让shiro自带的sample跑起来，要对samples/web/pom.xml文件做一些修改，需要将jstl的版本改为1.2，并且删掉servlet-api的scope字段。同时将jstl-1.2.jar放置在WEB-INF/lib下面。然后应该就可以跑起来并且调试了。 我们将断点打到org.apache.shiro.mgt.DefaultSecurityManager中的resolvePrincipals方法，并且发送一个带有rememberMe Cookie的请求，应该就可以断下来了。 我们继续跟进这个getRememberedIdentity方法： 继续一直跟到getRememberedSerializedIdentity方法： 在这个方法中，读出了我们传入的Cookie，并且进行了base64解码： 接下来shiro会调用convertBytesToPrincipals并将base64解码后的字节数组作为参数传入： 这里通过函数名也能猜出来，进行了两个操作，分别是解密和反序列化，我们先来看解密部分，经过简单的调试后，发现是一个AES解密，并且存在一个预设秘钥Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");，在shiro自带的sample中，并没有通过其他的方式设置这个秘钥，所以这里用的就是这个预设值。 而AES解密中遇到的IV也是从我们传入的Cookie中的前几个字节中获取的，于是我们可以轻易的构造出包含任意内容的Cookie值，解密好的明文就是序列化的内容，调用了deserialize进行反序列化。 最终会调用到org.apache.shiro.io.DefaultSerializer#deserialize方法进行反序列化： 整个流程十分简单，简要概括一下就是：读取cookie -> base64解码 -> AES解密 -> 反序列化 所以我们的payload构造起来也是十分的简单，完整的PoC我会放到我的GitHub上。 0x02 疑点解惑 在调试的过程中，遇到了一些问题，并没有成功的弹出计算器，这里记录一下。 1. 为什么本地搭建环境，使用CommonsBeanutils1打不成功，总是提示ClassNotFound异常？ 这个问题我当时调试了好久，一度以为是payload有问题或者shiro的代码因为年代久远有了变化，因为当时遇到的case就是这个payload一发入魂的，表示十分的迷茫。后来发现了关键问题，我们从github上clone到的sample中，commons-beanutils这个依赖的版本是1.8.3，而ysoserial生成的payload的版本是1.9.2，所以在默认的sample中是无法打成功的。于是我修改版本号到1.9.2，一发入魂。 所以遇到的那个案例中，实际的依赖环境版本可能也是这样的吧，所以才能直接打成功。 2. 假如我的依赖中就是没有高版本的commons-beanutils和commons-collections之类的包，怎么利用？ 这个项目clone下来之后，可以看到是存在一个commons-collections的包的： 但是使用ysoserial提供的CommonsCollections1是无法成功的，会爆出一个异常： 这就十分的奇怪了，为什么偏偏无法反序列化byte array类型，调试了一下发现是在这里抛出的异常： 查了一下Java中Class.forName()以及ClassLoader.loadClass()的区别，发现forName()总是使用调用者的ClassLoader()，而loadClass()则是可以自己指定一个不同的ClassLoader。那shi…

[Java unserialization] fastjson <= 1.2.24 反序列化漏洞分析
type: item
date: 2018-07-20T06:47:23+00:00
description: 0x00 fastjson fastjson是一个非常流行的库，可以将数据在JSON和Java Object之间互相转换，但是在2017年官方主动爆出了fastjson的反序列化漏洞以及升级公告，这次我们就学习一下这个漏洞。 最终的payload会放到我的GitHub上。 这次使用的fastjson是1.2.23版本： <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.23</version> </dependency> 在分析漏洞之前，我们先看下这个库都有什么样的功能，我们先创建一个User对象： class User { private int age; public String username; private String secret; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getSecret() { return secret; } } 我们主要关注一下从JSON还原回Object的方法，主要的API有两个，分别是JSON.parseObject和JSON.parse，最主要的区别就是前者返回的是JSONObject而后者返回的是实际类型的对象，当在没有对应类的定义的情况下，通常情况下都会使用JSON.parseObject来获取数据。 fastjson接受的JSON可以通过@type字段来指定该JSON应当还原成何种类型的对象，在反序列化的时候方便操作。 String myJSON = "{\"@type\":\"me.lightless.fastjsonvuln.User\",\"age\":99,\"username\":\"lightless\",\"secret\":\"2333\"}"; JSONObject u3 = JSON.parseObject(myJSON); System.out.println("u3 => " + u3.get("secret")); 如果需要还原出private成员的话，还需要加上Feature.SupportNonPublicField： User u3 = (User) JSON.parseObject(myJSON, User.class, Feature.SupportNonPublicField); 0x01 跟踪分析 根据官方的公告中的WAF检测方法来看，问题很有可能是因为反序列化了任意类型的class从而导致的RCE。 从网上找到的payload中也能看出利用的是TemplatesImpl来执行的命令，这个在之前的JDK7u21中已经分析过了，可能还存在其他的执行命令方法，这些我们暂且不谈，主要来看fastjson的部分。 我们搭建一个简单的Web应用来接受用户POST过来的JSON并且进行反序列化： @RestController public class IndexController { @RequestMapping(value = "/fastjson", method = RequestMethod.GET) public String fastjson() { return "Hello World!"; } @RequestMapping(value = "/fastjson", method = RequestMethod.POST) public JSONObject testVuln(@RequestBody String data) { JSONObject obj = JSON.parseObject(data, Feature.SupportNonPublicField); JSONObject ret = new JSONObject(); ret.put("code", 1001); ret.put("data", "Hello " + obj.get("name")); return ret; } } 传入我们带有@type类型的JSON字符串并且开始调试。在JSON.parseObject处下断并开始向下跟。 一开始会跟进到JSON.parse方法，并且调用了parse()方法继续进行JSON格式…

[Java unserialization] JRE8u20 反序列化漏洞分析
type: item
date: 2018-07-20T06:44:58+00:00
description: 这篇又是一个升华，之前在第一篇文章中我们简单的讲了一下Java的序列化机制，即通过ObjectOutputStream和ObjectInputStream来实现序列化和反序列化，但是内部的机制和原理一并跳过了。 0x00 TLDR 之前在第一篇文章中我们简单的讲了一下Java的序列化机制，即通过ObjectOutputStream和ObjectInputStream来实现序列化和反序列化，但是内部的机制和原理一并跳过了。 JRE8u20这个漏洞是其他人在之前的JDK7u21的基础上进行改进得到了，他绕过了JavaSE后续对AnnotationInvocationHandler类的修复，阻止了这个类反序列化任意类型的对象。 在研究JRE8u20这个漏洞之前，我们有必有对Java的序列化机制进行深入研究了。 0x01 Java序列化机制 关于Java的序列化格式以及协议字段释义可以参考这篇文档，如果想要深入学习序列化机制，强烈建议仔细阅读这篇文档，我们先通过代码来看一下序列化格式： class AuthClass implements Serializable { private static final long serialVersionUID = 100L; private String password; public AuthClass(String password) { this.password = password; } private void readObject(ObjectInputStream ois) throws Exception { ois.defaultReadObject(); if (!this.password.equals("root")) { throw new Exception("Wrong Password."); } } } // 序列化的时候写两次 AuthClass authClass = new AuthClass("123456"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/tmp/authClass.bin")); oos.writeObject(authClass); oos.writeObject(authClass); oos.close(); 将这个类序列化后，得到的内容如下： 00000000: aced 0005 7372 0027 6d65 2e6c 6967 6874 ....sr.'me.light 00000010: 6c65 7373 2e64 6573 6572 6961 6c69 7a65 less.deserialize 00000020: 2e76 756c 6e2e 4175 7468 436c 6173 7300 .vuln.AuthClass. 00000030: 0000 0000 0000 6402 0001 4c00 0870 6173 ......d...L..pas 00000040: 7377 6f72 6474 0012 4c6a 6176 612f 6c61 swordt..Ljava/la 00000050: 6e67 2f53 7472 696e 673b 7870 7400 0631 ng/String;xpt..1 00000060: 3233 3435 36 23456 分析文件的时候，推荐借助SerializationDumper来分析，由于SerializationDumper会帮我们加上一些原本没有的数据帮助我们理解，所以还是需要借助原始的十六进制字节来辅助研究。 前面说过了序列化后的字节流其实是有一些格式的，在我们开始阅读这些字节之前，需要先看几个常用的格式。 TC_STRING，这个表示的是一个字符串，格式如下： TC_STRING newHandle length(2 bytes) value TC_STRGIN 00 08 70 61 73 73 77 6f 72 64 其实newHandle是不会写入文件中的（后面的newHandle同理，也没有实际写入文件），但是在反序列化的时候确实会实际分配这样的4个字节，具体作用后面再看。 TC_OBJECT，表示一个对象，格式如下： TC_OBJECT classDesc newHandle classdata[] classDesc是一个TC_CLASSDESC结构，classdata[]就是对象中实际的数据。 TC_CLASSDESC，是一个用来描述类的结构，主要包括类的名称、有几个成员、每个成员的类型以及成员名等信息。 TC_CLASSDES…

[Java unserialization] JDK7u21 反序列化漏洞分析
type: item
date: 2018-07-20T06:41:08+00:00
description: 这篇文章应当是对Java反序列化漏洞理解的一个分界点吧。。。 0x00 TLDR 在上一次研究ApacheCommonsCollections的时候，由于本地的JRE环境是1.8，导致无法复现网上各位大佬提供的payload，但是在查找资料的过程中发现了ysoserial这个项目，简单的讲就是一个Java反序列化漏洞的利用框架，其中集成了很多针对不同框架的payload。在使用这个框架的过程中，发现了一个JDK7u21的payload，利用的是JDK本身的漏洞，但是如名称所言，这个只在JDK7u21及以前的版本中生效，在经过了多天的调试分析后，发现这个漏洞利用流程非常巧妙复杂，有很多值得学习的地方，漏洞作者也写出了一份writeup放在了gist上。 本以为这个漏洞看起来十分简单，但是实际调试起来却非常困难，足足让本菜鸡调了好几天才彻底搞明白，有一部分也是因为不熟悉Java的某些特性造成的，所以想要学习到东西还是要亲手尝试一下，只是拜读其他大佬的文章并没有什么用，而且总结成文也是再次加深了对漏洞的理解，毕竟想要给大家讲明白就需要自己先搞明白。 纸上得来终觉浅，绝知此事要躬行。 0x01 动态生成Java代码 在着手分析之前，我们先来学习一下前置技能。在这个PoC中，作者通过javassist来动态生成了恶意的gadgets，用来触发命令执行。 public static TemplatesImpl createTemplatesImpl(final String command) throws Exception { // 利用TemplatesImpl类来触发恶意的bytescode final TemplatesImpl templates = new TemplatesImpl(); // 获取容器ClassPool，注入classpath ClassPool pool = ClassPool.getDefault(); System.out.println("insertClassPath: " + new ClassClassPath(StubTransletPayload.class)); pool.insertClassPath(new ClassClassPath(StubTransletPayload.class)); // 获取已经编译好的类 System.out.println("ClassName: " + StubTransletPayload.class.getName()); final CtClass clazz = pool.get(StubTransletPayload.class.getName()); // 在静态的的构造方法中插入payload clazz.makeClassInitializer() .insertAfter("java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\"", "\\\"") + "\");"); // 给payload类设置一个名称 // unique name to allow repeated execution (watch out for PermGen exhaustion) clazz.setName("ysoserial.Pwner" + System.nanoTime()); // 获取该类的字节码 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", new TransformerFactoryImpl()); // 只要触发这个方法就能执行我们注入的bytecodes // templates.getOutputProperties(); return templates; } 相关的说明在注释中已经给出了，通过精心构造一个TemplatesImpl对象，并且想办法触发该对象的getOutputPropertites()方法，就能执行我们构…

[Java unserialization] ApacheCommonsCollections 反序列化漏洞分析
type: item
date: 2018-07-20T06:36:46+00:00
description: 0x00 CommonsCollections 上篇文章讲道了Spring-tx组件出现的问题，通过构造RMI和JNDI来供服务端下载恶意class并通过反序列化进行RCE，这次研究一下另外一种漏洞，利用Java的反射机制来执行任意命令，并且通过反序列化来进行RCE。本次分析的漏洞则是2015年出现的Apache-commons-collections组件出现的反序列化问题，这个包为Java提供了很多基础常用且强大的数据结构，方便开发。 0x01 TransformedMap 看网上的大佬们说这次出现的问题是由于TransformedMap和InvokerTransformer造成的。 TransformedMap这个类是用来对Map进行某些变换用的，例如当我们修改Map中的某个值时，就会触发我们预先定义好的某些操作来对Map进行处理。 Map transformedMap = TransformedMap.decorate(map, keyTransformer, valueTransformer); 通过decorate函数就可以将一个普通的Map转换为一个TransformedMap。第二个参数和第三个参数分别对应当key改变和value改变时需要做的操作；Transformer是一个接口，实现transform(Object input)方法即可进行实际的变换操作，按照如上代码生成transformedMap后，如果修改了其中的任意key或value，都会调用对应的transform方法去进行一些变换操作。 如果想要进行一系列的变换操作，可以通过定义一个ChainedTransformer来实现，只需要传入一个Transformer数组即可： Transformer[] transformers = new Transformer[] { new ConstantTransformer(...), new InvokerTransformer(...) }; Transformer chainedTransformer = new ChainedTransformer(transformers); Map transMap = TransformedMap.decorate(rawMap, null, chainedTransformer); CommonCollections也已经内置了许多常见的transformer，无需手工编写，其中有一个InvokerTransformer十分有趣，可以通过反射的方式去调用任意的函数，也是我们执行命令的关键。 0x02 Run exec 在Java中执行命令一般通过Runtime.getRuntime().exec("command")来执行命令，如果我们想在修改transformedMap时执行命令，就需要构造一个特殊的ChainedTransformer来反射出exec函数。 在构造之前，我们要先看一下ChainedTransformer和InvokerTransformer是如何工作的，下面的代码会触发chainedTransformer开始进行变换： Map normalMap = new HashMap(); normalMap.put("foo", "bar"); Map transformMap = TransformedMap.decorate(normalMap, transformChain, transformChain); Map.Entry entry = (Map.Entry) transformMap.entrySet().iterator().next(); entry.setValue("test"); 最终会调用到org/apache/commons/collections/functors/ChainedTransformer.class中的transform方法。 可以看到这个链中，会将上一次变换的结果作为下一次变换的输入，直到所有的变换完成，并返回最终的object，很容易理解，就不过多赘述了。 下面来看下InvokerTransformer的关键代码 关键部分在于通过getClass()、getMethod、invoke()来进行反射，查找并调用给定的方法。 在我们构造的chain中，最终实现的应当是执行类似于 ((Runtime) Runtime.class.getMethod("getRuntime").invoke()).exec("ifconfig") 这样的代码，所以chain的第一步就是获取Runtime类，可以通过内置的ConstantTransformer来获取，所以chain现在是这个样子： Transformer[…

[Java unserialization] Spring-tx.jar反序列化漏洞分析
type: item
date: 2018-07-20T06:33:00+00:00
description: Java反序列化的相关知识也学习了一些了，由于这是学习反序列化时的第一篇文章，其中有某些概念也时理解的半知不解，但是后面随着深入学习，一些东西也变得明朗了起来，现在回过头来看有些点可能还是有些问题，打算后续重新整理下这些文章，温故知新；这一系列的文章也全部发表到了MLSRC的公众号上，现在也先发到自己的博客上吧； 0x00 反序列化 之前对Java一直不太熟悉，不怎么接触Java安全，不了解Java中序列化与反序列化的一些机制，导致很多Java相关的RCE都看不懂，只知道拿来就用，想了想还是要深入了解一下比较好。 在PHP中我们可以通过serialize和unserialize来进行序列化相关的操作，到了Java的世界里，就没有这么直白的函数可以用了，相关的两个函数分别是： 序列化: ObjectOutputStream.writeObject() 反序列化: ObjectInputStream.readObject() 我们先来看一下最基本的序列化方式： package me.lightless.base; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; /** * Package: me.lightless.base * Author: lightless <root@lightless.me> * Date: 2018/3/29 */ public class Base { public static void main(String[] args) throws Exception { String hello = "Hello World!"; // 序列化并写入文件payload.bin ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("payload.bin")); objectOutputStream.writeObject(hello); objectOutputStream.close(); // 从文件payload.bin中读取数据并反序列化 ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("payload.bin")); String value = (String)objectInputStream.readObject(); objectInputStream.close(); System.out.println("Read value from file: " + value); } } 通过ObjectOutputStream和ObjectInputStream就可以完成最基础的序列化操作。如果想要序列化一个class，那么会稍微复杂一些，需要自己实现Serializable接口，只有实现了这个接口的类才能被序列化，来看一个简单的例子； package me.lightless.base; import java.io.*; /** * Package: me.lightless.base * Author: lightless <root@lightless.me> * Date: 2018/3/29 */ public class ClassBase { public static void main(String[] args) throws Exception { VulnObject vulnObject = new VulnObject(); vulnObject.value = "new_value"; // 序列化并写入文件payload.bin ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("payload.bin")); objectOutputStream.writeObject(vulnObject); objectOutputStream.close(); // 从文件payload.bin中读取数据并反序列化 ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("payload.bin")); …