L'application auditée est constituée d'une applet qui s'exécute dans le browser, et transmet différents messages au serveur. La décompilation de cette applet montre que chaque message transmis est en fait un objet java sérialisé. Par ailleurs en plaçant un sniffeur réseau, on voit rapidement passer des chaînes de caractères dans ces objets qui ressemblent furieusement à des requêtes SQL complètes ; de quoi titiller notre curiosité.

Le format de sérialization des objets est décrit en détail dans la documentation Java, plus particulièrement sur java.sun.com.

Cette syntaxe est très simple, et fondée sur le principe du stream : il n'y a jamais besoin de revenir en arrière dans le flux d'octets.
Tous les entiers utilisés sont encodés en big endian (network byte order) ; les chaînes de caractères sont en java-utf8, et préfixées par leur taille (sur 2 octets en général, 8 pour très grandes chaînes).

Format des données

Chaque paquet commence par une signature caractéristique de 2 octets (0xaced), suivi du numéro de version du stream (5), sur 2 octets également.

On trouve ensuite un objet java. Un objet sérializé est constitué de la description de sa classe, puis de la liste des valeurs de ses attributs. Lors de la désérialization, chaque élément complètement décodé se voit attribuer un numéro de séquence, et par la suite un élément encodé peut y faire référence par cet index. Cela permet de gagner énormément de place lors de l'encodage d'un tableau d'objets par exemple, puisque la description de classe n'apparaitra qu'une fois dans le flux de données.

La description d'une classe commence par le nom de la classe, suivi de la liste des attributs des objets de cette classe, et enfin la descriptaion de la classe parent (en cas d'héritage)

La liste d'attributs n'inclut que les champs ajoutés par la classe en cours de définition, ceux-ci s'ajoutant de manière implicite à la liste des attributs hérités.
Chaque attribut est un couple (type, nom). La classe des attributs détermine la manière d'interpréter les données représentant la valeur de l'attribut dans l'objet. Si un attribut est de type 'object' ou 'array', suit le nom de la classe de cet objet (ou des objets contenus), comme nous allons le voir.

Application

Pour illustrer ceci, reprenons l'exemple de la doc officielle :

00: ac ed 00 05 73 72 00 04 4c 69 73 74 69 c8 8a 15 >....sr..Listi...<
10: 40 16 ae 68 02 00 02 49 00 05 76 61 6c 75 65 4c >Z......I..valueL<
20: 00 04 6e 65 78 74 74 00 06 4c 4c 69 73 74 3b 78 >..nextt..LList;x<
30: 70 00 00 00 11 73 71 00 7e 00 00 00 00 00 13 70 >p....sq.~......p<
40: 71 00 7e 00 03 >q.~..<

Interprétation :

  • aced La signature 'java object stream'
  • 0005 La version du stream
  • 73 Cet octet indique qu'un objet java générique suit, on lit alors la description de sa classe
    • 72 C'est l'identifiant d'un 'class descriptor', suit le nom de la classe
    • 0004 4c697374 encodage du nom de la classe: 'List'
    • 69c88a154016ae68 serial number associé à cette classe (je ne sais pas à quoi il sert)
    • A partir de ce moment, cette classe se voit associer un handle, et on peut y faire référence par son numéro (ici 0x7e0000, car c'est le premier élément décodé)
    • 02 Suite du décodage de la classe: ceci est un octet contenant certains flags, ici 'serializable'
    • 0002 Nombre d'attributs défini par cette classe : 2
    • 49 Type du premier attribut: 49 => integer
    • 0005 76616c7565 Nom du premier attribut: 'value'
    • 4c Type du second attribut: 4c => object
    • 0004 6e657874 Nom du second attribut: 'next'
    • Comme cet attribut est de type object, suit le nom de sa classe
    • 74 identifiant pour 'string'
    • 0006 4c4c6973743b contenu de la chaîne: 'LList;'
    • Les attributs de type 'object' et 'array' ont le nom de la classe encodés d'une manière spécifique, en l'occurrence une chaîne qui commence par L et se termine par un point virgule.
    • 78 endofblockdata : pas d'annotation (usage inconnu)
    • 70 null => pas de classe parent
    La description de la classe est décodée. Suit alors la liste des attributs de l'objet ; d'après la description de la classe on sait qu'il y en aura 2 :
  • 00000011 Le premier attribut est un entier, ici 17
  • Le deuxième attribut est un objet, que l'on va decoder ici
  • 73 C'est un objet
    • 71 Description de la classe: 71 est la signature d'une référence
    • 007e0000 Le numéro de référence: 0x7e0000, c'est la description de la classe List
    • 00000013 Premier attribut: 19
    • 70 Deuxième attribut: null
    • L'objet est donc un objet de la classe List, ayant pour attributs (19, null)
  • L'objet est donc un objet de la classe List, ayant pour attributs (17, <objet precedemment cité>)

Le stream n'est pas fini, on lit encore un objet:

  • 71 C'est une référence
  • 007e0003 référence 0x7e0003: il s'agit de l'objet que l'on a vu, d'attributs (19, null).

Fin du stream, le décodage est terminé.

Script

Vous pouvez récupérer le script javaserial.rb (license WTFPL)

Si on le lance sur le stream que nous venons de voir, on voit son contenu de manière synthétique :

$ ruby javaserial.rb foo.bin
List
(int) value = 17
(object) next =
List
(int) value = 19
(object) next = nil
List
(int) value = 19
(object) next = nil

Le script permet également de réencoder des objets que l'on a manipulé, par le biais d'un petit script.

En l'occurrence, si l'on veut modifier la valeur du champs 'value' du second objet, on peut s'y prendre comme ceci :

<br />#!/usr/bin/ruby<br />require 'javaserial'<br /><br /># decode les objets du stream contenu dans foo.bin<br />java_stream = JavaObj.load File.open('foo.bin', 'rb') { |fd| fd.read }<br /># modifie le dernier objet du stream<br />java_stream.objects.last['value'] = 42<br /># enregistre le stream modifie dans bla.bin<br />File.open('bla.bin', 'wb') { |fd| fd.write java_stream.save }<br /><br /># affiche l'objet pour verification<br />puts java_stream

L'exécution donne bien

List
(int) value = 17
(object) next =
List
(int) value = 42
(object) next = nil
List
(int) value = 42
(object) next = nil

Pour la petite histoire, l'applet en question utilisait un compte restreint pour les requêtes SQL ; on a donc seulement pu dumper toute la base en patchant les requêtes.