alibaba / Sentinel

A powerful flow control component enabling reliability, resilience and monitoring for microservices. (面向云原生微服务的高可用流控防护组件)

Home Page:https://sentinelguard.io/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Report a Sentinel Security Vulnerability about SSRF

threedr3am opened this issue · comments

你好,我是SecCoder Security Lab的threedr3am,我发现了Alibaba开源限流熔断组件Sentinel中的管控平台sentinel-dashboard存在认证前SSRF漏洞,恶意用户无需认证即可通过该接口进行SSRF攻击。

Issue Description

Type: bug report

由于该开源项目的sentinel-dashboard module中存在着接口/registry/machine无需授权即可访问,并且客户端接入时提交的注册数据无任何权限校验就存储在内存中,恶意用户无需认证登陆,即可发送恶意的应用注册数据,让sentinel-dashboard定时任务对其数据中ip指定的主机发起GET请求,进行SSRF攻击。

漏洞点在com.alibaba.csp.sentinel.dashboard.metric.MetricFetcher#fetchOnce

通过查看代码可以发现,该方法中会遍历注册AppInfo中每台机器MachineInfo的注册信息,构造对应的URL进行采集客户端限流熔断等数据,但其ip字段无任何校验,通过井号'#'等字符就可以截断后续的URL内容(RFC),进而控制管控平台sentinel-dashboard发起任意GET请求。

/**
 * 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);
}

通过漏洞调用链,可以发现这是一个10秒钟执行一遍的定时任务

com.alibaba.csp.sentinel.dashboard.metric.MetricFetcher

private void start() {
    fetchScheduleService.scheduleAtFixedRate(() -> {
        try {
            fetchAllApp();
        } catch (Exception e) {
            logger.info("fetchAllApp error:", e);
        }
    }, 10, intervalSecond, TimeUnit.SECONDS);
}

Describe what happened (or what feature you want)

因为Sentinel的设计,该接口是用于客户端接入时进行注册用途,一般情况下,内网可信网络下无需认证,或k8s下使用类似istio等进行访问限制,所以,接口认证不存在问题。但对于拼接URL进行客户端限流熔断数据采样的行为,缺少了参数校验,导致可以任意控制URL发起HTTP GET请求的SSRF攻击。

因为port字段是Integer类型,使其具有了一定的限制,但ip字段没有任何校验,需要对其进行严格的校验,比如引入正则限制必须是ip,或者域名等等。

Describe what you expected to happen

SSRF

How to reproduce it (as minimally and precisely as possible)

  1. 到github拉取开源代码https://github.com/alibaba/Sentinel
  2. 运行Sentinel/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/DashboardApplication.java即可启动sentinel-dashboard后台
  3. 本地监听12345端口,nc -lvvp 12345
  4. 发起对本地localhost端口为12345的SSRF GET攻击,curl -XGET 'http://127.0.0.1:8080/registry/machine?app=SSRF-TEST&appType=0&version=0&hostname=TEST&ip=localhost:12345%23&port=0'

可以看到,nc监听到了GET请求

nc -lvvp 12345
Listening on any address 12345 (italk)
Connection from 127.0.0.1:61446
GET / HTTP/1.1
Connection: Close
Host: localhost:12345
User-Agent: Apache-HttpAsyncClient/4.1.3 (Java/1.8.0_241)

Tell us your environment

Anything else we need to know?

Thanks for reporting!