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打开,打开之后自动加载依赖项
自动加载完成依赖项后,访问sentinel-dashboard\src\main\java\com\alibaba\csp\sentinel\dashboard\DashboardApplication.java文件,点击左侧绿色按钮启动项目,如下图所示:
启动完成后,访问http://127.0.0.1:8080显示如下即为启动成功,如下图所示:
默认的登录账号密码为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。代码如下- /*
- * Copyright 1999-2018 Alibaba Group Holding Ltd.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package com.alibaba.csp.sentinel.dashboard.metric;
- import java.net.ConnectException;
- import java.net.SocketTimeoutException;
- import java.nio.charset.Charset;
- import java.util.Date;
- import java.util.HashSet;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
- import java.util.concurrent.ArrayBlockingQueue;
- import java.util.concurrent.ConcurrentHashMap;
- import java.util.concurrent.CountDownLatch;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.RejectedExecutionHandler;
- import java.util.concurrent.ScheduledExecutorService;
- import java.util.concurrent.ThreadPoolExecutor;
- import java.util.concurrent.ThreadPoolExecutor.DiscardPolicy;
- import java.util.concurrent.TimeUnit;
- import java.util.concurrent.atomic.AtomicLong;
- import com.alibaba.csp.sentinel.Constants;
- import com.alibaba.csp.sentinel.concurrent.NamedThreadFactory;
- import com.alibaba.csp.sentinel.config.SentinelConfig;
- import com.alibaba.csp.sentinel.dashboard.datasource.entity.MetricEntity;
- import com.alibaba.csp.sentinel.dashboard.discovery.AppInfo;
- import com.alibaba.csp.sentinel.dashboard.discovery.AppManagement;
- import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo;
- import com.alibaba.csp.sentinel.node.metric.MetricNode;
- import com.alibaba.csp.sentinel.util.StringUtil;
- import com.alibaba.csp.sentinel.dashboard.repository.metric.MetricsRepository;
- import org.apache.http.HttpResponse;
- import org.apache.http.client.methods.HttpGet;
- import org.apache.http.concurrent.FutureCallback;
- import org.apache.http.entity.ContentType;
- import org.apache.http.impl.client.DefaultRedirectStrategy;
- import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
- import org.apache.http.impl.nio.client.HttpAsyncClients;
- import org.apache.http.impl.nio.reactor.IOReactorConfig;
- import org.apache.http.protocol.HTTP;
- import org.apache.http.util.EntityUtils;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Component;
- /**
- * Fetch metric of machines.
- *
- * @author leyou
- */
- @Component
- public class MetricFetcher {
- public static final String NO_METRICS = "No metrics";
- private static final int HTTP_OK = 200;
- private static final long MAX_LAST_FETCH_INTERVAL_MS = 1000 * 15;
- private static final long FETCH_INTERVAL_SECOND = 6;
- private static final Charset DEFAULT_CHARSET = Charset.forName(SentinelConfig.charset());
- private final static String METRIC_URL_PATH = "metric";
- private static Logger logger = LoggerFactory.getLogger(MetricFetcher.class);
- private final long intervalSecond = 1;
- private Map<String, AtomicLong> appLastFetchTime = new ConcurrentHashMap<>();
- @Autowired
- private MetricsRepository<MetricEntity> metricStore;
- @Autowired
- private AppManagement appManagement;
- private CloseableHttpAsyncClient httpclient;
- @SuppressWarnings("PMD.ThreadPoolCreationRule")
- private ScheduledExecutorService fetchScheduleService = Executors.newScheduledThreadPool(1,
- new NamedThreadFactory("sentinel-dashboard-metrics-fetch-task"));
- private ExecutorService fetchService;
- private ExecutorService fetchWorker;
- public MetricFetcher() {
- int cores = Runtime.getRuntime().availableProcessors() * 2;
- long keepAliveTime = 0;
- int queueSize = 2048;
- RejectedExecutionHandler handler = new DiscardPolicy();
- fetchService = new ThreadPoolExecutor(cores, cores,
- keepAliveTime, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(queueSize),
- new NamedThreadFactory("sentinel-dashboard-metrics-fetchService"), handler);
- fetchWorker = new ThreadPoolExecutor(cores, cores,
- keepAliveTime, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(queueSize),
- new NamedThreadFactory("sentinel-dashboard-metrics-fetchWorker"), handler);
- IOReactorConfig ioConfig = IOReactorConfig.custom()
- .setConnectTimeout(3000)
- .setSoTimeout(3000)
- .setIoThreadCount(Runtime.getRuntime().availableProcessors() * 2)
- .build();
- httpclient = HttpAsyncClients.custom()
- .setRedirectStrategy(new DefaultRedirectStrategy() {
- @Override
- protected boolean isRedirectable(final String method) {
- return false;
- }
- }).setMaxConnTotal(4000)
- .setMaxConnPerRoute(1000)
- .setDefaultIOReactorConfig(ioConfig)
- .build();
- httpclient.start();
- start();
- }
- private void start() {
- fetchScheduleService.scheduleAtFixedRate(() -> {
- try {
- fetchAllApp();
- } catch (Exception e) {
- logger.info("fetchAllApp error:", e);
- }
- }, 10, intervalSecond, TimeUnit.SECONDS);
- }
- private void writeMetric(Map<String, MetricEntity> map) {
- if (map.isEmpty()) {
- return;
- }
- Date date = new Date();
- for (MetricEntity entity : map.values()) {
- entity.setGmtCreate(date);
- entity.setGmtModified(date);
- }
- metricStore.saveAll(map.values());
- }
- /**
- * Traverse each APP, and then pull the metric of all machines for that APP.
- */
- private void fetchAllApp() {
- List<String> apps = appManagement.getAppNames();
- if (apps == null) {
- return;
- }
- for (final String app : apps) {
- fetchService.submit(() -> {
- try {
- doFetchAppMetric(app);
- } catch (Exception e) {
- logger.error("fetchAppMetric error", e);
- }
- });
- }
- }
- /**
- * fetch metric between [startTime, endTime], both side inclusive
- */
- private void fetchOnce(String app, long startTime, long endTime, int maxWaitSeconds) {
- if (maxWaitSeconds <= 0) {
- throw new IllegalArgumentException("maxWaitSeconds must > 0, but " + maxWaitSeconds);
- }
- AppInfo appInfo = appManagement.getDetailApp(app);
- // auto remove for app
- if (appInfo.isDead()) {
- logger.info("Dead app removed: {}", app);
- appManagement.removeApp(app);
- return;
- }
- Set<MachineInfo> machines = appInfo.getMachines();
- logger.debug("enter fetchOnce(" + app + "), machines.size()=" + machines.size()
- + ", time intervalMs [" + startTime + ", " + endTime + "]");
- if (machines.isEmpty()) {
- return;
- }
- final String msg = "fetch";
- AtomicLong unhealthy = new AtomicLong();
- final AtomicLong success = new AtomicLong();
- final AtomicLong fail = new AtomicLong();
- long start = System.currentTimeMillis();
- /** app_resource_timeSecond -> metric */
- final Map<String, MetricEntity> metricMap = new ConcurrentHashMap<>(16);
- final CountDownLatch latch = new CountDownLatch(machines.size());
- for (final MachineInfo machine : machines) {
- // auto remove
- if (machine.isDead()) {
- latch.countDown();
- appManagement.getDetailApp(app).removeMachine(machine.getIp(), machine.getPort());
- logger.info("Dead machine removed: {}:{} of {}", machine.getIp(), machine.getPort(), app);
- continue;
- }
- if (!machine.isHealthy()) {
- latch.countDown();
- unhealthy.incrementAndGet();
- continue;
- }
- final String url = "http://" + machine.getIp() + ":" + machine.getPort() + "/" + METRIC_URL_PATH
- + "?startTime=" + startTime + "&endTime=" + endTime + "&refetch=" + false;
- final HttpGet httpGet = new HttpGet(url);
- httpGet.setHeader(HTTP.CONN_DIRECTIVE, HTTP.CONN_CLOSE);
- httpclient.execute(httpGet, new FutureCallback<HttpResponse>() {
- @Override
- public void completed(final HttpResponse response) {
- try {
- handleResponse(response, machine, metricMap);
- success.incrementAndGet();
- } catch (Exception e) {
- logger.error(msg + " metric " + url + " error:", e);
- } finally {
- latch.countDown();
- }
- }
- @Override
- public void failed(final Exception ex) {
- latch.countDown();
- fail.incrementAndGet();
- httpGet.abort();
- if (ex instanceof SocketTimeoutException) {
- logger.error("Failed to fetch metric from <{}>: socket timeout", url);
- } else if (ex instanceof ConnectException) {
- logger.error("Failed to fetch metric from <{}> (ConnectionException: {})", url, ex.getMessage());
- } else {
- logger.error(msg + " metric " + url + " error", ex);
- }
- }
- @Override
- public void cancelled() {
- latch.countDown();
- fail.incrementAndGet();
- httpGet.abort();
- }
- });
- }
- try {
- latch.await(maxWaitSeconds, TimeUnit.SECONDS);
- } catch (Exception e) {
- logger.info(msg + " metric, wait http client error:", e);
- }
- long cost = System.currentTimeMillis() - start;
- //logger.info("finished " + msg + " metric for " + app + ", time intervalMs [" + startTime + ", " + endTime
- // + "], total machines=" + machines.size() + ", dead=" + dead + ", fetch success="
- // + success + ", fetch fail=" + fail + ", time cost=" + cost + " ms");
- writeMetric(metricMap);
- }
- private void doFetchAppMetric(final String app) {
- long now = System.currentTimeMillis();
- long lastFetchMs = now - MAX_LAST_FETCH_INTERVAL_MS;
- if (appLastFetchTime.containsKey(app)) {
- lastFetchMs = Math.max(lastFetchMs, appLastFetchTime.get(app).get() + 1000);
- }
- // trim milliseconds
- lastFetchMs = lastFetchMs / 1000 * 1000;
- long endTime = lastFetchMs + FETCH_INTERVAL_SECOND * 1000;
- if (endTime > now - 1000 * 2) {
- // to near
- return;
- }
- // update last_fetch in advance.
- appLastFetchTime.computeIfAbsent(app, a -> new AtomicLong()).set(endTime);
- final long finalLastFetchMs = lastFetchMs;
- final long finalEndTime = endTime;
- try {
- // do real fetch async
- fetchWorker.submit(() -> {
- try {
- fetchOnce(app, finalLastFetchMs, finalEndTime, 5);
- } catch (Exception e) {
- logger.info("fetchOnce(" + app + ") error", e);
- }
- });
- } catch (Exception e) {
- logger.info("submit fetchOnce(" + app + ") fail, intervalMs [" + lastFetchMs + ", " + endTime + "]", e);
- }
- }
- private void handleResponse(final HttpResponse response, MachineInfo machine,
- Map<String, MetricEntity> metricMap) throws Exception {
- int code = response.getStatusLine().getStatusCode();
- if (code != HTTP_OK) {
- return;
- }
- Charset charset = null;
- try {
- String contentTypeStr = response.getFirstHeader("Content-type").getValue();
- if (StringUtil.isNotEmpty(contentTypeStr)) {
- ContentType contentType = ContentType.parse(contentTypeStr);
- charset = contentType.getCharset();
- }
- } catch (Exception ignore) {
- }
- String body = EntityUtils.toString(response.getEntity(), charset != null ? charset : DEFAULT_CHARSET);
- if (StringUtil.isEmpty(body) || body.startsWith(NO_METRICS)) {
- //logger.info(machine.getApp() + ":" + machine.getIp() + ":" + machine.getPort() + ", bodyStr is empty");
- return;
- }
- String[] lines = body.split("\n");
- //logger.info(machine.getApp() + ":" + machine.getIp() + ":" + machine.getPort() +
- // ", bodyStr.length()=" + body.length() + ", lines=" + lines.length);
- handleBody(lines, machine, metricMap);
- }
- private void handleBody(String[] lines, MachineInfo machine, Map<String, MetricEntity> map) {
- //logger.info("handleBody() lines=" + lines.length + ", machine=" + machine);
- if (lines.length < 1) {
- return;
- }
- for (String line : lines) {
- try {
- MetricNode node = MetricNode.fromThinString(line);
- if (shouldFilterOut(node.getResource())) {
- continue;
- }
- /*
- * aggregation metrics by app_resource_timeSecond, ignore ip and port.
- */
- String key = buildMetricKey(machine.getApp(), node.getResource(), node.getTimestamp());
- MetricEntity entity = map.get(key);
- if (entity != null) {
- entity.addPassQps(node.getPassQps());
- entity.addBlockQps(node.getBlockQps());
- entity.addRtAndSuccessQps(node.getRt(), node.getSuccessQps());
- entity.addExceptionQps(node.getExceptionQps());
- entity.addCount(1);
- } else {
- entity = new MetricEntity();
- entity.setApp(machine.getApp());
- entity.setTimestamp(new Date(node.getTimestamp()));
- entity.setPassQps(node.getPassQps());
- entity.setBlockQps(node.getBlockQps());
- entity.setRtAndSuccessQps(node.getRt(), node.getSuccessQps());
- entity.setExceptionQps(node.getExceptionQps());
- entity.setCount(1);
- entity.setResource(node.getResource());
- map.put(key, entity);
- }
- } catch (Exception e) {
- logger.warn("handleBody line exception, machine: {}, line: {}", machine.toLogString(), line);
- }
- }
- }
- private String buildMetricKey(String app, String resource, long timestamp) {
- return app + "__" + resource + "__" + (timestamp / 1000);
- }
- private boolean shouldFilterOut(String resource) {
- return RES_EXCLUSION_SET.contains(resource);
- }
- private static final Set<String> RES_EXCLUSION_SET = new HashSet<String>() {{
- add(Constants.TOTAL_IN_RESOURCE_NAME);
- add(Constants.SYSTEM_LOAD_RESOURCE_NAME);
- add(Constants.CPU_USAGE_RESOURCE_NAME);
- }};
- }
复制代码 漏洞触发点位于第 212 和 214 行,是 fetchOnce() 方法
fetchOnce():该方法会向给定的地址发送 HTTP GET 请求,该地址由应用程序的管理类 AppManagement 提供。然后使用回调函数来处理异步的 HTTP 响应,该响应包含了度量数据。在获取到响应后,该方法会解析响应的内容,将其中的度量数据保存在内存仓库中,以便后续使用。
漏洞代码- final HttpGet httpGet = new HttpGet(url);
- httpGet.setHeader(HTTP.CONN_DIRECTIVE, HTTP.CONN_CLOSE);
- httpclient.execute(httpGet, new FutureCallback<HttpResponse>()
复制代码
这里可以看到使用了httpGet,他是Apache HttpClient 库中的一个类,用于创建 HTTP GET 请求,最终使用 execute 方法执行 HTTP 请求。
接下来追踪一下参数的传递过程
向上追踪发现 参数url是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 方法添加进去的。
下面就是追踪 addMachine 方法了,可以看到调用关系,SimpleMachineDiscovery 类重写了 addMachine 方法,如下图所示:
继续追踪 addMachine 方法,查看调用关系,可以看到 MachineRegistryController 和 AppManagement 都有所调用,但仔细看会发现 MachineRegistryController 处的调用即是 appManagement.addMachine,所以进入那个最终都是可以到 MachineRegistryController 层的
直接进入 MachineRegistryController分析,其主要作用是,获取请求中的参数,并进行相应的处理和判断,最终将信息添加到应用管理中,并返回注册结果。通过代码可以得出接口地址为 /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.3、漏洞验证
在师傅的报告中我们看到该接口存在未授权,这个未授权的原因是为什么呢
在resources-application.properties文件下,我们可以看到这边设置了一个auth.filter.exclude-urls
全局搜索一下auth.filter.exclude-urls,找到另一处auth.filter.exclude-urls
这边可以看到设置了有些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,接收到请求信息
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |