IT Security 101 - FCSC 2020

Vous suivez un cours de sécurité informatique dans une célèbre université. Pour faciliter l'échange de documents, votre professeur a mis en ligne un service pour déposer les devoirs au format PDF.

Hacker dans l'âme, vous voulez évaluer la sécurité de ce système. Vous savez que votre professeur consulte fréquemment ce service, et vous l'avez vu utiliser le lecteur muPDF sur son ordinateur. Vous avez par ailleurs réussi à intercepter les fichiers message.tex et message.pdf envoyés par le directeur de l'université à votre professeur qui indique un deuxième échange de fichier. Grâce à vos incroyables talents, vous avez également intercepté ce deuxième fichier (flag.pdf), mais celui-ci est malheureusement protégé par un mot de passe que vous ne connaissez pas. Saurez-vous malgré tout lire le contenu de ce fichier ?

Note : Les PDFs acceptés sont limités à 8ko.

TL; DR

Génération d’un PDF qui une fois ouvert et déchiffré va envoyer le contenu à un serveur distant. Le challenge se base sur une implémentation du papier Practical Decryption exFiltration: Breaking PDF Encryption

Aperçu du format PDF (Portable Document Format)

Le PDF est un format de document développé par Adobe et largement utilisé dans l’informatique moderne. Son objectif est de garder la mise en page prévu par l’auteur.

Le format PDF est composé d’objets qui peuvent contenir à la fois du texte ou des données binaires.

Format du PDF

Lors de la résolution de ce challenge, il m’a été nécessaire d’apprendre à écrire mes PDF moi-même dans un éditeur de code ( Visual Studio Code pour les intimes ).

Un pdf est composé de plusieurs parties que l’on peut retrouver sur le schéma fait par @Corkami :

En résumé, on retrouve dans un PDF basique :

Les objets présents dans le corps sont entourés de X Y objX est l’index de l’objet, Y est le numéro de génération et se terminent par endobj, il sont ensuite appelés par les autres objets avec la notation X Y R.

Dans le trailer est référencé l’index de l’objet /Catalog, c’est lui qui va définir certaines informations globales mais aussi le lien vers le tableau des pages /Pages.

Le tableau des pages contient un paramètre/Kids qui va contenir des pointeurs vers toutes les pages.

Enfin les différentes page contiennent des pointeurs vers toute sorte d’objets qui contiennent le texte et les images.

Le contenu du PDF est soit représenté sous forme de texte :

X Y obj
<</Something>>>

AAAAAAAAAAAAAAAAAAAA
endobj

Soit sous forme de stream :

X Y obj
<</Something /Length 86>>
stream
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ...
endstream
endobj

De nos jours, les PDF générés contiendront plus d’objets de type stream car cela leur permet de compresser le contenu des objets et donc de réduire la taille finale. On reconnait un objet compressé parcequ’il y a le paramètre /FlateDecode ajouté aux filtres :

X Y obj
<</Filter FlateDecode /Length 85>>
stream
AAAAAAAA
endstream
endobj

Enfin on peut représenter le texte de notre objet en ascii ou en hexadécimal :

(plaintext string)
<hex-encoded string>

Pour savoir plus en détail comment rédiger un pdf : Let’s write a PDF file

La cryptographie dans tout ça ?

Hé ! oui, le but de ce challenge est de déchiffrer un message sans la clé, donc intéressons nous à la cryptographie d’un PDF.

Il est possible de chiffrer un PDF en précisant un owner password et un user password.

Une personne déchiffrant le PDF avec un owner password pourra lire, modifier et rechiffrer le contenu d’un PDF. Une personne déchiffrant le PDF avec un user password pourra seulement le lire.

Comment est défini la cryptographie

Dans l’objet associé au Trailer, est défini deux paramètres utiles :

/Encrypt X Y R
/Id [<xxxxxxxxxxxxxx><xxxxxxxxxxxxxx>]

/Encrypt est l’objet qui va gérer la cryptographie et /Id l’id du document, il est généré de façon aléatoire.

Dans notre objet de Encrypt est défini :

Dans les anciennes version de PDF, le chiffrement était fait par défaut en RC4, il est maintenant effectué en AES-CBC avec du PKCS#5 pour le padding.

Pour générer les clés de chiffrement les données suivantes sont concaténées (dans l’ordre) :

Le tout est haché en MD5 (voir sources) et les 10 premiers bytes du résultat sont utilisés pour chiffrer l’objet.

On a donc un système de KDF en fonction de l’objet chiffré ( ce sera important par la suite).

Enfin pour chiffrer, le contenu de l’objet est paddé avec du PKCS#5 et est préfixé des 16 bytes de l'IV.

En résumé, si on veut modifier le PDF pour notre exploit, il ne faut pas modifier :

Résolution du challenge

Maintenant que les bases sont posées, il est temps de résoudre le challenge qui nous est proposé.

Nous avons pour ce challenge :

Submit automatique des PDF

Pas la partie la plus importante mais pour résoudre ce challenge avec plus de commodité, j’ai écris un script permettant d’envoyer automatiquement mon PDF. Pour cela, un proof of work devait être résolu :

Ci-dessous le script utilisé :

import requests
import hashlib
import sys
import re

# récupération des paramètres pour solve le proof of work
requests = requests.Session()
regex_p = r"name=\"P\" value=\"([a-z\d-]+)"
regex_t = r"name=\"T\" value=\"([a-z\d]+)"
home = requests.get("http://challenges2.france-cybersecurity-challenge.fr:6003/").text
P = re.search(regex_p,home).group(1)
T = re.search(regex_t,home).group(1)


# On trouve un suffixe qui valide le proof of work
i = 0
while True:
    sha = hashlib.sha256((P + str(i)).encode()).hexdigest()
    if(sha.startswith(T)):
        print(P, T, i, sha )
        break
    i = i + 1

# upload du fichier
files = {'file': open(sys.argv[1], 'rb')}
data = {  "P":P,
          "T": T,
          "S" : str(i)}
res = requests.post("http://challenges2.france-cybersecurity-challenge.fr:6003/upload",data=data,files=files)

Création de l’exploit PDF

Dans la description, le lecteur de PDF utilisé pour ouvrir le PDF est indiqué, muPDF, je me suis assez rapidement orienté sur le papier Practical Decryption exFiltration: Breaking PDF Encryption car il n’était pas question d’une attaque où il fallait obtenir un shell, mais plutôt une attaque visant à récupérer le flag déchiffré.

Dans le papier on a plusieurs informations, muPDF 1.4.10 est vulnérable aux attaques A2 et B2, l’attaque A2 étant clairement plus simple à exploiter que l’attaque B2 qui consiste à leaker des morceaux du cipher en profitant du padding.

Le principe de l’attaque A2 est de créer un formulaire de soumission qui serait lancé lors d’un clique de l’utilisateur sur une partie du PDF, et que ce formulaire soumettra un objet en tant que partie de l’URI. L’objet ayant été déchiffré par l’utilisateur lors de l’ouverture du PDF, les données exfiltrés seront en clair.

Il est possible de récupérer des templates d’exploit pour chaque lecteur PDF sur le site de la vulnérabilité.

Voici le code qui permet de soummettre un objet en requête GET à notre serveur d’exfiltration :

À partir de la nous avons toutes les infos qu’il nous faut : données à garder et données à ajouter.

Donc on modifie le PDF A2-link_00-str-unencrypted.pdf en prenant bien soin de remplacer dedans :

A noter qu’il faut préalablement encoder en hexadécimal le stream de l’objet que l’on veux exfiltrer, car il est impossible de faire passer un stream comme URI.

Pour cela, j’ai utilisé le script suivant :

import zlib
import sys
import re
import binascii

stream_simple = rb"(\d+ \d) obj\n<<.*?>>\nstream\n(.*?)\nendstream"

data = b""
with open(sys.argv[1],"rb") as pdf:
    data = pdf.read()

    # encode streams to hex
    iterator = re.finditer(stream_simple, data, re.MULTILINE | re.DOTALL)
    for match in iterator:
        obj  = match.group(1)
        stream  = match.group(2)
        stream = binascii.hexlify(stream)
        print(len(stream))
        print(b"obj" + obj)
        print(stream)    

Une fois le tout assemblé, on envoi le PDF modifié et on récupère notre objet décodé ( vous trouverez un script pour générer automatiquement un pdf exploitant la vulnérabilité sur mon github : https://gist.github.com/Areizen/272cba5f295e2d44172a4936e860d0b0) :

Ce qui nous donne une fois l’URL encoding retiré :

 q 1 0 0 1 72 104.015 cm BT /F1 9.9626 Tf -43.654 36.739 Td[<002e0032001c0060>-333<002b0051004800480032001c003b006d0032002d>]TJ 0 -21.917 Td[<003e003200600032>-317<00420062>-317<0069003f0032>-318<007e001c003b>-317<0069003f001c0069>-318<0076>27<0051006d>-316<002b001c004d>-317<006d00620032>-318<00690051>-317<0032004d>27<006900320060>-317<0076>27<0051006d0060>-316<00620069006d002f0032004d>27<0069>-316<003b0060001c002f00320062>-318<0051004d>-317<0069003f0032>-318<006d004d00420070>27<00320060006200420069>28<0076>]TJ 0 -11.955 Td[<0072>27<003200230062004200690032002c>]TJ 0 -21.918 Td[<0036>27<002a0061002a002600520033006b0033001c006a00380032003300230052003300520052006b002b001c00370064001c004e007900650039004e002b006a0037002b002f006b002f00640039002f002f0079004e003700390027>]TJ 0 -21.918 Td[<0022003200620069>-333<00600032003b001c0060002f0062002d>]TJ 0 -21.918 Td[<0068003f0032>-333<005400600042004d002b00420054001c0048>]TJ 0 -11.955 Td[<005900790052006b006a00390038006500640033004e>]TJ ET Q

“Tiens ! ça ressemble pas trop à un flag … “

Effectivement, si on compile un PDF avec le Latex qui nous est donné, on verra qu’une police d’écriture est ajoutée et qu’une table d’association est créée.

10 0 obj
<</Length 398>>
stream
/CIDInit /ProcSet findresource begin
12 dict begin
begincmap
/CMapName /QNQQNX+LMRoman10-Regular-UTF16 def
/CMapType 2 def
/CIDSystemInfo <<
  /Registry (Adobe)
  /Ordering (UCS)
  /Supplement 0
>> def
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
4 beginbfchar
<002A> <0043>

    ...

<0061> <0053>
endbfchar
endcmap
CMapName currentdict /CMap defineresource pop
end
end

Il faut donc recroiser tous les charactères, mais une solution reste plus simple. Pour déchiffrer le flag, on reprends notre message.pdf et on remplace l’objet contenant le texte par le notre, comme celui-ci contient déjà tous les caractères présent dans le flag, il fera l’affichage et nous verrons apparaître notre précieux.

Flag : FCSC{1828a35e8b18112caf7a90649c3fcd2d74dd09f4}

Merci à @\J pour ce challenge qui m’a bien occupé.

Notes :

Ce flag m’a pris énormément de temps à récupérer car plusieurs erreurs étaient présentes dans mon setup local :

Références :