0x00 前言

知识补漏行动继续,这次是jenkins。

0x01 环境搭建

搭环境最惨的就是踩了一堆坑之后发现前边做的都是没用的。。。

1
2
3
4
5
6
git clone https://github.com/jenkinsci/jenkins.git
cd jenkins
git checkout jenkins-2.136 #默认关闭anonymous read access的情况下,影响版本为<=1.137
mvn clean install -pl war -am -DskipTests #编译war包
cd war
mvnDebug org.jenkins-ci.tools:maven-jenkins-dev-plugin:run #以debug模式运行,调试端口8000,web端口8080,暂时不知道咋换。。。

接下来在idea配置远程调试:

注意以调试模式运行时,只有接收到idea的调试链接之后才会继续运行。

调试模式启动的jenkins没有任何插件,需要手工安装,可以将vulhub环境中的/var/jenkins_home/plugins复制到本地的jenkins/war/work/plugins。也没有默认的登陆认证,也可以用vulhub中的配置文件。

另外要注意的一点是,jenkins的默认配置中,匿名用户的读权限也是默认关闭的:

0x02 漏洞分析

路由解析

jenkins/war/src/main/webapp/WEB-INF/web.xml

从web.xml可以看到jenkins将所有的请求发送到org.kohsuke.stapler.Stapler这个servlet进行统一处理,跟进看一下org/kohsuke/stapler/stapler/1.254.1/stapler-1.254.1-sources.jar!/org/kohsuke/stapler/Stapler.javaservice方法:

在处理一些特殊情况之后,会创建一个根结点(这里暂时理解为jenkins对路由的处理是将其转化为一个一个的节点),进入invoke方法:

org/kohsuke/stapler/stapler/1.254.1/stapler-1.254.1-sources.jar!/org/kohsuke/stapler/Stapler.java

在这里将url按照/进行分割,并进一步封装request和response对象。

org/kohsuke/stapler/stapler/1.254.1/stapler-1.254.1-sources.jar!/org/kohsuke/stapler/TokenList.java

最后请求会进入tryInvoke方法中进行处理:

org/kohsuke/stapler/stapler/1.254.1/stapler-1.254.1-sources.jar!/org/kohsuke/stapler/Stapler.java

如果节点的类型为StaplerProxy,会检查用户是否有权限访问该路由:

jenkins/core/src/main/java/jenkins/model/Jenkins.java

在第一次权限检查失败之后,会有一个二次检查:

如果我们访问的url在白名单中,是可以通过权限检测的。

回到tryInvoke方法:

根据node获取metaClass,在这个过程中有一个buildDispatchers方法,它的主要作用就是寻找对应的node节点与相应的处理方法(继承家族树中的所有类)并把这个方法加入到分配器dispatchers中。

由于这个方法特别长,总结起来就是遍历一个方法列表,如果有符合如下条件的dispatcher,就添加到metaClass.dispatchers中:

  • .do(...)也就是do(...)@WebMethod标注的方法
  • .doIndex(...)
  • js也就是js(...)
  • @JavaScriptMethod标注的方法
  • NODE.getTOKEN()也就是get()
  • NODE.getTOKEN(StaplerRequest)也就是get(StaplerRequest)
  • .get(String)也就是get(String)
  • .get(int)也就是get(int)
  • .get(long)也就是get(long)
  • .getDynamic(,...)也就是getDynamic()
  • .doDynamic(...)也就是doDynamic()

然后遍历metaClass的所有dispatcher,找到符合路由的dispatcher进行调用:

继续跟进:

org/kohsuke/stapler/stapler/1.254.1/stapler-1.254.1-sources.jar!/org/kohsuke/stapler/MetaClass.java

这里首先调用了getSecurityRealm方法,返回的结果作为新的node,再次进入tryInvoke方法进行递归解析,直到url解析完毕。

到这里路由解析的流程就梳理清楚了,如果能够找到token对应的方法,就调用,找不到就作为上一个方法的参数:

1
http://jenkin.local/adjuncts/whatever/class/classLoader/resource/index.jsp/content
1
2
3
4
5
jenkins.model.Jenkins.getAdjuncts("whatever") 
.getClass()
.getClassLoader()
.getResource("index.jsp")
.getContent()

这个漏洞利用的核心点就在于通过寻找getter方法,构成一条利用链,直到rce。

寻找利用链

payload:

1
http://127.0.0.1:8080/jenkins/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript?sandbox=true&value=import+groovy.transform.*%0a%40ASTTest(value%3d%7bassert+java.lang.Runtime.getRuntime().exec(%22open+%2fApplications%2fCalculator.app%22)%7d)%0aclass+Person%7b%7d

从前面的分析中我们知道路由/securityRealm是在白名单中的,任何权限都可以访问:

解析之后会得到一个HudsonPrivateSecurityRealm类型的对象:

jenkins/core/src/main/java/jenkins/model/Jenkins.java

查看对应的dispatchers,发现能够接收用户输入的只有:

同样的方法我们会得到一个hudson.model.User的对象,到这里看起来还没有利用漏洞的点,继续找接收用户输入的dispatcher:

jenkins/core/src/main/java/hudson/model/DescriptorByNameOwner.java

一路跟进来到jenkins/core/src/main/java/jenkins/model/Jenkins.java

这里会获取当前jenkins安装的所有插件,如果与传入参数匹配,则返回。这里选择了org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript这个插件,希望通过执行groovy脚本来达到rce的目的。

org/jenkins-ci/plugins/script-security/1.13/script-security-1.13-sources.jar!/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript.java

调用栈如下:

下面就是在我看来这个洞最nb的地方,通常我们认为恶意代码只有在运行的时候才能造成危害,但是编程语言为了让自身更加灵活,会赋予自身许多动态特性,比如java中的字节码,是在编译的时候才将字节码插入指定的类中。我们能否寻找一些在编译时就执行命令的groovy script呢?答案当然是肯定的。

在orange大佬提出这个思路之后,许许多多的大佬贡献了非常多的利用方式,在这篇Jenkins RCE漏洞分析汇总中有详细的记录,就不再copy了XD。

0x03 参考链接

Jenkins调试的两种方法

[Jenkins RCE分析(CVE-2018-1000861分析)](https://lucifaer.com/2019/03/04/Jenkins RCE分析(CVE-2018-1000861分析))

Hacking Jenkins Part 1 - Play with Dynamic Routing

Hacking Jenkins Part 2 - Abusing Meta Programming for Unauthenticated RCE!

Jenkins RCE漏洞分析汇总