Jenkins动态路由漏洞分析与利用链
在软件工程中,持续集成和持续交付(CI/CD)是开发者减少常规工作的最佳实践。在CI/CD工具中,最知名的就是Jenkins。由于其易用性、强大的Pipeline系统和容器集成,Jenkins也是全球使用最广泛的CI/CD应用。根据Snyk 2018年的JVM生态系统报告,Jenkins在CI/CD服务器调查中占据了约60%的市场份额。
对于红队而言,Jenkins也是每个黑客都想控制的战场。如果有人控制了Jenkins服务器,他就可以获取大量源代码和凭证,甚至控制Jenkins节点!在DEVCORE红队案例中,也有几个案例是从Jenkins服务器作为入口点入侵整个公司的!
本文主要关于去年对Jenkins的简要安全审查。在这次审查中,我们发现了7个漏洞,包括:
- CVE-2018-1999002 - 任意文件读取漏洞
- CVE-2018-1000600 - GitHub插件中的CSRF和权限检查缺失
- CVE-2018-1999046 - 未授权用户可访问代理日志
- CVE-2018-1000861 - 通过精心构造的URL执行代码
- CVE-2019-1003000 - 脚本安全和管道插件中的沙箱绕过
- CVE-2019-1003001 - 脚本安全和管道插件中的沙箱绕过
- CVE-2019-1003002 - 脚本安全和管道插件中的沙箱绕过
其中,讨论较多的是漏洞CVE-2018-1999002。这是一个通过不寻常攻击向量的任意文件读取漏洞!腾讯云鼎安全实验室已经写了详细的公告,并演示了如何利用此漏洞从任意文件读取到在从Shodan找到的真实Jenkins站点上实现RCE!
然而,我们不会在这篇博客文章中讨论这个问题。相反,这篇文章是关于在深入研究Stapler框架时发现的另一个漏洞,目的是找到绕过CVE-2018-1999002的最低权限要求ANONYMOUS_READ=True的方法!如果你只看公告描述,你可能会好奇——仅通过精心构造的URL就能获得代码执行是真的吗?
从我个人的角度来看,这个漏洞只是一个访问控制列表(ACL)绕过,但由于这是一个架构问题而不是单个程序的问题,有多种方法可以利用这个漏洞!为了偿还设计债务,Jenkins团队也付出了很多努力(在Jenkins端和Stapler端都打了补丁)来修复这个问题。补丁不仅引入了新的路由黑名单和白名单,还扩展了原有的服务提供者接口(SPI)来保护Jenkins的路由。现在让我们弄清楚为什么Jenkins需要进行如此巨大的代码修改!
审查范围
这不是一个完整的代码审查(全面的安全审查需要很多时间…),所以这次审查只针对高影响漏洞。审查范围包括:
- Jenkins核心
- Stapler Web框架
- 建议的插件
在安装过程中,Jenkins会询问是否要安装建议的插件,如Git、GitHub、SVN和Pipeline。基本上,大多数人选择是,否则他们会得到一个不方便且难以使用的Jenkins。
权限级别
由于漏洞是ACL绕过,我们首先需要介绍Jenkins中的权限级别!在Jenkins中,有不同种类的ACL角色,Jenkins甚至有一个专门的插件Matrix Authorization Strategy Plugin(也在建议插件列表中)来配置每个项目的详细权限。从攻击者的角度来看,我们大致将ACL分为3种类型:
-
完全访问 你可以完全控制Jenkins。一旦攻击者获得此权限,他可以通过脚本控制台执行任意Groovy代码!
1
print "uname -a".execute().text
这是最黑客友好的场景,但由于安全意识的提高和大量机器人扫描所有IPv4,现在很难公开看到这种配置。
-
只读模式 这可以从“配置全局安全”中启用,并选中单选按钮:
1
允许匿名读取访问
在这种模式下,所有内容都是可见和可读的。例如代理日志和作业/节点信息。对于攻击者来说,这种模式的最大好处是可访问大量私有源代码!然而,攻击者无法做任何进一步的事情或执行Groovy脚本!
虽然这不是默认设置,但对于DevOps来说,他们可能仍然为自动化打开此选项。根据对Shodan的小调查,大约有12%的服务器启用了此模式!我们将在以下部分中称此模式为ANONYMOUS_READ=True。
-
认证模式 这是默认模式。没有有效的凭证,你看不到任何信息!我们将在以下部分中使用ANONYMOUS_READ=False来称呼此模式。
漏洞分析
为了解释这个漏洞,我们将从Jenkins的动态路由开始。为了为开发者提供更多灵活性,Jenkins使用命名约定来解析URL并动态调用方法。
Jenkins首先将所有的URL按/
进行标记化,并从jenkins.model.Jenkins
作为入口点开始,逐个匹配标记。如果标记匹配(1)公共类成员或(2)符合以下命名约定的公共类方法,Jenkins会递归调用!
get<token>()
get<token>(String)
get<token>(Int)
get<token>(Long)
get<token>(StaplerRequest)
getDynamic(String, …)
doDynamic(…)
do<token>(…)
js<token>(…)
- 带有
@WebMethod
注解的类方法 - 带有
@JavaScriptMethod
注解的类方法
看起来Jenkins为开发者提供了很多灵活性。然而,太多的自由并不总是好事。基于这个命名约定有两个问题!
-
一切都是java.lang.Object的子类 在Java中,一切都是
java.lang.Object
的子类。因此,所有对象都必须存在方法getClass()
,而getClass()
的名称正好匹配命名约定规则#1!所以方法getClass()
也可以在Jenkins动态路由期间调用! -
白名单绕过 如前所述,ANONYMOUS_READ=True和ANONYMOUS_READ=False之间的最大区别是,如果标志设置为False,入口点将在
jenkins.model.Jenkins#getTarget()
中多做一次检查。检查是基于URL前缀的白名单检查,以下是列表:1 2 3 4 5 6 7 8 9 10 11 12 13
private static final ImmutableSet<String> ALWAYS_READABLE_PATHS = ImmutableSet.of( "/login", "/logout", "/accessDenied", "/adjuncts/", "/error", "/oops", "/signup", "/tcpSlaveAgentListener", "/federatedLoginService/", "/securityRealm", "/instance-identity" );
这意味着你被限制在这些入口,但如果你能从白名单入口找到交叉引用跳转到其他对象,你仍然可以绕过这个URL前缀检查!这似乎有点难以理解。让我们举一个简单的例子来演示动态路由:
1
http://jenkin.local/adjuncts/whatever/class/classLoader/resource/index.jsp/content
上面的URL将按顺序调用以下方法!
1 2 3 4 5
jenkins.model.Jenkins.getAdjuncts("whatever") .getClass() .getClassLoader() .getResource("index.jsp") .getContent()
这个执行链看起来很顺利,但遗憾的是,它无法检索结果。因此,这不是一个潜在的风险,但它仍然是理解机制的好案例!
一旦我们意识到原理,剩下的部分就像解决一个迷宫。jenkins.model.Jenkins
是入口点。这个对象中的每个成员都可以引用到一个新对象,所以我们的工作是一层层链接对象,直到出口门,即危险的方法调用!
顺便说一句,最令人伤心的是这个漏洞不能调用SETTER,否则这绝对是另一个有趣的classLoader操作漏洞,就像Struts2 RCE和Spring Framework RCE一样!!
如何利用?
如何利用?简而言之,这个漏洞可以实现的是使用交叉引用对象来绕过ACL策略。为了利用它,我们需要找到一个合适的gadget,以便在这个对象森林中更方便地调用我们喜欢的对象!这里我们选择gadget:
|
|
gadget将按顺序调用以下方法。
|
|
在Jenkins中,所有可配置的对象都将扩展类型hudson.model.Descriptor
。并且,任何扩展Descriptor类型的类都可以通过方法hudson.model.DescriptorByNameOwner#getDescriptorByName(String)
访问。总的来说,总共有大约500个类类型可以访问!但由于Jenkins的架构,大多数开发者会在危险操作前再次检查权限。所以即使我们能找到对脚本控制台的对象引用,没有权限Jenkins.RUN_SCRIPTS
,我们仍然无法做任何事情 :(
即便如此,这个漏洞仍然可以被视为绕过第一个ACL限制并链接其他漏洞的垫脚石。我们将展示3个漏洞链作为我们的案例研究!(虽然我们只展示了3个案例,但还有更多!如果你感兴趣,强烈建议你自己找到其他漏洞 :P)
1. 预认证用户信息泄露
在测试Jenkins时,一个常见的场景是你想执行暴力攻击,但不知道可以尝试哪个账户(有效的凭证至少可以读取源代码,所以值得首先尝试)。
在这种情况下,这个漏洞很有用! 由于搜索功能缺乏权限检查。通过将关键字从a修改到z,攻击者可以列出Jenkins上的所有用户!
PoC:
|
|
此外,这个漏洞还可以与Ananthapadmanabhan S R报告的SECURITY-514链接,以泄露用户的电子邮件地址!例如:
|
|
2. 与CVE-2018-1000600链接实现预认证完全响应SSRF
下一个漏洞是CVE-2018-1000600,这个漏洞是由Orange Tsai(是的,就是我 :P)报告的。关于这个漏洞,官方描述是:
|
|
它可以用已知的凭证ID提取Jenkins中存储的任何凭证。但如果没有用户提供的值,凭证ID是一个随机的UUID。所以似乎不可能利用?(或者如果有人知道如何获取凭证ID,请告诉我!)
虽然它无法在没有已知凭证ID的情况下提取任何凭证,但还有另一个攻击原语 - 一个完全响应的SSRF!我们都知道利用盲SSRF有多难,这就是为什么完全响应的SSRF如此有价值!
PoC:
|
|
3. 预认证远程代码执行
请不要胡说,RCE在哪里!!!
为了最大化影响,我还发现了一个有趣的远程代码执行,可以与此漏洞链接,实现当之无愧的预认证RCE!但它仍在负责任的披露过程中。请等待并查看第2部分!(将于2月中旬发布 :P)
待办事项
以下是我的待办事项列表,可以使这个漏洞更加完美。如果你找到任何,请告诉我,非常感谢 :P
- 在ANONYMOUS_READ=False下获取Plugin对象引用。如果这可以实现,它可以绕过CVE-2018-1999002和CVE-2018-6356的ACL限制,实现真正的预认证任意文件读取!
- 在ANONYMOUS_READ=False下找到另一个gadget来调用方法
getDescriptorByName(String)
。为了修复SECURITY-672,Jenkins在hudson.model.User
上应用了一个检查,以确保最低权限Jenkins.READ
。所以原始的gadget在Jenkins版本2.138之后会失败。
致谢
感谢Jenkins安全团队,特别是Daniel Beck的协调和漏洞修复!以下是简要的时间线:
- 2018年5月30日 - 向Jenkins报告漏洞
- 2018年6月15日 - Jenkins修补了漏洞并分配了CVE-2018-1000600
- 2018年7月18日 - Jenkins修补了漏洞并分配了CVE-2018-1999002
- 2018年8月15日 - Jenkins修补了漏洞并分配了CVE-2018-1999046
- 2018年12月5日 - Jenkins修补了漏洞并分配了CVE-2018-1000861
- 2018年12月20日 - 向Jenkins报告Groovy漏洞
- 2019年1月8日 - Jenkins修补了Groovy漏洞并分配了CVE-2019-1003000、CVE-2019-1003001和CVE-2019-1003002