CVE-2015-5377 (Java, published 2018-03-06T20:29Z)
Description: value: 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 v3 base score: 9.8)
Weakness: CWE-74 \u2014 Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
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.
Repository: https://github.com/elastic/elasticsearch
Files changed:
src/main/java/org/elasticsearch/common/io/ThrowableObjectInputStream.java
src/main/java/org/elasticsearch/common/io/ThrowableObjectOutputStream.java
src/main/java/org/elasticsearch/transport/local/LocalTransportChannel.java
src/main/java/org/elasticsearch/transport/netty/NettyTransportChannel.java
src/test/java/org/elasticsearch/ExceptionsSerializationTests.java
Commit message: 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


Vulnerable code:
                return super.readClassDescriptor();
                return ObjectStreamClass.lookup(clazz);
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.OutputStream;

Fixed code:
import com.fasterxml.jackson.core.JsonLocation;
import com.google.common.collect.ImmutableMap;
import org.elasticsearch.common.collect.IdentityHashSet;
import org.joda.time.DateTimeFieldType;
import java.net.*;
import java.util.*;
                return verify(super.readClassDescriptor());
                return verify(ObjectStreamClass.lookup(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());
    }
import java.io.*;

    /**
     * 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;
        }
    }
        if (ThrowableObjectOutputStream.canSerialize(error) == false) {
            assert false : "Can not serialize exception: " + error; // make sure tests fail
            error = new NotSerializableTransportException(error);
        }
        if (ThrowableObjectOutputStream.canSerialize(error) == false) {
            assert false : "Can not serialize exception: " + error; // make sure tests fail
            error = new NotSerializableTransportException(error);
        }
/*
 * 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;
        }
    }
}