找回密码
 立即注册
首页 业界区 安全 Alibaba Sentinel SSRF漏洞分析(CVE-2021-44139) ...

Alibaba Sentinel SSRF漏洞分析(CVE-2021-44139)

祖柔惠 2025-6-1 20:49:44
Alibaba Sentinel SSRF漏洞分析(CVE-2021-44139)

一、Alibaba Sentienl 简介

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel  是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。
Alibaba Sentinel 官方文档:https://sentinelguard.io/zh-cn/docs/introduction.html
二、漏洞代码分析

2.1、环境部署

源码地址:https://github.com/alibaba/Sentinel/releases/tag/v1.8.0
解压之后使用IDEA打开,打开之后自动加载依赖项
1.png

自动加载完成依赖项后,访问sentinel-dashboard\src\main\java\com\alibaba\csp\sentinel\dashboard\DashboardApplication.java文件,点击左侧绿色按钮启动项目,如下图所示:
2.png

启动完成后,访问http://127.0.0.1:8080显示如下即为启动成功,如下图所示:
3.png

默认的登录账号密码为sentinel/sentinel
2.2、漏洞代码分析

这里可以先看一下threedr3am 师傅的提交的报告原文:https://github.com/alibaba/Sentinel/issues/2451
漏洞的触发点在 MetricFetcher 类中,位于sentinel-dashboard\src\main\java\com\alibaba\csp\sentinel\dashboard\metric\MetricFetcher.java。代码如下
  1. /*
  2. * Copyright 1999-2018 Alibaba Group Holding Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. *      http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package com.alibaba.csp.sentinel.dashboard.metric;
  17. import java.net.ConnectException;
  18. import java.net.SocketTimeoutException;
  19. import java.nio.charset.Charset;
  20. import java.util.Date;
  21. import java.util.HashSet;
  22. import java.util.List;
  23. import java.util.Map;
  24. import java.util.Set;
  25. import java.util.concurrent.ArrayBlockingQueue;
  26. import java.util.concurrent.ConcurrentHashMap;
  27. import java.util.concurrent.CountDownLatch;
  28. import java.util.concurrent.ExecutorService;
  29. import java.util.concurrent.Executors;
  30. import java.util.concurrent.RejectedExecutionHandler;
  31. import java.util.concurrent.ScheduledExecutorService;
  32. import java.util.concurrent.ThreadPoolExecutor;
  33. import java.util.concurrent.ThreadPoolExecutor.DiscardPolicy;
  34. import java.util.concurrent.TimeUnit;
  35. import java.util.concurrent.atomic.AtomicLong;
  36. import com.alibaba.csp.sentinel.Constants;
  37. import com.alibaba.csp.sentinel.concurrent.NamedThreadFactory;
  38. import com.alibaba.csp.sentinel.config.SentinelConfig;
  39. import com.alibaba.csp.sentinel.dashboard.datasource.entity.MetricEntity;
  40. import com.alibaba.csp.sentinel.dashboard.discovery.AppInfo;
  41. import com.alibaba.csp.sentinel.dashboard.discovery.AppManagement;
  42. import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo;
  43. import com.alibaba.csp.sentinel.node.metric.MetricNode;
  44. import com.alibaba.csp.sentinel.util.StringUtil;
  45. import com.alibaba.csp.sentinel.dashboard.repository.metric.MetricsRepository;
  46. import org.apache.http.HttpResponse;
  47. import org.apache.http.client.methods.HttpGet;
  48. import org.apache.http.concurrent.FutureCallback;
  49. import org.apache.http.entity.ContentType;
  50. import org.apache.http.impl.client.DefaultRedirectStrategy;
  51. import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
  52. import org.apache.http.impl.nio.client.HttpAsyncClients;
  53. import org.apache.http.impl.nio.reactor.IOReactorConfig;
  54. import org.apache.http.protocol.HTTP;
  55. import org.apache.http.util.EntityUtils;
  56. import org.slf4j.Logger;
  57. import org.slf4j.LoggerFactory;
  58. import org.springframework.beans.factory.annotation.Autowired;
  59. import org.springframework.stereotype.Component;
  60. /**
  61. * Fetch metric of machines.
  62. *
  63. * @author leyou
  64. */
  65. @Component
  66. public class MetricFetcher {
  67.     public static final String NO_METRICS = "No metrics";
  68.     private static final int HTTP_OK = 200;
  69.     private static final long MAX_LAST_FETCH_INTERVAL_MS = 1000 * 15;
  70.     private static final long FETCH_INTERVAL_SECOND = 6;
  71.     private static final Charset DEFAULT_CHARSET = Charset.forName(SentinelConfig.charset());
  72.     private final static String METRIC_URL_PATH = "metric";
  73.     private static Logger logger = LoggerFactory.getLogger(MetricFetcher.class);
  74.     private final long intervalSecond = 1;
  75.     private Map<String, AtomicLong> appLastFetchTime = new ConcurrentHashMap<>();
  76.     @Autowired
  77.     private MetricsRepository<MetricEntity> metricStore;
  78.     @Autowired
  79.     private AppManagement appManagement;
  80.     private CloseableHttpAsyncClient httpclient;
  81.     @SuppressWarnings("PMD.ThreadPoolCreationRule")
  82.     private ScheduledExecutorService fetchScheduleService = Executors.newScheduledThreadPool(1,
  83.         new NamedThreadFactory("sentinel-dashboard-metrics-fetch-task"));
  84.     private ExecutorService fetchService;
  85.     private ExecutorService fetchWorker;
  86.     public MetricFetcher() {
  87.         int cores = Runtime.getRuntime().availableProcessors() * 2;
  88.         long keepAliveTime = 0;
  89.         int queueSize = 2048;
  90.         RejectedExecutionHandler handler = new DiscardPolicy();
  91.         fetchService = new ThreadPoolExecutor(cores, cores,
  92.             keepAliveTime, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(queueSize),
  93.             new NamedThreadFactory("sentinel-dashboard-metrics-fetchService"), handler);
  94.         fetchWorker = new ThreadPoolExecutor(cores, cores,
  95.             keepAliveTime, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(queueSize),
  96.             new NamedThreadFactory("sentinel-dashboard-metrics-fetchWorker"), handler);
  97.         IOReactorConfig ioConfig = IOReactorConfig.custom()
  98.             .setConnectTimeout(3000)
  99.             .setSoTimeout(3000)
  100.             .setIoThreadCount(Runtime.getRuntime().availableProcessors() * 2)
  101.             .build();
  102.         httpclient = HttpAsyncClients.custom()
  103.             .setRedirectStrategy(new DefaultRedirectStrategy() {
  104.                 @Override
  105.                 protected boolean isRedirectable(final String method) {
  106.                     return false;
  107.                 }
  108.             }).setMaxConnTotal(4000)
  109.             .setMaxConnPerRoute(1000)
  110.             .setDefaultIOReactorConfig(ioConfig)
  111.             .build();
  112.         httpclient.start();
  113.         start();
  114.     }
  115.     private void start() {
  116.         fetchScheduleService.scheduleAtFixedRate(() -> {
  117.             try {
  118.                 fetchAllApp();
  119.             } catch (Exception e) {
  120.                 logger.info("fetchAllApp error:", e);
  121.             }
  122.         }, 10, intervalSecond, TimeUnit.SECONDS);
  123.     }
  124.     private void writeMetric(Map<String, MetricEntity> map) {
  125.         if (map.isEmpty()) {
  126.             return;
  127.         }
  128.         Date date = new Date();
  129.         for (MetricEntity entity : map.values()) {
  130.             entity.setGmtCreate(date);
  131.             entity.setGmtModified(date);
  132.         }
  133.         metricStore.saveAll(map.values());
  134.     }
  135.     /**
  136.      * Traverse each APP, and then pull the metric of all machines for that APP.
  137.      */
  138.     private void fetchAllApp() {
  139.         List<String> apps = appManagement.getAppNames();
  140.         if (apps == null) {
  141.             return;
  142.         }
  143.         for (final String app : apps) {
  144.             fetchService.submit(() -> {
  145.                 try {
  146.                     doFetchAppMetric(app);
  147.                 } catch (Exception e) {
  148.                     logger.error("fetchAppMetric error", e);
  149.                 }
  150.             });
  151.         }
  152.     }
  153.     /**
  154.      * fetch metric between [startTime, endTime], both side inclusive
  155.      */
  156.     private void fetchOnce(String app, long startTime, long endTime, int maxWaitSeconds) {
  157.         if (maxWaitSeconds <= 0) {
  158.             throw new IllegalArgumentException("maxWaitSeconds must > 0, but " + maxWaitSeconds);
  159.         }
  160.         AppInfo appInfo = appManagement.getDetailApp(app);
  161.         // auto remove for app
  162.         if (appInfo.isDead()) {
  163.             logger.info("Dead app removed: {}", app);
  164.             appManagement.removeApp(app);
  165.             return;
  166.         }
  167.         Set<MachineInfo> machines = appInfo.getMachines();
  168.         logger.debug("enter fetchOnce(" + app + "), machines.size()=" + machines.size()
  169.             + ", time intervalMs [" + startTime + ", " + endTime + "]");
  170.         if (machines.isEmpty()) {
  171.             return;
  172.         }
  173.         final String msg = "fetch";
  174.         AtomicLong unhealthy = new AtomicLong();
  175.         final AtomicLong success = new AtomicLong();
  176.         final AtomicLong fail = new AtomicLong();
  177.         long start = System.currentTimeMillis();
  178.         /** app_resource_timeSecond -> metric */
  179.         final Map<String, MetricEntity> metricMap = new ConcurrentHashMap<>(16);
  180.         final CountDownLatch latch = new CountDownLatch(machines.size());
  181.         for (final MachineInfo machine : machines) {
  182.             // auto remove
  183.             if (machine.isDead()) {
  184.                 latch.countDown();
  185.                 appManagement.getDetailApp(app).removeMachine(machine.getIp(), machine.getPort());
  186.                 logger.info("Dead machine removed: {}:{} of {}", machine.getIp(), machine.getPort(), app);
  187.                 continue;
  188.             }
  189.             if (!machine.isHealthy()) {
  190.                 latch.countDown();
  191.                 unhealthy.incrementAndGet();
  192.                 continue;
  193.             }
  194.             final String url = "http://" + machine.getIp() + ":" + machine.getPort() + "/" + METRIC_URL_PATH
  195.                 + "?startTime=" + startTime + "&endTime=" + endTime + "&refetch=" + false;
  196.             final HttpGet httpGet = new HttpGet(url);
  197.             httpGet.setHeader(HTTP.CONN_DIRECTIVE, HTTP.CONN_CLOSE);
  198.             httpclient.execute(httpGet, new FutureCallback<HttpResponse>() {
  199.                 @Override
  200.                 public void completed(final HttpResponse response) {
  201.                     try {
  202.                         handleResponse(response, machine, metricMap);
  203.                         success.incrementAndGet();
  204.                     } catch (Exception e) {
  205.                         logger.error(msg + " metric " + url + " error:", e);
  206.                     } finally {
  207.                         latch.countDown();
  208.                     }
  209.                 }
  210.                 @Override
  211.                 public void failed(final Exception ex) {
  212.                     latch.countDown();
  213.                     fail.incrementAndGet();
  214.                     httpGet.abort();
  215.                     if (ex instanceof SocketTimeoutException) {
  216.                         logger.error("Failed to fetch metric from <{}>: socket timeout", url);
  217.                     } else if (ex instanceof ConnectException) {
  218.                         logger.error("Failed to fetch metric from <{}> (ConnectionException: {})", url, ex.getMessage());
  219.                     } else {
  220.                         logger.error(msg + " metric " + url + " error", ex);
  221.                     }
  222.                 }
  223.                 @Override
  224.                 public void cancelled() {
  225.                     latch.countDown();
  226.                     fail.incrementAndGet();
  227.                     httpGet.abort();
  228.                 }
  229.             });
  230.         }
  231.         try {
  232.             latch.await(maxWaitSeconds, TimeUnit.SECONDS);
  233.         } catch (Exception e) {
  234.             logger.info(msg + " metric, wait http client error:", e);
  235.         }
  236.         long cost = System.currentTimeMillis() - start;
  237.         //logger.info("finished " + msg + " metric for " + app + ", time intervalMs [" + startTime + ", " + endTime
  238.         //    + "], total machines=" + machines.size() + ", dead=" + dead + ", fetch success="
  239.         //    + success + ", fetch fail=" + fail + ", time cost=" + cost + " ms");
  240.         writeMetric(metricMap);
  241.     }
  242.     private void doFetchAppMetric(final String app) {
  243.         long now = System.currentTimeMillis();
  244.         long lastFetchMs = now - MAX_LAST_FETCH_INTERVAL_MS;
  245.         if (appLastFetchTime.containsKey(app)) {
  246.             lastFetchMs = Math.max(lastFetchMs, appLastFetchTime.get(app).get() + 1000);
  247.         }
  248.         // trim milliseconds
  249.         lastFetchMs = lastFetchMs / 1000 * 1000;
  250.         long endTime = lastFetchMs + FETCH_INTERVAL_SECOND * 1000;
  251.         if (endTime > now - 1000 * 2) {
  252.             // to near
  253.             return;
  254.         }
  255.         // update last_fetch in advance.
  256.         appLastFetchTime.computeIfAbsent(app, a -> new AtomicLong()).set(endTime);
  257.         final long finalLastFetchMs = lastFetchMs;
  258.         final long finalEndTime = endTime;
  259.         try {
  260.             // do real fetch async
  261.             fetchWorker.submit(() -> {
  262.                 try {
  263.                     fetchOnce(app, finalLastFetchMs, finalEndTime, 5);
  264.                 } catch (Exception e) {
  265.                     logger.info("fetchOnce(" + app + ") error", e);
  266.                 }
  267.             });
  268.         } catch (Exception e) {
  269.             logger.info("submit fetchOnce(" + app + ") fail, intervalMs [" + lastFetchMs + ", " + endTime + "]", e);
  270.         }
  271.     }
  272.     private void handleResponse(final HttpResponse response, MachineInfo machine,
  273.                                 Map<String, MetricEntity> metricMap) throws Exception {
  274.         int code = response.getStatusLine().getStatusCode();
  275.         if (code != HTTP_OK) {
  276.             return;
  277.         }
  278.         Charset charset = null;
  279.         try {
  280.             String contentTypeStr = response.getFirstHeader("Content-type").getValue();
  281.             if (StringUtil.isNotEmpty(contentTypeStr)) {
  282.                 ContentType contentType = ContentType.parse(contentTypeStr);
  283.                 charset = contentType.getCharset();
  284.             }
  285.         } catch (Exception ignore) {
  286.         }
  287.         String body = EntityUtils.toString(response.getEntity(), charset != null ? charset : DEFAULT_CHARSET);
  288.         if (StringUtil.isEmpty(body) || body.startsWith(NO_METRICS)) {
  289.             //logger.info(machine.getApp() + ":" + machine.getIp() + ":" + machine.getPort() + ", bodyStr is empty");
  290.             return;
  291.         }
  292.         String[] lines = body.split("\n");
  293.         //logger.info(machine.getApp() + ":" + machine.getIp() + ":" + machine.getPort() +
  294.         //    ", bodyStr.length()=" + body.length() + ", lines=" + lines.length);
  295.         handleBody(lines, machine, metricMap);
  296.     }
  297.     private void handleBody(String[] lines, MachineInfo machine, Map<String, MetricEntity> map) {
  298.         //logger.info("handleBody() lines=" + lines.length + ", machine=" + machine);
  299.         if (lines.length < 1) {
  300.             return;
  301.         }
  302.         for (String line : lines) {
  303.             try {
  304.                 MetricNode node = MetricNode.fromThinString(line);
  305.                 if (shouldFilterOut(node.getResource())) {
  306.                     continue;
  307.                 }
  308.                 /*
  309.                  * aggregation metrics by app_resource_timeSecond, ignore ip and port.
  310.                  */
  311.                 String key = buildMetricKey(machine.getApp(), node.getResource(), node.getTimestamp());
  312.                 MetricEntity entity = map.get(key);
  313.                 if (entity != null) {
  314.                     entity.addPassQps(node.getPassQps());
  315.                     entity.addBlockQps(node.getBlockQps());
  316.                     entity.addRtAndSuccessQps(node.getRt(), node.getSuccessQps());
  317.                     entity.addExceptionQps(node.getExceptionQps());
  318.                     entity.addCount(1);
  319.                 } else {
  320.                     entity = new MetricEntity();
  321.                     entity.setApp(machine.getApp());
  322.                     entity.setTimestamp(new Date(node.getTimestamp()));
  323.                     entity.setPassQps(node.getPassQps());
  324.                     entity.setBlockQps(node.getBlockQps());
  325.                     entity.setRtAndSuccessQps(node.getRt(), node.getSuccessQps());
  326.                     entity.setExceptionQps(node.getExceptionQps());
  327.                     entity.setCount(1);
  328.                     entity.setResource(node.getResource());
  329.                     map.put(key, entity);
  330.                 }
  331.             } catch (Exception e) {
  332.                 logger.warn("handleBody line exception, machine: {}, line: {}", machine.toLogString(), line);
  333.             }
  334.         }
  335.     }
  336.     private String buildMetricKey(String app, String resource, long timestamp) {
  337.         return app + "__" + resource + "__" + (timestamp / 1000);
  338.     }
  339.     private boolean shouldFilterOut(String resource) {
  340.         return RES_EXCLUSION_SET.contains(resource);
  341.     }
  342.     private static final Set<String> RES_EXCLUSION_SET = new HashSet<String>() {{
  343.        add(Constants.TOTAL_IN_RESOURCE_NAME);
  344.        add(Constants.SYSTEM_LOAD_RESOURCE_NAME);
  345.        add(Constants.CPU_USAGE_RESOURCE_NAME);
  346.     }};
  347. }
复制代码
漏洞触发点位于第 212 和 214 行,是 fetchOnce() 方法
fetchOnce():该方法会向给定的地址发送 HTTP GET 请求,该地址由应用程序的管理类 AppManagement 提供。然后使用回调函数来处理异步的 HTTP 响应,该响应包含了度量数据。在获取到响应后,该方法会解析响应的内容,将其中的度量数据保存在内存仓库中,以便后续使用。
漏洞代码
  1. final HttpGet httpGet = new HttpGet(url);
  2. httpGet.setHeader(HTTP.CONN_DIRECTIVE, HTTP.CONN_CLOSE);
  3. httpclient.execute(httpGet, new FutureCallback<HttpResponse>()
复制代码
4.png

这里可以看到使用了httpGet,他是Apache HttpClient 库中的一个类,用于创建 HTTP GET 请求,最终使用 execute 方法执行 HTTP 请求。
接下来追踪一下参数的传递过程
向上追踪发现 参数url是String url 中从 machine.getIp() 和 machine.getPort() 获取了 IP 地址和端口号,以及拼接了一些时间等,而 machine 是从 for-each 循环中的 machines 获取值后将值赋予给 machine
5.png

继续追踪 machines,发现在第 182 行处从 appInfo.getMachines(); 处获取的值,期间只做了一个判空处理,如下图所示:
6.png

接着进入 appInfo.getMachines(); 方法,在这段代码中 getMachines 中 return 了 machines。而变量 machines 使用了 ConcurrentHashMap.newKeySet() 方法创建了一个线程安全的 Set(集合),其中 machines 值是由 addMachine 方法添加进去的。
7.png

下面就是追踪 addMachine 方法了,可以看到调用关系,SimpleMachineDiscovery 类重写了 addMachine 方法,如下图所示:
8.png

9.png

继续追踪 addMachine 方法,查看调用关系,可以看到 MachineRegistryController 和 AppManagement 都有所调用,但仔细看会发现 MachineRegistryController 处的调用即是 appManagement.addMachine,所以进入那个最终都是可以到 MachineRegistryController 层的
10.png

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

继续看下半部分代码,就是将从请求中获取到的数据,分别设置成 machineInfo 的属性值,最后调用appManagement.addMachine(machineInfo);方法添加注册机器信息
12.png

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

13.png

在师傅的报告中我们看到该接口存在未授权,这个未授权的原因是为什么呢
在resources-application.properties文件下,我们可以看到这边设置了一个auth.filter.exclude-urls
14.png

全局搜索一下auth.filter.exclude-urls,找到另一处auth.filter.exclude-urls
15.png

这边可以看到设置了有些url不需要auth也可以访问,所以存在未授权
接下来验证一下漏洞
在本地通过开启一个服务,构造漏洞接口http://127.0.0.1:8080/registry/machine?app=SSRF-TEST&appType=0&version=0&hostname=TEST&ip=xxx.xxx.xxx.xxxx&port=8000,接收到请求信息
16.png


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册