CVE-2015-5377 (published 2018-03-06T20:29Z)

Elasticsearch before 1.6.1 allows remote attackers to execute arbitrary code via unspecified vectors involving the transport protocol.  NOTE: ZDI appears to claim that CVE-2015-3253 and CVE-2015-5377 are the same vulnerability

Severity: HIGH
CVSS v2 base score: 7.5 (AV:N/AC:L/Au:N/C:P/I:P/A:P)
CVSS v3 base score: 9.8, CRITICAL (CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-74
cwe_name: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
description: The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
extended_description: Software or other automated logic has certain assumptions about what constitutes data and control respectively. It is the lack of verification of these assumptions for user-controlled input that leads to injection problems. Injection problems encompass a wide variety of issues -- all mitigated in very different ways and usually attempted in order to alter the control flow of the process. For this reason, the most effective way to discuss these weaknesses is to note the distinct features that classify them as injection weaknesses. The most important issue to note is that all injection problems share one thing in common -- i.e., they allow for the injection of control plane data into the user-controlled data plane. This means that the execution of the process may be altered by sending code in through legitimate data channels, using no other mechanism. While buffer overflows, and many other flaws, involve the use of some further issue to gain execution, injection problems need only for the data to be parsed.
         

Fix information:
repo_url: https://github.com/elastic/elasticsearch
commit_details: repo_url: https://github.com/elastic/elasticsearch
author: Simon Willnauer
author_date: 2015-07-06 16:11:37+02:00
msg: Restrict Exception deserialization to Core and JDK classes

when 3rd party exceptions are deserialized they might carry
classes not present on the deserializing node. This causes hard
exceptions and looses the exception entirely. This commit restricts
the classes we support for deserialization for core and selected JDK
classes to guarantee they are present on both source and target nodes
repository: repo_url: https://github.com/elastic/elasticsearch
repo_name: elastic/elasticsearch
description: Free and Open, Distributed, RESTful Search Engine
repo_language: Java
owner: elastic
file_changes: filename: ThrowableObjectInputStream.java
change_type: ModificationType.MODIFY
diff: @@ -19,9 +19,15 @@
 
 package org.elasticsearch.common.io;
 
+import com.fasterxml.jackson.core.JsonLocation;
+import com.google.common.collect.ImmutableMap;
 import org.elasticsearch.common.Classes;
+import org.elasticsearch.common.collect.IdentityHashSet;
+import org.joda.time.DateTimeFieldType;
 
 import java.io.*;
+import java.net.*;
+import java.util.*;
 
 /**
  *
@@ -61,11 +67,11 @@ public class ThrowableObjectInputStream extends ObjectInputStream {
             case ThrowableObjectOutputStream.TYPE_STACKTRACEELEMENT:
                 return ObjectStreamClass.lookup(StackTraceElement.class);
             case ThrowableObjectOutputStream.TYPE_FAT_DESCRIPTOR:
-                return super.readClassDescriptor();
+                return verify(super.readClassDescriptor());
             case ThrowableObjectOutputStream.TYPE_THIN_DESCRIPTOR:
                 String className = readUTF();
                 Class<?> clazz = loadClass(className);
-                return ObjectStreamClass.lookup(clazz);
+                return verify(ObjectStreamClass.lookup(clazz));
             default:
                 throw new StreamCorruptedException(
                         "Unexpected class descriptor type: " + type);
@@ -96,4 +102,40 @@ public class ThrowableObjectInputStream extends ObjectInputStream {
         }
         return clazz;
     }
+
+    private static final Set<Class<?>> CLASS_WHITELIST;
+    private static final Set<Package> PKG_WHITELIST;
+    static {
+        IdentityHashSet<Class<?>> classes = new IdentityHashSet<>();
+        classes.add(String.class);
+        // inet stuff is needed for DiscoveryNode
+        classes.add(Inet6Address.class);
+        classes.add(Inet4Address.class);
+        classes.add(InetAddress.class);
+        classes.add(InetSocketAddress.class);
+        classes.add(SocketAddress.class);
+        classes.add(StackTraceElement.class);
+        classes.add(JsonLocation.class); // JsonParseException uses this
+        IdentityHashSet<Package> packages = new IdentityHashSet<>();
+        packages.add(Integer.class.getPackage()); // java.lang
+        packages.add(List.class.getPackage()); // java.util
+        packages.add(ImmutableMap.class.getPackage()); // com.google.common.collect
+        packages.add(DateTimeFieldType.class.getPackage()); // org.joda.time
+        CLASS_WHITELIST = Collections.unmodifiableSet(classes);
+        PKG_WHITELIST = Collections.unmodifiableSet(packages);
+    }
+
+    private ObjectStreamClass verify(ObjectStreamClass streamClass) throws IOException, ClassNotFoundException {
+        Class<?> aClass = resolveClass(streamClass);
+        Package pkg = aClass.getPackage();
+        if (aClass.isPrimitive() // primitives are fine
+                || aClass.isArray() // arrays are ok too
+                || Throwable.class.isAssignableFrom(aClass)// exceptions are fine
+                || CLASS_WHITELIST.contains(aClass) // whitelist JDK stuff we need
+                || PKG_WHITELIST.contains(aClass.getPackage())
+                || pkg.getName().startsWith("org.elasticsearch")) { // es classes are ok
+            return streamClass;
+        }
+        throw new NotSerializableException(aClass.getName());
+    }
 }

programming_language: Java
filename: ThrowableObjectOutputStream.java
change_type: ModificationType.MODIFY
diff: @@ -19,10 +19,7 @@
 
 package org.elasticsearch.common.io;
 
-import java.io.IOException;
-import java.io.ObjectOutputStream;
-import java.io.ObjectStreamClass;
-import java.io.OutputStream;
+import java.io.*;
 
 /**
  *
@@ -65,4 +62,30 @@ public class ThrowableObjectOutputStream extends ObjectOutputStream {
             }
         }
     }
+
+    /**
+     * Simple helper method to roundtrip a serializable object within the ThrowableObjectInput/Output stream
+     */
+    public static <T extends Serializable> T serialize(T t) throws IOException, ClassNotFoundException {
+        ByteArrayOutputStream stream = new ByteArrayOutputStream();
+        try (ThrowableObjectOutputStream outputStream = new ThrowableObjectOutputStream(stream)) {
+            outputStream.writeObject(t);
+        }
+        try (ThrowableObjectInputStream in = new ThrowableObjectInputStream(new ByteArrayInputStream(stream.toByteArray()))) {
+            return (T) in.readObject();
+        }
+    }
+
+    /**
+     * Returns <code>true</code> iff the exception can be serialized and deserialized using
+     * {@link ThrowableObjectOutputStream} and {@link ThrowableObjectInputStream}. Otherwise <code>false</code>
+     */
+    public static boolean canSerialize(Throwable t) {
+        try {
+            serialize(t);
+            return true;
+        } catch (Throwable throwable) {
+            return false;
+        }
+    }
 }

programming_language: Java
filename: LocalTransportChannel.java
change_type: ModificationType.MODIFY
diff: @@ -86,6 +86,10 @@ public class LocalTransportChannel implements TransportChannel {
     @Override
     public void sendResponse(Throwable error) throws IOException {
         BytesStreamOutput stream = new BytesStreamOutput();
+        if (ThrowableObjectOutputStream.canSerialize(error) == false) {
+            assert false : "Can not serialize exception: " + error; // make sure tests fail
+            error = new NotSerializableTransportException(error);
+        }
         try {
             writeResponseExceptionHeader(stream);
             RemoteTransportException tx = new RemoteTransportException(targetTransport.nodeName(), targetTransport.boundAddress().boundAddress(), action, error);

programming_language: Java
filename: NettyTransportChannel.java
change_type: ModificationType.MODIFY
diff: @@ -117,6 +117,10 @@ public class NettyTransportChannel implements TransportChannel {
     @Override
     public void sendResponse(Throwable error) throws IOException {
         BytesStreamOutput stream = new BytesStreamOutput();
+        if (ThrowableObjectOutputStream.canSerialize(error) == false) {
+            assert false : "Can not serialize exception: " + error; // make sure tests fail
+            error = new NotSerializableTransportException(error);
+        }
         try {
             stream.skip(NettyHeader.HEADER_SIZE);
             RemoteTransportException tx = new RemoteTransportException(transport.nodeName(), transport.wrapAddress(channel.getLocalAddress()), action, error);

programming_language: Java
filename: ExceptionsSerializationTests.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,98 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.elasticsearch;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.reflect.TypeToken;
+import org.apache.lucene.store.AlreadyClosedException;
+import org.elasticsearch.cluster.node.DiscoveryNode;
+import org.elasticsearch.common.collect.Tuple;
+import org.elasticsearch.common.io.ThrowableObjectInputStream;
+import org.elasticsearch.common.transport.InetSocketTransportAddress;
+import org.elasticsearch.index.shard.IndexShard;
+import org.elasticsearch.index.shard.IndexShardState;
+import org.elasticsearch.index.shard.ShardId;
+import org.elasticsearch.indices.recovery.RecoveryFailedException;
+import org.elasticsearch.test.ElasticsearchTestCase;
+import org.elasticsearch.transport.ConnectTransportException;
+import org.junit.Test;
+
+import java.io.*;
+import java.net.InetAddress;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.elasticsearch.common.io.ThrowableObjectOutputStream.serialize;
+
+public class ExceptionsSerializationTests extends ElasticsearchTestCase {
+
+    public void testBasicExceptions() throws IOException, ClassNotFoundException {
+        ShardId id = new ShardId("foo", 1);
+        DiscoveryNode src = new DiscoveryNode("someNode", new InetSocketTransportAddress("127.0.0.1", 6666), Version.CURRENT);
+        DiscoveryNode target = new DiscoveryNode("otherNode", new InetSocketTransportAddress("127.0.0.1", 8888), Version.CURRENT);
+
+        RecoveryFailedException ex = new RecoveryFailedException(id, src, target, new AlreadyClosedException("closed", new SecurityException("booom booom boom", new FileNotFoundException("no such file"))));
+        RecoveryFailedException serialize = serialize(ex);
+        assertEquals(ex.getMessage(), serialize.getMessage());
+        assertEquals(AlreadyClosedException.class, serialize.getCause().getClass());
+        assertEquals(SecurityException.class, serialize.getCause().getCause().getClass());
+        assertEquals(FileNotFoundException.class, serialize.getCause().getCause().getCause().getClass());
+        ConnectTransportException tpEx = new ConnectTransportException(src, "foo", new IllegalArgumentException("boom"));
+        ConnectTransportException serializeTpEx = serialize(tpEx);
+        assertEquals(tpEx.getMessage(), serializeTpEx.getMessage());
+        assertEquals(src, tpEx.node());
+
+        TestException testException = new TestException(Arrays.asList("foo"), EnumSet.allOf(IndexShardState.class), ImmutableMap.<String,String>builder().put("foo", "bar").build(), InetAddress.getByName("localhost"), new Number[] {new Integer(1)});
+        assertEquals(serialize(testException).list.get(0), "foo");
+        assertTrue(serialize(testException).set.containsAll(Arrays.asList(IndexShardState.values())));
+        assertEquals(serialize(testException).map.get("foo"), "bar");
+    }
+
+    public void testPreventBogusFromSerializing() throws IOException, ClassNotFoundException {
+        Serializable[] serializables = new Serializable[] {
+                new AtomicBoolean(false),
+                TypeToken.of(String.class),
+        };
+        for (Serializable s : serializables) {
+            try {
+                serialize(s);
+                fail(s.getClass() + " should fail");
+            } catch (NotSerializableException e) {
+                // all is well
+            }
+        }
+    }
+
+    public static class TestException extends Throwable {
+        final List<String> list;
+        final EnumSet<IndexShardState> set;
+        final Map<String, String> map;
+        final InetAddress address;
+        final Object[] someArray;
+
+        public TestException(List<String> list, EnumSet<IndexShardState> set, Map<String, String> map, InetAddress address, Object[] someArray) {
+            super("foo", null);
+            this.list = list;
+            this.set = set;
+            this.map = map;
+            this.address = address;
+            this.someArray = someArray;
+        }
+    }
+}

programming_language: Java