Exploration de la Réflexion en Java
Méthodes fondamentales de l'API Reflection
- Chargement de classe :
Class.forName(String className)permet d'obtenir une référence vers une classe par son nom complet. - Accès aux méthodes :
getMethod(String name, Class>... parameterTypes)pour les méthodes publiques etgetDeclaredMethodpour inclure les méthodes privées. - Récupération des constructeurs :
getConstructor()etgetDeclaredConstructor(). - Manipulation des champs :
getField(String name)etgetDeclaredField(String name)pour accéder aux attributs. - Insatnciation :
newInstance()ouConstructor.newInstance(Object... initargs). - Invocation :
invoke(Object obj, Object... args)pour exécuter une méthode sur une instance.
Distinction entre getConstructor() et getDeclaredConstructor()
La méthode getConstructor() retourne uniquement les contsructeurs publics de la classe. À l'inverse, getDeclaredConstructor() accède à tous les constructeurs déclarés dans la classe, qu'ils soient private, protected ou default.
Modification d'un champ final via la réflexion
Pour modifier un champ marqué final, il faut d'abord rendre le champ accessible, puis modifier les modificateurs internes de l'objet Field pour retirer le flag FINAL.
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
public void modifierChampFinal(Object instance, String nomChamp, Object nouvelleValeur) throws Exception {
Field cibleField = instance.getClass().getDeclaredField(nomChamp);
cibleField.setAccessible(true);
// Accès au champ 'modifiers' de la classe Field pour supprimer le flag FINAL
Field modificateurs = Field.class.getDeclaredField("modifiers");
modificateurs.setAccessible(true);
modificateurs.setInt(cibleField, cibleField.getModifiers() & ~Modifier.FINAL);
cibleField.set(instance, nouvelleValeur);
}
Stratégies d'obtention d'un objet Class
objet.getClass(): Utilisé lorsqu'une instance est déjà disponible.MaClasse.class: Utilisé lorsque la classe est connue à la compilation.Class.forName("com.package.MaClasse"): Utilisé pour un chargement dynamique basé sur une chaîne de caractères.
Instanciation de classes complexes
Pour un Singleton, on invoque généralement la méthode statique de récupération d'instance (ex: getInstance). Si une classe n'a pas de constructeur sans argument, comme ProcessBuilder, on doit cibler spécifiquement un constructeur existant :
// Exemple avec ProcessBuilder(List<String> command)
Class> pbClass = Class.forName("java.lang.ProcessBuilder");
Object instance = pbClass.getConstructor(java.util.List.class).newInstance(java.util.Arrays.asList("calc.exe"));
pbClass.getMethod("start").invoke(instance);
Proxys Dynamiques et Chargement de Classes
Proxy vs Réflexion
La réflexion est un mécanisme d'introspection permettant d'analyser et de manipuler le code au runtime. Le proxy dynamique est un motif de conception (Design Pattern) qui utilise la réflexion pour créer des objets capables d'intercepter les appels vers une interface, ajoutant ainsi une logique transverse (AOP).
Fonctionnement des Web Shells JSP (type Behinder)
Ces outils utilisent un ClassLoader personnalisé. Le client envoie un bytecode chiffré (AES). Le serveur le déchiffre, puis utilise une méthode exposant defineClass pour transformer ce tableau d'octets en une classe Java active en mémoire, permettant l'exécution de code arbitraire sans écrire de fichier suspect sur le disque.
Analyse des chaînes de gadgets (Gadget Chains)
Chaîne URLDNS
Il s'agit d'une chaîne de désérialisation simple utilisée pour détecter une vulnérabilité. Elle repose sur HashMap.readObject(). Lors de la désérialisation, HashMap recalcule le hash des clés. Si la clé est un objet java.net.URL, son hashCode() déclenche une résolution DNS via le URLStreamHandler si la valeur mise en cache est -1.
CommonsCollections 1 (CC1)
CC1 exploite InvokerTransformer pour appeler des méthodes arbitraires. Elle utilise soit TransformedMap soit LazyMap. L'élément déclencheur est souvent AnnotationInvocationHandler.readObject(), qui manipule la Map désérialisée, provoquant l'appel à transform() ou get(). Pour contourner l'impossibilité de sérialiser Runtime, on utilise Runtime.class couplé à une chaîne de transformers commençant par un ConstantTransformer.
CommonsCollections 2 (CC2) et TemplatesImpl
CC2 utilise PriorityQueue et un TransformingComparator. Lors de la désérialisation, PriorityQueue réorganise ses éléments via siftDown, ce qui appelle le comparateur et donc le transform(). L'utilisation de TemplatesImpl permet de charger du bytecode malveillant via le champ _bytecodes lors de l'appel à newTransformer().
// Exemple de génération de bytecode avec Javassist
ClassPool poule = ClassPool.getDefault();
poule.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass ctClasse = poule.makeClass("PayloadExploit");
ctClasse.setSuperclass(poule.get(AbstractTranslet.class.getName()));
String codeExec = "java.lang.Runtime.getRuntime().exec('calc.exe');";
ctClasse.makeClassInitializer().insertBefore(codeExec);
byte[] octetsBytecode = ctClasse.toBytecode();
CommonsCollections 6 (CC6)
CC6 est une variante universelle utilisant TiedMapEntry pour lier une LazyMap à une HashMap. On utilise souvent une réflexion finale pour injecter la vraie chaîne de transformation après l'insertion dans la Map afin d'éviter de déclencher l'exploit localement pendant la construction du payload.
CommonsCollections 7 (CC7)
Cette chaîne exploite les collisions de hashcode entre deux clés (comme "yy" et "zZ") dans une Hashtable. Lors de la désérialisation de la Hashtable, la collision force l'appel à equals() sur une LazyMap, ce qui déclenche la chaîne de gadgets.