Jenkins allows Deserialization of Untrusted Data via an XML File
Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.
Affected versions
1.643 → fixed in 1.6500 → fixed in 1.642.2
Details
Multiple unspecified API endpoints in Jenkins before 1.650 and LTS before 1.642.2 allow remote authenticated users to execute arbitrary code via serialized data in an XML file, related to XStream and groovy.util.Expando.
The fix
[FIX SECURITY-247] Prevent loading of MethodClosure from
core/src/main/java/hudson/util/XStream2.java+18 −0
@@ -159,6 +159,8 @@ private void init() {// but before reflection-based one kicks in.registerConverter(new AssociatedConverterImpl(this), -10);+registerConverter(new BlacklistedTypesConverter(), PRIORITY_VERY_HIGH); // SECURITY-247 defense+registerConverter(new DynamicProxyConverter(getMapper()) { // SECURITY-105 defense@Override public boolean canConvert(Class type) {return /* this precedes NullConverter */ type != null && super.canConvert(type);@@ -434,4 +436,20 @@ class PluginClassOwnership implements ClassOwnership {}+private static class BlacklistedTypesConverter implements Converter {+@Override+public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {+throw new UnsupportedOperationException("Cannot marshal MethodClosure");+}++@Override+public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {+throw new ConversionException("Cannot load MethodClosure for security reasons");+}++@Override+public boolean canConvert(Class type) {+return type != null && "org.codehaus.groovy.runtime.MethodClosure".equals(type.getName());+}+}}
test/src/test/java/hudson/util/XStream2Security247Test.java+43 −0
@@ -0,0 +1,43 @@+package hudson.util;++import hudson.Functions;+import hudson.model.Items;+import org.apache.commons.io.FileUtils;+import org.junit.Rule;+import org.junit.Test;+import org.jvnet.hudson.test.Issue;+import org.jvnet.hudson.test.JenkinsRule;++import java.io.File;++import static org.junit.Assert.assertFalse;++public class XStream2Security247Test {++@Rule+public JenkinsRule j = new JenkinsRule();++@Test+@Issue("SECURITY-247")+public void dontUnmarshalMethodClosure() throws Exception {+if (Functions.isWindows()) return;+File exploitFile = new File("/tmp/jenkins-security247test");+try {+// be extra sure there's no file already+if (exploitFile.exists() && !exploitFile.delete()) {+throw new IllegalStateException("file exists and cannot be deleted");+}+File tempJobDir = new File(j.jenkins.getRootDir(), "security247");+FileUtils.copyInputStreamToFile(XStream2Security247Test.class.getResourceAsStream("/hudson/util/XStream2Security247Test/config.xml"),+new File(tempJobDir, "config.xml"));+try {+Items.load(j.jenkins, tempJobDir);+} catch (Exception e) {+// ignore+}+assertFalse("no file should be created here", exploitFile.exists());+} finally {+exploitFile.delete();+}+}+}
test/src/test/resources/hudson/util/XStream2Security247Test/config.xml+27 −0
@@ -0,0 +1,27 @@+<map>+<entry>+<groovy.util.Expando>+<expandoProperties>+<entry>+<string>hashCode</string>+<org.codehaus.groovy.runtime.MethodClosure>+<delegate class="groovy.util.Expando" reference="../../../.."/>+<owner class="java.lang.ProcessBuilder">+<command>+<string>touch</string>+<string>/tmp/jenkins-security247test</string>+</command>+<redirectErrorStream>false</redirectErrorStream>+</owner>+<resolveStrategy>0</resolveStrategy>+<directive>0</directive>+<parameterTypes/>+<maximumNumberOfParameters>0</maximumNumberOfParameters>+<method>start</method>+</org.codehaus.groovy.runtime.MethodClosure>+</entry>+</expandoProperties>+</groovy.util.Expando>+<int>1</int>+</entry>+</map>test.../hudson/util/XStream2Security247Test.java | 91 +++++++++++++++++--.../util/XStream2Security247Test/config.xml | 2 +-2 files changed, 85 insertions(+), 8 deletions(-)
test/src/test/java/hudson/util/XStream2Security247Test.java+84 −7
@@ -1,35 +1,65 @@package hudson.util;-import hudson.Functions;import hudson.model.Items;-import org.apache.commons.io.FileUtils;+import org.apache.commons.io.*;+import org.apache.commons.io.IOUtils;+import org.junit.Before;import org.junit.Rule;import org.junit.Test;+import org.junit.rules.TemporaryFolder;import org.jvnet.hudson.test.Issue;import org.jvnet.hudson.test.JenkinsRule;+import org.kohsuke.stapler.StaplerRequest;+import org.kohsuke.stapler.StaplerResponse;+import org.mockito.Mock;+import org.mockito.MockitoAnnotations;+import javax.servlet.ServletInputStream;import java.io.File;+import java.io.IOException;+import java.io.InputStream;import static org.junit.Assert.assertFalse;+import static org.mockito.Mockito.when;public class XStream2Security247Test {@Rulepublic JenkinsRule j = new JenkinsRule();+@Rule+public TemporaryFolder f = new TemporaryFolder();++@Mock+private StaplerRequest req;++@Mock+private StaplerResponse rsp;++@Before+public void setUp() throws Exception {+MockitoAnnotations.initMocks(this);+}+@Test@Issue("SECURITY-247")-public void dontUnmarshalMethodClosure() throws Exception {-if (Functions.isWindows()) return;-File exploitFile = new File("/tmp/jenkins-security247test");+public void testXmlLoad() throws Exception {+File exploitFile = f.newFile();try {// be extra sure there's no file alreadyif (exploitFile.exists() && !exploitFile.delete()) {throw new IllegalStateException("file exists and cannot be deleted");}File tempJobDir = new File(j.jenkins.getRootDir(), "security247");-FileUtils.copyInputStreamToFile(XStream2Security247Test.class.getResourceAsStream("/hudson/util/XStream2Security247Test/config.xml"),-new File(tempJobDir, "config.xml"));++String exploitXml = org.apache.commons.io.IOUtils.toString(+XStream2Security247Test.class.getResourceAsStream(+"/hudson/util/XStream2Security247Test/config.xml"), "UTF-8");++exploitXml = exploitXml.replace("@TOKEN@", exploitFile.getAbsolutePath());++FileUtils.write(new File(tempJobDir, "config.xml"), exploitXml);+try {Items.load(j.jenkins, tempJobDir);} catch (Exception e) {@@ -40,4 +70,51 @@ public void dontUnmarshalMethodClosure() throws Exception {exploitFile.delete();}}++@Test+@Issue("SECURITY-247")+public void testPostJobXml() throws Exception {+File exploitFile = f.newFile();+try {+// be extra sure there's no file already+if (exploitFile.exists() && !exploitFile.delete()) {+throw new IllegalStateException("file exists and cannot be deleted");+}+File tempJobDir = new File(j.jenkins.getRootDir(), "security247");++String exploitXml = org.apache.commons.io.IOUtils.toString(+XStream2Security247Test.class.getResourceAsStream(+"/hudson/util/XStream2Security247Test/config.xml"), "UTF-8");++exploitXml = exploitXml.replace("@TOKEN@", exploitFile.getAbsolutePath());++when(req.getMethod()).thenReturn("POST");+when(req.getInputStream()).thenReturn(new Stream(IOUtils.toInputStream(exploitXml)));+when(req.getContentType()).thenReturn("application/xml");+when(req.getParameter("name")).thenReturn("foo");++try {+j.jenkins.doCreateItem(req, rsp);+} catch (Exception e) {+// don't care+}++assertFalse("no file should be created here", exploitFile.exists());+} finally {+exploitFile.delete();+}+}++private static class Stream extends ServletInputStream {+private final InputStream inner;++public Stream(final InputStream inner) {+this.inner = inner;+}++@Override+public int read() throws IOException {+return inner.read();+}+}}
test/src/test/resources/hudson/util/XStream2Security247Test/config.xml+1 −1
@@ -9,7 +9,7 @@<owner class="java.lang.ProcessBuilder"><command><string>touch</string>-<string>/tmp/jenkins-security247test</string>+<string>@TOKEN@</string></command><redirectErrorStream>false</redirectErrorStream></owner>coveragecore/src/main/java/hudson/util/XStream2.java | 16 +++++++++++++---1 file changed, 13 insertions(+), 3 deletions(-)
core/src/main/java/hudson/util/XStream2.java+13 −3
@@ -47,6 +47,7 @@import hudson.PluginManager;import hudson.PluginWrapper;import hudson.diagnosis.OldDataMonitor;+import hudson.remoting.ClassFilter;import hudson.util.xstream.ImmutableSetConverter;import hudson.util.xstream.ImmutableSortedSetConverter;import jenkins.model.Jenkins;@@ -439,17 +440,26 @@ class PluginClassOwnership implements ClassOwnership {private static class BlacklistedTypesConverter implements Converter {@Overridepublic void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {-throw new UnsupportedOperationException("Cannot marshal MethodClosure");+throw new UnsupportedOperationException("Refusing to marshal for security reasons");}@Overridepublic Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {-throw new ConversionException("Cannot load MethodClosure for security reasons");+throw new ConversionException("Refusing to unmarshal for security reasons");}@Overridepublic boolean canConvert(Class type) {-return type != null && "org.codehaus.groovy.runtime.MethodClosure".equals(type.getName());+if (type == null) {+return false;+}+try {+ClassFilter.DEFAULT.check(type.getName());+} catch (SecurityException se) {+// claim we can convert all the scary stuff so we can throw exceptions when attempting to do so+return true;+}+return false;}}}core/src/main/java/hudson/util/XStream2.java | 1 +1 file changed, 1 insertion(+)
core/src/main/java/hudson/util/XStream2.java+1 −0
@@ -454,6 +454,7 @@ public boolean canConvert(Class type) {return false;}try {+ClassFilter.DEFAULT.check(type);ClassFilter.DEFAULT.check(type.getName());} catch (SecurityException se) {// claim we can convert all the scary stuff so we can throw exceptions when attempting to do so
References
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2016-0792
- WEBhttps://github.com/jenkinsci/jenkins/commit/7f202f0317e60cd3160f61467b8558f864f83f41
- WEBhttps://access.redhat.com/errata/RHSA-2016:0711
- PACKAGEhttps://github.com/jenkinsci/jenkins
- WEBhttps://wiki.jenkins-ci.org/display/SECURITY/Jenkins+Security+Advisory+2016-02-24
- WEBhttps://www.contrastsecurity.com/security-influencers/serialization-must-die-act-2-xstream
- WEBhttps://www.exploit-db.com/exploits/42394
- WEBhttps://www.exploit-db.com/exploits/43375
- WEBhttp://rhn.redhat.com/errata/RHSA-2016-1773.html