alibaba-sentinel-SSRF分析

一、SSRF漏洞审计

1、HttpClient

1.1、介绍

HttpClient 是 Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。

HttpClient 实现了 HTTP1.0 和 HTTP1.1。也实现了 HTTP 全部的方法,如:GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE

官方介绍:https://hc.apache.org/httpcomponents-client-5.2.x/index.html

1.2、代码示例

首先,在pom.xml中引入 HttpClient 依赖,记得点击左上角重新加载Mavan变更,最终如下图所示:

1
2
3
4
5
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>

httpclient依赖

然后在src\main\java\com\example\ssrfdemo下新建一个名为HttpClientController的 Java Class,并键入以下代码,最终如下图所示:

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
@RestController
@RequestMapping("/ssrfvul")
public class HttpClientController {

//访问链接:http://ip:port/ssrfvul/httpclient/vul?url=https://www.baidu.com
@GetMapping("/httpclient/vul")
public String HttpClientDemo(@RequestParam String url) throws IOException {

StringBuilder result = new StringBuilder();
//创建 Httpclient 对象
CloseableHttpClient client = HttpClients.createDefault();
//创建 GET 请求
HttpGet httpGet = new HttpGet(url);
//发送请求
HttpResponse httpResponse = client.execute(httpGet);
//获取响应内容
BufferedReader rd = new BufferedReader(new InputStreamReader(httpResponse.getEntity().getContent()));
String line;
while ((line = rd.readLine()) != null) {
result.append(line);
}
return result.toString();
}

}

httpclient代码

在该示例中,使用了execute()方法执行了 HTTP 请求。

启动项目,浏览器访问地址:http://ip:port/ssrfvul/httpclient/vul?url=https://www.baidu.com

2、HttpAsyncClient

2.1、介绍

HttpAsyncClient 是一个异步的 HTTP 客户端开发包,基于 HttpCore NIO 和 HttpClient 组件。

HttpAsyncClient 的出现并不是为了替换 HttpClient,而是作为一个补充用于需要大量并发连接,对性能要求非常高的基于 HTTP 的原生数据通信,而且提供了事件驱动的 API。

官方介绍:https://hc.apache.org/httpcomponents-asyncclient-4.1.x/index.html

2.2、代码示例

首先,在pom.xml中引入 HttpAsyncClient 依赖,最终如下图所示:

1
2
3
4
5
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpasyncclient</artifactId>
<version>4.1.3</version>
</dependency>

httpasyncclient依赖

然后在src\main\java\com\example\ssrfdemo下新建一个名为HttpAsyncClientController的 Java Class,并键入以下代码,最终如下图所示:

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
package com.example.ssrfdemo;

import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.apache.http.util.EntityUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

/**
* 编号7089
*/
@RestController
@RequestMapping("/ssrfvul")
public class HttpAsyncClientController {

//访问链接:http://ip:port/ssrfvul/httpasyncclient/vul?url=https://www.baidu.com
@GetMapping("/httpasyncclient/vul")
public String HttpAsyncClientDemo(@RequestParam String url) throws IOException {
//创建Httpclient对象
CloseableHttpAsyncClient httpclient = HttpAsyncClients.createDefault();
try {
httpclient.start();
final HttpGet request = new HttpGet(url);
//发送请求
Future<HttpResponse> future = httpclient.execute(request, null);
HttpResponse response = future.get();
return EntityUtils.toString(response.getEntity());
} catch (Exception e) {
return e.getMessage();
} finally {
try {
httpclient.close();
} catch (Exception e) {
return e.getMessage();
}
}
}
}

httpasyncclient代码

在该示例中,使用了execute()方法执行了HTTP请求。

启动项目,浏览器访问地址:http://ip:port/ssrfvul/httpasyncclient/vul?url=https://www.baidu.com

3、java.net.URLConnection

3.1、介绍

java.net.URLConnection,是 Java 原生的 HTTP 请求方法。URLConnection 类包含了许多方法可以让你的 URL 在网络上通信。此类的实例既可用于读取URL所引用的资源,也可用于写入URL所引用资源。

官方介绍:https://docs.oracle.com/javase/8/docs/api/java/net/URLConnection.html

3.2、代码示例

java.net.URLConnection不需要额外引入依赖,已封装在JDK中。

然后在src\main\java\com\example\ssrfdemo下新建一个名为UrlConnectionController的 Java Class,并键入以下代码,最终如下图所示:

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
package com.example.ssrfdemo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;


@RestController
@RequestMapping("/ssrfvul")
public class UrlConnectionController {
//访问链接:http://ip:port/ssrfvul/urlconnection/vul?url=https://www.baidu.com
@GetMapping("/urlconnection/vul")
public String UrlConnectionDemo(@RequestParam String url) throws IOException {

StringBuilder result = new StringBuilder();
URL url1 = new URL(url);
URLConnection urlConn = url1.openConnection();
urlConn.connect();
BufferedReader in = new BufferedReader(new InputStreamReader(
urlConn.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
result.append(inputLine);
}
in.close();
return result.toString();
}
}

urlconnection代码

在该示例中,使用了openConnection()方法执行了HTTP请求。

启动项目,浏览器访问地址:http://ip:port/ssrfvul/urlconnection/vul?url=https://www.baidu.com

4、java.net.HttpURLConnection

4.1、介绍

HttpURLConnection 继承自 URLConnection。可以向指定网站发起GET或POST请求。

官方介绍:https://docs.oracle.com/javase/8/docs/api/java/net/HttpURLConnection.html

4.2、代码示例

java.net.HttpURLConnection不需要额外引入依赖,已内嵌在JDK中。

然后在src\main\java\com\example\ssrfdemo下新建一个名为HttpUrlConnectionController的 Java Class,并键入以下代码,最终如下图所示:

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
package com.example.ssrfdemo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

@RestController
@RequestMapping("/ssrfvul")
public class HttpUrlConnectionController {

//访问链接:http://ip:port/ssrfvul/httpurlconnection/vul?url=https://www.baidu.com
@GetMapping("/httpurlconnection/vul")
public String HttpUrlConnectionDemo(@RequestParam String url) throws IOException {

StringBuilder result = new StringBuilder();
URL url1 = new URL(url);
HttpURLConnection connection = (HttpURLConnection) url1.openConnection();
//设置请求方式
connection.setRequestMethod("GET");
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK){
BufferedReader in = new BufferedReader(new InputStreamReader(
connection.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
result.append(inputLine);
}
}
return result.toString();
}
}

httpurlconnection代码

在该示例中,使用了openConnection()方法执行了HTTP请求。

启动项目,浏览器访问地址:http://ip:port/ssrfvul/httpurlconnection/vul?url=https://www.baidu.com

5、java.net.URL

5.1、介绍

在 java.net 包中定义了 URL 类,该类用来处理有关 URL 的内容。通过使用 URL 对象的 openStream()方法创建打开指定 URL 链接,以获取输入流资源内容。

官方介绍:https://docs.oracle.com/javase/8/docs/api/java/net/URL.html

5.2、代码示例

java.net.URL 不需要额外引入依赖,已内嵌在JDK中。

然后在src\main\java\com\example\ssrfdemo下新建一个名为UrlController的 Java Class,并键入以下代码,最终如下图所示:

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
package com.example.ssrfdemo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.*;
import java.net.URL;


@RestController
@RequestMapping("/ssrfvul")
public class UrlController {

//访问链接:http://ip:port/ssrfvul/url/vul?url=https://www.baidu.com
@GetMapping("/url/vul")
public String UrlDemo(@RequestParam String url) throws IOException {

StringBuilder result = new StringBuilder();
URL url1 = new URL(url);
//使用 URL 对象的 openStream()方法创建打开指定 URL 链接,以获取输入流资源内容。
BufferedReader in = new BufferedReader(new InputStreamReader(
url1.openStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
result.append(inputLine);
}
in.close();
return result.toString();
}
}

URL代码

在该示例中,使用了openStream()方法执行了HTTP请求。

启动项目,浏览器访问地址:http://ip:port/ssrfvul/url/vul?url=https://www.baidu.com

6、java.net.Socket

6.1、介绍

java.net.Socket 是 Java 套接字编程使用的类。提供了两台计算机之间的通信机制。在Java代码审计中,我们可能会遇见使用Socket判断IP与端口连通性的代码。如果IP和端口接受外部输入,那么极有可能存在SSRF漏洞。

官方介绍:https://docs.oracle.com/javase/8/docs/api/java/net/Socket.html

6.2、代码示例

java.net.Socket 不需要额外引入依赖,已内嵌在JDK中。

然后在src\main\java\com\example\ssrfdemo下新建一个名为SocketController的 Java Class,并键入以下代码,最终如下图所示:

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
package com.example.ssrfdemo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;

@RestController
@RequestMapping("/ssrfvul")
public class SocketController {

//访问链接:http://ip:port/ssrfvul/socket/vul?url=127.0.0.1&port=8888
@GetMapping("/socket/vul")
public String SocketDemo(@RequestParam String url, int port) throws IOException {

StringBuilder result = new StringBuilder();
Socket ss = new Socket(url,port);
BufferedReader in = new BufferedReader(new InputStreamReader(
ss.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
result.append(inputLine);
}
in.close();
return result.toString();
}
}

Socket代码

在该示例中,在实例化Sokcet时执行了网络请求。

启动项目,浏览器分别访问地址:http://ip:port/ssrfvul/url/vul?url=127.0.0.1&port=8088http://ip:port/ssrfvul/url/vul?url=127.0.0.1&port=9999。其中8088端口是本项目启动的端口。

7、OkHttp

7.1、介绍

OKHttp 是一个网络请求框架,OKHttp会为每个客户端创建自己的连接池和线程池。重用连接和线程可以减少延迟并节省内存。OkHttp中请求方式分为同步请求(client.newCall(request).execute() )和异步请求(client.newCall(request).enqueue())两种。

官方介绍:https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/

7.2、代码示例

首先,在pom.xml中引入 OkHttpClient 依赖,最终如下图所示:

1
2
3
4
5
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.12.0</version>
</dependency>

okhttp引入的依赖

然后在src\main\java\com\example\ssrfdemo下新建一个名为OkHttpClientController的 Java Class,并键入以下代码,最终如下图所示:

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
package com.example.ssrfdemo;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@RestController
@RequestMapping("/ssrfvul")
public class OkHttpClientController {

//访问链接:http://ip:port/ssrfvul/okhttpclient/vul?url=https://www.baidu.com
@GetMapping("/okhttpclient/vul")
public String OkHttpClientDemo(@RequestParam String url){
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.build();
try (Response response = client.newCall(request).execute()) {
return response.body().string();
} catch (IOException e) {
throw new RuntimeException(e);
}


}
}

okhttp代码

如果运行报错:java: 程序包okhttp3不存在,需要访问Settings-->Build-->Build Tools-->Maven-->Runner-->勾选上Delegagte IDE build/run actions to Maven

在该示例中,使用了.newCall(request).execute()方法执行了HTTP请求。

启动项目,访问http://127.0.0.1:8088/ssrfvul/okhttpclient/vul?url=https://www.baidu.com

8、ImageIO

8.1、介绍

ImageIO 是Java读写图片操作的一个类。在代码审计中,如果目标使用了ImageIO.read读取图片,且读取的图片地址可控的话,可能会存在SSRF漏洞。

官方介绍:https://docs.oracle.com/javase/8/docs/api/javax/imageio/ImageIO.html

8.2、代码示例

javax.imageio.ImageIO 不需要额外引入依赖,已封装在JDK中。

然后在src\main\java\com\example\ssrfdemo下新建一个名为ImageIOController的 Java Class,并键入以下代码,最终如下图所示:

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
package com.example.ssrfdemo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.imageio.ImageIO;
import java.awt.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;

@RestController
@RequestMapping("/ssrfvul")
public class ImageIOController {

//访问链接:http://ip:port/ssrfvul/imageio/vul?url=https://www.baidu.com/img/flexible/logo/pc/result.png
@GetMapping("/imageio/vul")
public String ImageioDemo(@RequestParam String url) throws IOException {

StringBuilder result = new StringBuilder();
URL url1 = new URL(url);
Image image = ImageIO.read(url1);
return image.toString();

}
}

imageio代码

在该示例中,使用了ImageIO.read()方法执行了HTTP请求。

启动项目,访问http://ip:port/ssrfvul/imageio/vul?url=https://www.baidu.com/img/flexible/logo/pc/result.png

9、Hutool

9.1、介绍

Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅。

在Hutool中,也实现了HTTP客户端,Hutool-http针对JDK的HttpUrlConnection做一层封装,简化了HTTPS请求、文件上传、Cookie记忆等操作,使Http请求变得无比简单。

Hutool-http的核心集中在两个类:

  • HttpRequest
  • HttpResponse

官方网站:https://www.hutool.cn/

Http客户端官方介绍:https://hutool.cn/docs/#/http/%E6%A6%82%E8%BF%B0

9.2、代码示例

首先,在pom.xml中引入 Hutool依赖,最终如下图所示:

1
2
3
4
5
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.20</version>
</dependency

hutool依赖

然后在src\main\java\com\example\ssrfdemo下新建一个名为HutoolController的 Java Class,并键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.ssrfdemo;

import cn.hutool.http.HttpRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/ssrfvul")
public class HutoolController {

//访问链接:http://ip:port/ssrfvul/hutool/vul?url=https://www.baidu.com
@GetMapping("/hutool/vul")
public String HutoolDemo(@RequestParam String url){

HttpRequest httpRequest = HttpRequest.get(url);
String result = httpRequest.execute().body();
return result;
}
}

hutool代码

在该示例中,使用了execute()方法执行了HTTP请求。

启动项目,访问:http://ip:port/ssrfvul/hutool/vul?url=https://www.baidu.com

10、Jsoup

10.1、介绍

Jsoup 是基于 Java 的 HTML 解析器,可以从指定的 URL 中解析 HTML 内容。

官方介绍:https://jsoup.org/

10.2、代码示例

首先,在pom.xml中引入 jsoup 依赖,最终如下图所示:

1
2
3
4
5
6
<dependency>
<!-- jsoup HTML parser library @ https://jsoup.org/ -->
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.15.3</version>
</dependency>

jsoup依赖

然后在src\main\java\com\example\ssrfdemo下新建一个名为JsoupController的 Java Class,并键入以下代码,最终如下图所示:

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
package com.example.ssrfdemo;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@RestController
@RequestMapping("/ssrfvul")
public class JsoupController {

//访问链接:http://ip:port/ssrfvul/jsoup/vul?url=https://www.baidu.com
@GetMapping("/jsoup/vul")
public String JsoupDemo(@RequestParam String url) throws IOException {

Document doc = Jsoup.connect(url).get();
//String title = doc.title();
//return title.toString();
return doc.toString();
}
}

jsoup代码

在该示例中,使用了Jsoup.connect()方法执行了HTTP请求。

启动项目,访问:http://ip:port/ssrfvul/jsoup/vul?url=https://www.baidu.com

11、RestTemplate

11.1、介绍

RestTemplate 是从Spring3.0 开始支持的一个HTTP 请求工具,它提供了常见的REST请求方案的模版,例如GET 请求、POST 请求、PUT 请求等等。从名称上来看,是更针对RESTFUL风格API设计的。但通过他调用普通的HTTP接口也是可以的。

官方介绍:https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html

11.2、代码示例

首先,在pom.xml中引入 RestTemplate 依赖,RestTemplate 依赖其实就在spring-web这个包下面,引入该依赖即可,如下:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

然后在src\main\java\com\example\ssrfdemo下新建一个名为xxxController的 Java Class,并键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.ssrfdemo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/ssrfvul")
public class RestTemplateController {

//访问链接:http://ip:port/ssrfvul/resttemplate/vul?url=https://www.baidu.com
@GetMapping("/resttemplate/vul")
public String RestTemplateDemo(@RequestParam String url){

RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.getForObject(url ,String.class);
return result;
}
}

RestTemplate代码

在该示例中,使用了getForObject()方法执行了HTTP请求。

启动项目,访问:http://ip:port/ssrfvul/resttemplate/vul?url=https://www.baidu.com

ssrf审计常见思路:基于发起网络请求的关键字

下面关键字是基于上述常用依赖整理出来的,通过关键字可快速定位是否使用了该依赖以及相关HTTP请求方法,具体逻辑还需根据实际代码分析。

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
HttpRequest.get
HttpRequest.post
Jsoup.connect
getForObject
RestTemplate
postForObject
httpclient
execute
HttpClients.createDefault
httpasyncclient
HttpAsyncClients.createDefault
java.net.URLConnection
openConnection
java.net.HttpURLConnection
openStream
Socket
java.net.Socket
okhttp
OkHttpClient
newCall
ImageIO.read
javax.imageio.ImageIO
HttpRequest.get
jsoup
Jsoup.connect
RestTemplate
org.springframework.web.client.RestTemplate

二、Alibaba Sentinel 简介与ssrf漏洞审计

引用自官方介绍

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。

Alibaba Sentinel 官方文档:https://sentinelguard.io/zh-cn/docs/introduction.html

Alibaba Sentinel 新手指南:https://github.com/alibaba/Sentinel/wiki/%E6%96%B0%E6%89%8B%E6%8C%87%E5%8D%97

CVE 编号为 CVE-2021-44139,该漏洞来自 threedr3am 大师傅的随手贡献之作(https://threedr3am.github.io/about/)

threedr3am 大师傅的报告原文:https://github.com/alibaba/Sentinel/issues/2451

漏洞源码:https://github.com/alibaba/Sentinel/releases/tag/v1.8.0

审计过程

1.漏洞入参跟踪

漏洞触发点在 MetricFetcher 类中,位于sentinel-dashboard\src\main\java\com\alibaba\csp\sentinel\dashboard\metric\MetricFetcher.java

Sentinel Dashboard 项目中 MetricFetcher 类的实现主要是用于获取机器的指标信息,该类包含了以下几个主要方法:

  • MetricFetcher():构造函数,初始化一些参数,包括创建了线程池、HTTP 客户端、启动定时任务 fetchScheduleService。
  • start():启动 fetchScheduleService 定时任务,通过调用 fetchAllApp 方法获取所有应用程序的指标数据。
  • writeMetric():将机器指标数据写入到 repository 中。
  • fetchAllApp():获取所有应用程序的指标数据。该方法会调用 fetchMachine 方法,获取应用程序的所有机器指标数据,然后将机器的指标数据聚合,将结果写入 repository 中。
  • fetchMachine():获取应用程序下所有机器的指标数据。该方法通过使用 HTTP 协议调用机器提供的 /metric 接口获取指标数据,并将结果保存到 map 中。
  • fetchOnce():该方法会向给定的地址发送 HTTP GET 请求,该地址由应用程序的管理类 AppManagement 提供。然后使用回调函数来处理异步的 HTTP 响应,该响应包含了度量数据。在获取到响应后,该方法会解析响应的内容,将其中的度量数据保存在内存仓库中,以便后续使用。

调用关系大致为:start() –> fetchAllApp() –> doFetchAppMetric() –> fetchOnce(),其中 fetchOnce() 执行 HTTP GET 请求,进而触发 SSRF 漏洞。

① MetricFetcher 类中查看 fetchOnce() 方法,漏洞触发点位于第 212 和 214 行,漏洞代码如下:

image-20240922165501883

HttpGet 是 Apache HttpClient 库中的一个类,用于创建 HTTP GET 请求,最终使用 execute 方法执行 HTTP 请求

向上追踪发现 String url 中从 machine.getIp() 和 machine.getPort() 获取了 IP 地址和端口号

② machine 是从 for-each 循环中的 machines 获取值后将值赋予给 machine

image-20240922165733847

继续追踪 machines,发现在第 182 行处从 appInfo.getMachines(); 处获取的值,期间就做了一个判空处理

image-20240922165831875

③ 追踪appInfo.getMachines(); 方法,在这段代码中 getMachines 中 return 了 machines(回溯代码来看这简单来说应该就是机器列表信息)。而变量 machines 使用了 ConcurrentHashMap.newKeySet() 方法来创建一个线程安全的 Set(集合),其中 machines 值是由 addMachine 方法添加进去的,ConcurrentHashMap 的读取操作不会被写入操作所阻塞,这意味着在添加机器信息的同时,其他线程可以安全地读取 machines 中的信息,因此,在别的代码中可以安全地使用 getMachines() 获取添加的机器信息

image-20240922165911236

④ 追踪addMachine方法,SimpleMachineDiscovery 类重写了 addMachine 方法

image-20240922170114246

⑤ 继续追踪 addMachine 方法,继续查看调用关系,按住 Ctrl 加鼠标左键,可以看到 MachineRegistryController 和 AppManagement 都对齐有所调用,但仔细看会发现 MachineRegistryController 处的调用即是 appManagement.addMachine,所以进入那个最终都是可以到 MachineRegistryController 层的,如下图所示:

image-20240922170210631

⑥ 追踪进入MachineRegistryController,其主要作用是,获取请求中的参数,并进行相应的处理和判断,最终将机器信息添加到应用管理中,并返回注册结果。下面我们关注第 42 到 76行,通过代码可以得出接口地址为 /registry/machine,得到传入参数有 app,appType,version,v,hostname,ip,port,并对传入的 app,ip 和 port 参数进行了判断是否为 null 的操作,如下图所示:

image-20240922170415486

将从请求中获取到的数据,分别设置成 machineInfo 的属性值,最后调用appManagement.addMachine(machineInfo);方法添加注册机器信息

总结下大致流程就是:参数从 MachineRegistryController 传进来,其中涉及 IP 和 port,通过 appManagement.addMachine(machineInfo); 方法添加机器信息,最终在 MetricFetcher 中使用了 start() 方法定时执行任务,其中有个任务是调用 fetchOnce 方法执行 HTTP GET 请求。

2.漏洞如何触发

调用关系大致为:start() –> fetchAllApp() –> doFetchAppMetric() –> fetchOnce(),其中 fetchOnce() 执行 HTTP GET 请求,进而触发 SSRF 漏洞

其中start()方法在构造函数MetricFetcher()中被调用,且每隔10秒钟执行一次

image-20240922170958021

跟踪构造方法发现没有地方调用。看类的上方有注解@Component,该类会被自动注册到spring容器中成为一个bean,不需要再显示的注册,所以在程序运行后,该类的构造方法会被自动调用,从而调用了start()方法

image-20240922171242033

3.漏洞为何是未授权

在配置文件中可以看到,作者定义了不需要鉴权的路径,其中就包括ssrf漏洞接口/registry/machine。所以不需要登录也能直接触发

image-20240922170724889

image-20240922171425899

漏洞验证

在本地通过nc -lvvp 1778监听端口,构造漏洞接口http://127.0.0.1:8080/registry/machine?app=SSRF-TEST&appType=0&version=0&hostname=TEST&ip=xxx&port=1778,接收到请求信息,如下图所示

image-20240922171617574

三、SSRF漏洞修复

1、安全代码参考来源:https://github.com/j3ers3/Hello-Java-Sec

省事不打开版。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 判断是否是http类型
public static boolean isHttp(String url) {
return url.startsWith("http://") || url.startsWith("https://");
}

// 判断是否为内网
public static boolean isIntranet(String url) {
Pattern reg = Pattern.compile("^(127\\.0\\.0\\.1)|(localhost)|(10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})|(172\\.((1[6-9])|(2\\d)|(3[01]))\\.\\d{1,3}\\.\\d{1,3})|(192\\.168\\.\\d{1,3}\\.\\d{1,3})$");
Matcher match = reg.matcher(url);
Boolean a = match.find();
return a;
}

// 不允许跳转或判断跳转
HttpURLConnection conn = (HttpURLConnection) u.openConnection();
conn.setInstanceFollowRedirects(false); // 不允许重定向或者对重定向后的地址做二次判断
conn.connect();

2、安全代码参考来源:https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/security/SecurityUtil.java

省事不打开版。

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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
package org.joychou.security;

import org.joychou.config.WebConfig;
import org.joychou.security.ssrf.SSRFChecker;
import org.joychou.security.ssrf.SocketHook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.regex.Pattern;


public class SecurityUtil {

private static final Pattern FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9_/\\.-]+$");
private static Logger logger = LoggerFactory.getLogger(SecurityUtil.class);


/**
* Determine if the URL starts with HTTP.
*
* @param url url
* @return true or false
*/
public static boolean isHttp(String url) {
return url.startsWith("http://") || url.startsWith("https://");
}


/**
* Get http url host.
*
* @param url url
* @return host
*/
public static String gethost(String url) {
try {
URI uri = new URI(url);
return uri.getHost().toLowerCase();
} catch (URISyntaxException e) {
return "";
}
}


/**
* 同时支持一级域名和多级域名,相关配置在resources目录下url/url_safe_domain.xml文件。
* 优先判断黑名单,如果满足黑名单return null。
*
* @param url the url need to check
* @return Safe url returns original url; Illegal url returns null;
*/
public static String checkURL(String url) {

if (null == url){
return null;
}

ArrayList<String> safeDomains = WebConfig.getSafeDomains();
ArrayList<String> blockDomains = WebConfig.getBlockDomains();

try {
String host = gethost(url);

// 必须http/https
if (!isHttp(url)) {
return null;
}

// 如果满足黑名单返回null
if (blockDomains.contains(host)){
return null;
}
for(String blockDomain: blockDomains) {
if(host.endsWith("." + blockDomain)) {
return null;
}
}

// 支持多级域名
if (safeDomains.contains(host)){
return url;
}

// 支持一级域名
for(String safedomain: safeDomains) {
if(host.endsWith("." + safedomain)) {
return url;
}
}
return null;
} catch (NullPointerException e) {
logger.error(e.toString());
return null;
}
}


/**
* 通过自定义白名单域名处理SSRF漏洞。如果URL范围收敛,强烈建议使用该方案。
* 这是最简单也最有效的修复方式。因为SSRF都是发起URL请求时造成,大多数场景是图片场景,一般图片的域名都是CDN或者OSS等,所以限定域名白名单即可完成SSRF漏洞修复。
*
* @author JoyChou @ 2020-03-30
* @param url 需要校验的url
* @return Safe url returns true. Dangerous url returns false.
*/
public static boolean checkSSRFByWhitehosts(String url) {
return SSRFChecker.checkURLFckSSRF(url);
}


/**
* 解析URL的IP,判断IP是否是内网IP。如果有重定向跳转,循环解析重定向跳转的IP。不建议使用该方案。
*
* 存在的问题:
* 1、会主动发起请求,可能会有性能问题
* 2、设置重定向跳转为第一次302不跳转,第二次302跳转到内网IP 即可绕过该防御方案
* 3、TTL设置为0会被绕过
*
* @param url check的url
* @return 安全返回true,危险返回false
*/
@Deprecated
public static boolean checkSSRF(String url) {
int checkTimes = 10;
return SSRFChecker.checkSSRF(url, checkTimes);
}


/**
* 不能使用白名单的情况下建议使用该方案。前提是禁用重定向并且TTL默认不为0。
*
* 存在问题:
* 1、TTL为0会被绕过
* 2、使用重定向可绕过
*
* @param url The url that needs to check.
* @return Safe url returns true. Dangerous url returns false.
*/
public static boolean checkSSRFWithoutRedirect(String url) {
if(url == null) {
return false;
}
return !SSRFChecker.isInternalIpByUrl(url);
}

/**
* Check ssrf by hook socket. Start socket hook.
*
* @author liergou @ 2020-04-04 02:15
*/
public static void startSSRFHook() throws IOException {
SocketHook.startHook();
}

/**
* Close socket hook.
*
* @author liergou @ 2020-04-04 02:15
**/
public static void stopSSRFHook(){
SocketHook.stopHook();
}



/**
* Filter file path to prevent path traversal vulns.
*
* @param filepath file path
* @return illegal file path return null
*/
public static String pathFilter(String filepath) {
String temp = filepath;

// use while to sovle multi urlencode
while (temp.indexOf('%') != -1) {
try {
temp = URLDecoder.decode(temp, "utf-8");
} catch (UnsupportedEncodingException e) {
logger.info("Unsupported encoding exception: " + filepath);
return null;
} catch (Exception e) {
logger.info(e.toString());
return null;
}
}

if (temp.contains("..") || temp.charAt(0) == '/') {
return null;
}

return filepath;
}


public static String cmdFilter(String input) {
if (!FILTER_PATTERN.matcher(input).matches()) {
return null;
}

return input;
}


/**
* 过滤mybatis中order by不能用#的情况。
* 严格限制用户输入只能包含<code>a-zA-Z0-9_-.</code>字符。
*
* @param sql sql
* @return 安全sql,否则返回null
*/
public static String sqlFilter(String sql) {
if (!FILTER_PATTERN.matcher(sql).matches()) {
return null;
}
return sql;
}

/**
* 将非<code>0-9a-zA-Z/-.</code>的字符替换为空
*
* @param str 字符串
* @return 被过滤的字符串
*/
public static String replaceSpecialStr(String str) {
StringBuilder sb = new StringBuilder();
str = str.toLowerCase();
for(int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
// 如果是0-9
if (ch >= 48 && ch <= 57 ){
sb.append(ch);
}
// 如果是a-z
else if(ch >= 97 && ch <= 122) {
sb.append(ch);
}
else if(ch == '/' || ch == '.' || ch == '-'){
sb.append(ch);
}
}

return sb.toString();
}

public static void main(String[] args) {
}

}

alibaba-sentinel-SSRF分析
http://example.com/2024/09/22/alibaba-sentinel-SSRF分析/
作者
r1
发布于
2024年9月22日
许可协议