一、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>
|

然后在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 {
@GetMapping("/httpclient/vul") public String HttpClientDemo(@RequestParam String url) throws IOException {
StringBuilder result = new StringBuilder(); CloseableHttpClient client = HttpClients.createDefault(); 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(); }
}
|

在该示例中,使用了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>
|

然后在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;
@RestController @RequestMapping("/ssrfvul") public class HttpAsyncClientController {
@GetMapping("/httpasyncclient/vul") public String HttpAsyncClientDemo(@RequestParam String url) throws IOException { 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(); } } } }
|

在该示例中,使用了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 { @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(); } }
|

在该示例中,使用了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 {
@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(); } }
|

在该示例中,使用了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 {
@GetMapping("/url/vul") public String UrlDemo(@RequestParam String url) throws IOException {
StringBuilder result = new StringBuilder(); URL url1 = new URL(url); BufferedReader in = new BufferedReader(new InputStreamReader( url1.openStream())); String inputLine; while ((inputLine = in.readLine()) != null) { result.append(inputLine); } in.close(); return result.toString(); } }
|

在该示例中,使用了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 {
@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(); } }
|

在该示例中,在实例化Sokcet时执行了网络请求。
启动项目,浏览器分别访问地址:http://ip:port/ssrfvul/url/vul?url=127.0.0.1&port=8088
和http://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>
|

然后在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 {
@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); }
} }
|

如果运行报错: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 {
@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.read()
方法执行了HTTP请求。
启动项目,访问http://ip:port/ssrfvul/imageio/vul?url=https://www.baidu.com/img/flexible/logo/pc/result.png
。
9.1、介绍
Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅。
在Hutool中,也实现了HTTP客户端,Hutool-http针对JDK的HttpUrlConnection做一层封装,简化了HTTPS请求、文件上传、Cookie记忆等操作,使Http请求变得无比简单。
Hutool-http的核心集中在两个类:
官方网站: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
|

然后在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 {
@GetMapping("/hutool/vul") public String HutoolDemo(@RequestParam String url){
HttpRequest httpRequest = HttpRequest.get(url); String result = httpRequest.execute().body(); return result; } }
|

在该示例中,使用了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: <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.15.3</version> </dependency>
|

然后在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 {
@GetMapping("/jsoup/vul") public String JsoupDemo(@RequestParam String url) throws IOException {
Document doc = Jsoup.connect(url).get(); return doc.toString(); } }
|

在该示例中,使用了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 {
@GetMapping("/resttemplate/vul") public String RestTemplateDemo(@RequestParam String url){
RestTemplate restTemplate = new RestTemplate(); String result = restTemplate.getForObject(url ,String.class); return result; } }
|

在该示例中,使用了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 行,漏洞代码如下:

HttpGet
是 Apache HttpClient 库中的一个类,用于创建 HTTP GET 请求,最终使用 execute 方法执行 HTTP 请求
向上追踪发现 String url 中从 machine.getIp() 和 machine.getPort() 获取了 IP 地址和端口号
② machine 是从 for-each 循环中的 machines 获取值后将值赋予给 machine

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

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

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

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

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

将从请求中获取到的数据,分别设置成 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秒钟执行一次

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

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


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

三、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
| 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);
public static boolean isHttp(String url) { return url.startsWith("http://") || url.startsWith("https://"); }
public static String gethost(String url) { try { URI uri = new URI(url); return uri.getHost().toLowerCase(); } catch (URISyntaxException e) { return ""; } }
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);
if (!isHttp(url)) { return 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; } }
public static boolean checkSSRFByWhitehosts(String url) { return SSRFChecker.checkURLFckSSRF(url); }
@Deprecated public static boolean checkSSRF(String url) { int checkTimes = 10; return SSRFChecker.checkSSRF(url, checkTimes); }
public static boolean checkSSRFWithoutRedirect(String url) { if(url == null) { return false; } return !SSRFChecker.isInternalIpByUrl(url); }
public static void startSSRFHook() throws IOException { SocketHook.startHook(); }
public static void stopSSRFHook(){ SocketHook.stopHook(); }
public static String pathFilter(String filepath) { String temp = filepath;
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; }
public static String sqlFilter(String sql) { if (!FILTER_PATTERN.matcher(sql).matches()) { return null; } return sql; }
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); if (ch >= 48 && ch <= 57 ){ sb.append(ch); } 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) { }
}
|