
Analyse technique approfondie et anti-POC pour CVE-2022-3602, un dépassement de tampon punycode dans OpenSSL 3.0.x, avec scripts de reproduction, analyse de la pile et évaluation des atténuations du compilateur.
Ce document et ce référentiel sont une analyse de CVE−2022-3602, un problème de débordement de tampon punycode dans OpenSSL. Il s'agit d'un « anti-POC » (le problème ne semble pas exploitable) destiné aux personnes qui maintiennent leurs propres builds d'OpenSSL et aux mainteneurs de compilateurs.
Il existe un autre CVE dans la même version, CVE-2022-3786, qui conduit également à des débordements de tampon, mais un attaquant ne peut pas contrôler le contenu dans ce cas. Il n'y a pas de reproduction ici pour ce problème, mais ce problème peut conduire à un déni de service en raison d'un plantage.
Les crashs et les débordements de tampon ne sont jamais bons et si vous utilisez OpenSSL 3.0.x, il est prudent de mettre à jour dès que possible.
N'hésitez pas à signaler toute erreur ou omission via les issues GitHub ou les pull-requests.
Il y a un problème de décalage d'un dans la façon dont ossl_punycode_decode gère le décodage punycode, ce qui entraîne un débordement de 4 octets. Ce problème n'est raisonnable que lorsqu'OpenSSL traite une chaîne de certificats et nécessite deux conditions. Premièrement, un certificat CA ou Intermédiaire dans une chaîne doit contenir un champ name-constraint qui utilise le punycode.
nameConstraints = permitted;email:xn-maccrthaigh-n7a.com
Deuxièmement, le certificat feuille doit contenir un champ SubjectAlternateName (SAN) otherName qui spécifie une chaîne SmtpUTF8Mailbox.
otherName = 1.3.6.1.5.5.7.8.9;UTF8:[email protected]
Lorsqu'il est déclenché, le punycode dans le champ nameConstraints, mais pas le punycode dans le champ otherName, sera traité par l'analyseur punycode vulnérable d'OpenSSL.
David Benjamin et Matt Caswell ont déterminé que la vérification des nameConstraints se produit après la validation normale de la chaîne de certificats et la vérification des signatures. Pour la plupart des applications, cela signifie que le problème ne peut pas être déclenché avec un certificat auto-signé ou une chaîne invalide.
Notez que les applications s_client et s_server d'openssl sont destinées au débogage et n'arrêtent pas le traitement lorsqu'une chaîne est invalide.
Une CA ou Intermédiaire de confiance devra contenir la charge utile malveillante et devra également avoir signé le certificat feuille qui déclenche le problème.
Il peut y avoir certains environnements où des parties non fiables sont les CA ou Intermédiaires, par exemple un service d'hébergement qui prend en charge les CA privées fournies par le client, mais cela n'est pas courant.
La réponse pour de nombreuses applications sera « non » en raison de la façon dont le compilateur a disposé la pile et de la présence d'autres protections telles que les canaris de pile / cookies de pile, le padding, le PIE, FORTIFY_SOURCE.
Le problème conduit effectivement à un débordement de 32 bits sur la pile. Cela ne suffit pas pour exécuter directement du code shell, mais cela peut suffire pour modifier le flux de contrôle d'une application. Par exemple, sauter vers du code shell intégré dans une chaîne de certificats X509 peut être possible si ces données sont également copiées sur la pile à un emplacement exécutable.
Sur toutes les plateformes Linux que j'ai testées, le débordement se produit dans le padding et est inoffensif. En théorie, un compilateur peut disposer les variables de sorte que le débordement se produise dans l'une des autres variables de la fonction ossl_a2ulabel.
En fonction de l'inlining, la liste complète des variables présentes est :
outptr, inptr, size, result, tmpptr, delta, seed, utfsize
et aucune ne me semble offrir un chemin évident vers une élévation de privilèges ou un contrôle intéressant.
J'ai joint une archive avec des outils qui peuvent être utilisés pour créer des reproductions et des débordements avec autant de contrôle sur les quatre octets que possible. La chaîne de reproduction de référence (xn--ww90271...aaaa) fait déborder les quatre octets avec les valeurs 0xFF 0x0F 0x0F 0x0F. Si cela ne fait pas planter une application, il est possible (probable ?) que cette application ne soit pas vulnérable.
Le script shell run-poc peut être utilisé pour générer une chaîne de certificats malveillante. Un certificat CA malveillant est généré à partir de ca.cnf et un certificat feuille déclencheur est généré à partir de leaf.cnf.
Le certificat CA utilise la charge utile de référence suivante :
xn--ww902716aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Un script Python peut être utilisé pour générer d'autres chaînes punycode pour différentes charges utiles.
Lors de l'exécution, run-poc lancera un client et un serveur openssl et tentera d'exploiter le problème dix fois.
Un OpenSSL vulnérable plantera probablement. Cela ne signifie pas que la version d'OpenSSL est vulnérable à une RCE, car les canaris de pile et les protections par cookie de pile provoquent également généralement un plantage (plus sûr) de l'application. Notez également que cela ne modifie en rien la gravité de l'autre CVE dans la même version.
Il est étonnamment nuancé d'obtenir un contrôle quasi complet des quatre octets de débordement et nécessite d'exploiter le décodeur punycode d'OpenSSL avec du punycode non standard / invalide. L'archive jointe contient un script qui peut construire une chaîne qui gère cette nuance. Ce qui suit est une explication de son fonctionnement.
Le problème de sécurité se trouve dans ossl_punycode_decode()
int ossl_punycode_decode(const char *pEncoded, const size_t enc_len,
unsigned int *pDecoded, unsigned int *pout_length)
ossl_punycode_decode est invoqué depuis ossl_a2ulabel. Le tampon pEncoded est un tampon de taille plus ou moins arbitraire provenant d'une chaîne de certificats X509. C'est la partie qui vient après tout "xn--" dans un champ nameConstraint. Voir la [reproduction] pour savoir comment reproduire une telle chaîne de certificats.
pDecoded est un tableau de unsigned int de taille LABEL_BUF_SIZE. LABEL_BUF_SIZE vaut 512, et sur la plupart des plateformes, un unsigned int fera 4 octets de large. Donc sur la plupart des plateformes, pDecoded fait 2048 octets de long.
À l'intérieur de ossl_punycode_decode(), le cœur du problème est cette vérification de longueur incorrecte :
if (written_out > max_out)
max_out correspond à *pout_length qui vaut toujours 512. Et written_out garde la trace du nombre de unsigned int écrits dans pDecoded. Parce que written_out est incrémenté plus tard seulement après l'écriture, cette vérification erronée permet d'écrire 513 unsigned int dans pDecoded. Le résultat final ressemble à ceci ...
pDecoded = [ ... , 'X , 'Y' , 'Z' ] 'P'
// Indices 509 510 511
Ici, selon la convention du C, les indices commencent à zéro, donc l'emplacement numéro 511 est le 512e élément du tableau. 'P' est une charge utile de quatre octets qui a été placée hors limites, au-delà de l'espace alloué sur la pile pour le tampon buf dans ossl_a2ulabel(), vers lequel pointe pDecoded.