Jenkins动态路由漏洞分析与利用链

本文深入分析了Jenkins动态路由机制中的安全漏洞,包括ACL绕过原理、多个CVE漏洞的利用链,以及如何通过精心构造的URL实现未授权信息泄露、SSRF甚至远程代码执行。

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种类型:

  1. 完全访问 你可以完全控制Jenkins。一旦攻击者获得此权限,他可以通过脚本控制台执行任意Groovy代码!

    1
    
    print "uname -a".execute().text
    

    这是最黑客友好的场景,但由于安全意识的提高和大量机器人扫描所有IPv4,现在很难公开看到这种配置。

  2. 只读模式 这可以从“配置全局安全”中启用,并选中单选按钮:

    1
    
    允许匿名读取访问
    

    在这种模式下,所有内容都是可见和可读的。例如代理日志和作业/节点信息。对于攻击者来说,这种模式的最大好处是可访问大量私有源代码!然而,攻击者无法做任何进一步的事情或执行Groovy脚本!

    虽然这不是默认设置,但对于DevOps来说,他们可能仍然为自动化打开此选项。根据对Shodan的小调查,大约有12%的服务器启用了此模式!我们将在以下部分中称此模式为ANONYMOUS_READ=True。

  3. 认证模式 这是默认模式。没有有效的凭证,你看不到任何信息!我们将在以下部分中使用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为开发者提供了很多灵活性。然而,太多的自由并不总是好事。基于这个命名约定有两个问题!

  1. 一切都是java.lang.Object的子类 在Java中,一切都是java.lang.Object的子类。因此,所有对象都必须存在方法getClass(),而getClass()的名称正好匹配命名约定规则#1!所以方法getClass()也可以在Jenkins动态路由期间调用!

  2. 白名单绕过 如前所述,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:

1
/securityRealm/user/[username]/descriptorByName/[descriptor_name]/

gadget将按顺序调用以下方法。

1
2
3
jenkins.model.Jenkins.getSecurityRealm()
.getUser([username])
.getDescriptorByName([descriptor_name])

在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:

1
http://jenkins.local/securityRealm/user/admin/search/index?q=[keyword]

此外,这个漏洞还可以与Ananthapadmanabhan S R报告的SECURITY-514链接,以泄露用户的电子邮件地址!例如:

1
http://jenkins.local/securityRealm/user/admin/api/xml

2. 与CVE-2018-1000600链接实现预认证完全响应SSRF

下一个漏洞是CVE-2018-1000600,这个漏洞是由Orange Tsai(是的,就是我 :P)报告的。关于这个漏洞,官方描述是:

1
GitHub插件中的CSRF漏洞和缺失的权限检查允许捕获凭证

它可以用已知的凭证ID提取Jenkins中存储的任何凭证。但如果没有用户提供的值,凭证ID是一个随机的UUID。所以似乎不可能利用?(或者如果有人知道如何获取凭证ID,请告诉我!)

虽然它无法在没有已知凭证ID的情况下提取任何凭证,但还有另一个攻击原语 - 一个完全响应的SSRF!我们都知道利用盲SSRF有多难,这就是为什么完全响应的SSRF如此有价值!

PoC:

1
2
3
4
http://jenkins.local/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.github.config.GitHubTokenCredentialsCreator/createTokenByPassword
?apiUrl=http://169.254.169.254/%23
&login=orange
&password=tsai

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
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计