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 :
- d’un header, qui spécifie la version de PDF utilisée
- d’un corps, où l’on va trouver le contenu du document
- d’une table de références indiquant les offsets des différents objets du PDF
- et d’un trailer contenant des informations globales au PDF (objet root, cryptographie utilisées, offset de la table des références, etc.)
Les objets présents dans le corps sont entourés de X Y obj
où X
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>]
là /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 :
/V
et/R
, la version et révision de l’algorithme/U
un hash duuser
password/O
, un hash duowner
password/P
un flag de permissions
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) :
- 5 premier bytes de la clé de fichier (dérivée du mot de passe user et du random ID du pdf )
- 3 LSB du numéro d’objet (
X
) - 2 LSB du numéro de génération (
Y
) - un salt
sALT
si l’algorithme utilisé est AES
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 :
- le random ID
- l’algorithme de chiffrement utilisé
- les hash owner et user des passwords
- l’index et le numéro de génération de l’objet à déchiffrer
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 :
- accés à une plateforme permettant d’envoyer un pdf (taille inférieure à 8 ko), qui sera ouvert par une personne possédant la clé de déchiffrement du flag
- un PDF chiffré contenant le flag
- un template Latex pour compiler un PDF
- un PDF en clair compilé à partir du template contenant un message
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 :
- l’ID du pdf
- les paramètres cryptographiques
- l’url du serveur d’exfiltration
- garder le même index et numéro de génération de l’objet que l’ont veut exfiltrer
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 :
- Ma version de
xelatex
ne prenait pas en compte la ligne\special{dvipdfmx:config z 0}
et mon PDF de test était compressé - Aucune mention n’est faite dans le papier de la KDF en fonction de l’index de l’objet et du numéro de génération
Références :
- PDF Insecurity : https://pdf-insecurity.org/
- Sources de MuPDF : http://git.ghostscript.com/?p=mupdf.git;a=summary
- Format de fichier PDF par Adobe : https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/PDF32000_2008.pdf
- How Secure is PDF Encryption : https://gendignoux.com/blog/2016/11/02/pdf-encryption.html
- Encryption explainations : https://www.cs.cmu.edu/~dst/Adobe/Gallery/anon21jul01-pdf-encryption.txt