Jenkins漏洞利用:滥用元编程实现未授权远程代码执行
漏洞分析
首先从Jenkins Pipeline开始解释CVE-2019-1003000。人们选择Jenkins的主要原因之一是它提供了强大的Pipeline功能,这使得编写软件构建、测试和交付脚本更加容易!你可以将Pipeline想象成一种操作Jenkins的强大语言(实际上,Pipeline是用Groovy构建的DSL)。
为了检查用户提供脚本的语法是否正确,Jenkins为开发者提供了一个接口。如果你是这个功能的开发者,你会如何实现这个语法错误检查功能?你可以自己编写一个AST(抽象语法树)解析器,但这太困难了。最简单的方法是重用现有函数和库!
如前所述,Pipeline只是用Groovy构建的DSL,因此Pipeline必须遵循Groovy语法!如果Groovy解析器能够处理Pipeline脚本而没有错误,那么语法一定是正确的!以下代码片段显示了Jenkins如何验证Pipeline:
|
|
这里Jenkins使用GroovyClassLoader.parseClass(…)方法验证Pipeline!需要注意的是,这只是一个AST解析。没有运行execute()方法,任何危险的调用都不会被执行!如果你尝试解析以下Groovy脚本,你什么也得不到:
|
|
从开发者的角度来看,Pipeline可以控制Jenkins,因此它必须是危险的,并且需要在每次Pipeline调用之前进行严格的权限检查!然而,这只是一个简单的语法验证,因此这里的权限检查比平时要少。没有任何execute()方法,它只是一个AST解析器,必须是安全的!这是我第一次看到这个验证时的想法。然而,在写技术博客时,元编程(Meta-Programming)闪现在我的脑海中!
什么是元编程
元编程是一种编程概念!元编程的思想是为程序员提供一个抽象层,以不同的方式考虑程序,使程序更灵活和高效!元编程没有明确的定义。一般来说,程序自身处理程序以及编写操作其他程序(编译器、解释器或预处理器等)的程序都是元编程!这里的哲学非常深刻,甚至可以成为编程语言的一个大主题!
如果仍然难以理解,你可以将eval(…)视为另一种元编程,它让你动态操作程序。虽然这有点不准确,但对于理解来说仍然是一个很好的比喻!在软件工程中,也有很多与元编程相关的技术。例如:
- C宏
- C++模板
- Java注解
- Ruby(Ruby是一种对元编程友好的语言,甚至有相关的书籍)
- DSL(领域特定语言,如Sinatra和Gradle)
当我们谈论元编程时,我们根据范围将其分为(1)编译时和(2)运行时元编程。今天,我们关注编译时元编程!
注意:用非母语解释元编程很困难。如果你感兴趣,这里有一些材料!Wiki, Ref1, Ref2
注意:我不是编程语言大师,如果有任何不正确或不准确的地方,请原谅我<(_ _)>
如何利用?
从前一节我们知道Jenkins通过parseClass(…)验证Pipeline,并了解到元编程可以在编译时干扰解析器!编译(或解析)是一项艰巨的工作,有很多复杂的事情和隐藏的功能。所以,问题是,有没有我们可以利用的副作用?
有许多简单的案例证明元编程可以使程序易受攻击,例如C语言中的宏扩展:
|
|
或者编译器资源炸弹(仅用18字节制作16GB ELF):
|
|
或者通过编译器计算斐波那契数:
|
|
从编译后的二进制文件的汇编语言中,我们可以确定结果是在编译时计算的,而不是运行时!
|
|
更多例子,你可以参考StackOverflow上的文章《构建编译器炸弹》!
第一次尝试
回到我们的利用,Pipeline只是用Groovy构建的DSL,而Groovy也是一种对元编程友好的语言。我们开始阅读Groovy官方元编程手册,寻找一些利用方法。在2.1.9节中,我们找到了@groovy.transform.ASTTest注解。以下是它的描述:
@ASTTest是一种特殊的AST转换,旨在帮助调试其他AST转换或Groovy编译器本身。它将让开发者在编译期间“探索”AST,并对AST执行断言,而不是对编译结果执行断言。这意味着这个AST转换在字节码产生之前提供对AST的访问。@ASTTest可以放在任何可注解的节点上,并需要两个参数:
什么!对AST执行断言?这不正是我们想要的吗?让我们先在本地环境中编写一个简单的概念验证:
|
|
酷,它工作了!然而,在远程Jenkins上复现时,它显示:
|
|
什么鬼!!!出了什么问题?
经过一点挖掘,我们找到了根本原因。这是由Pipeline Shared Groovy Libraries Plugin引起的!为了在Pipeline中重用函数,Jenkins提供了可以将自定义库导入Pipeline的功能!Jenkins会在每个执行的Pipeline之前加载这个库。因此,问题变成了在编译时缺少classPath中相应的库。这就是为什么错误unable to resolve class会出现!
如何解决这个问题?很简单!只需转到Jenkins插件管理器并删除Pipeline Shared Groovy Libraries Plugin!它可以解决问题,然后我们可以无任何错误地执行任意代码!但是,这不是一个好的解决方案,因为这个插件是随Pipeline一起安装的。要求管理员删除插件以执行代码是很蹩脚的!我们停止挖掘这个,尝试寻找另一种方法!
第二次尝试
我们继续阅读Groovy元编程手册,发现了另一个有趣的注解 - @Grab。手册上没有关于@Grab的详细信息。然而,我们在搜索引擎上找到了另一篇文章 - 《用Grape进行依赖管理》!
哦,从文章中我们了解到Grape是Groovy中内置的JAR依赖管理!它可以帮助程序员导入不在classPath中的库。用法如下:
|
|
通过使用@Grab注解,它可以在编译时自动导入不在classPath中的JAR文件!如果你只是想通过有效凭据和Pipeline执行权限绕过Pipeline沙箱,那就足够了。你可以按照@adamyordan提供的PoC执行任意命令!
然而,没有有效凭据和execute()方法,这只是一个AST解析器,你甚至无法控制远程服务器上的文件。那么,我们能做什么?通过更深入地研究@Grab,我们发现了另一个有趣的注解 - @GrabResolver:
|
|
如果你足够聪明,你会想将root参数改为恶意网站!让我们在本地环境中尝试:
|
|
|
|
哇,它工作了!现在,我们相信我们可以通过Grape让Jenkins导入任何恶意库!然而,下一个问题是,如何获得代码执行?
代码执行之路
在利用中,目标始终是将读取原语或写入原语升级为代码执行!从前一节中,我们可以通过Grape将恶意JAR文件写入远程Jenkins服务器。然而,下一个问题是如何执行代码?
通过深入研究Groovy上的Grape实现,我们意识到库获取是由类groovy.grape.GrapeIvy完成的!我们开始寻找是否有任何方法可以利用,并注意到了一个有趣的方法processOtherServices(…)!
|
|
JAR文件只是ZIP格式的子集。在processOtherServices(…)中,如果有一些指定的入口点,Grape会注册服务。其中,Runner引起了我的兴趣。通过查看processRunners(…)的实现,我们发现了这个:
|
|
这里我们看到了newInstance()。这是否意味着我们可以在任何类上调用构造函数?是的,所以,我们可以创建一个恶意JAR文件,并将类名放入文件META-INF/services/org.codehaus.groovy.plugins.Runners中,我们就可以调用构造函数并执行任意代码!
以下是完整的利用:
|
|
|
|
PoC:
|
|
视频
[视频内容]
结语
通过这个利用,我们可以获得远程Jenkins服务器的完全访问权限!我们使用元编程在编译时导入恶意JAR文件,并通过Runner服务执行任意代码!尽管Jenkins上有内置的Groovy沙箱(脚本安全插件)来保护Pipeline,但它毫无用处,因为漏洞在编译时,而不是运行时!
因为这是Groovy核心上的攻击向量,所有与Groovy解析器相关的方法都会受到影响!它打破了开发者的思维,认为没有执行就没有问题。这也是一个需要计算机科学知识的攻击向量。否则,你无法想到元编程!这就是使这个漏洞有趣的原因。除了我报告的入口点doCheckScriptCompile(…)和toJson(…)之外,在漏洞修复后,Mikhail Egorov还快速找到了另一个触发此漏洞的入口点!
除此之外,这个漏洞还可以与我之前关于Hacking Jenkins Part 1的利用链结合,绕过Overall/Read限制,实现应得的预认证远程代码执行。如果你完全理解了文章,你就知道如何链式利用:P
感谢阅读这篇文章,希望你喜欢!这是Hacking Jenkins系列的结束,我将在未来发布更多有趣的研究!
2019/07/02 更新
2019/05/10 更新
2019/02/22 更新