Avant-propos
Dans le processus d'apprentissage de PHP, j'ai trouvé que certaines fonctionnalités de PHP sont difficiles à comprendre, comme comme dans la troncature PHP 00, les failles MD5, le contournement de la désérialisation __wakeup
et plus encore. Je ne veux pas m'en tenir à une compréhension superficielle, mais je veux explorer comment le noyau PHP le fait.
Ce qui suit est une vulnérabilité de désérialisation couramment utilisée dans CTF, CVE-2016-7124 (contournant la fonction magique __wakeup), comme exemple pour partager le processus de débogage du noyau PHP. Y compris toute la partie depuis la création de l'environnement de débogage du code source du noyau, l'analyse du code source du noyau de sérialisation et de désérialisation jusqu'à l'analyse finale de la vulnérabilité. (Recommandé : Tutoriel PHP)
1. Réflexions déclenchées par un exemple
Nous pouvons d'abord regarder le petit exemple que j'ai écrit.
Sur la base de l'image ci-dessus, introduisons d'abord les fonctions magiques en PHP :
Jetons d'abord un coup d'œil à la documentation officielle de plusieurs fonctions magiques couramment utilisées Introduction :
Pour résumer ici, __construct
sera appelé lorsqu'une classe est initialisée en tant qu'instance, et __destruct
sera appelé lorsqu'elle l'est détruit.
Lorsqu'une classe appelle serialize
pour la sérialisation, la fonction __sleep
est automatiquement appelée. Lorsqu'une chaîne doit être désérialisée dans une classe à l'aide de unserialize
, la fonction __wakeup
est appelée. . Les fonctions magiques ci-dessus seront appelées automatiquement si elles existent. Pas besoin de passer manuellement des appels sur écran vous-même.
Regardons maintenant la partie initiale du code. Dans la fonction __destruct
, il y a des opérations sensibles d'écriture de fichiers. Nous utilisons ici la désérialisation pour construire des chaînes dangereuses, ce qui peut entraîner des vulnérabilités d'exécution de code.
Lorsque nous avons construit la chaîne correspondante et nous sommes préparés à l'utiliser, nous avons constaté qu'il y avait une opération de filtrage dans sa fonction __wakeup
, ce qui a gêné notre construction. Car on sait que la désérialisation doit d'abord appeler la fonction __wakeup
.
Ici, nous ne pouvons nous empêcher de penser à utiliser cette vulnérabilité de désérialisation PHP CVE-2016-7124 (contournant la fonction magique __wakeup) pour contourner facilement la fonction magique ___wakeup qui est automatiquement appelée lors de la désérialisation Sensitive. les opérations sont écrites dans le fichier.
Bien sûr, le code ci-dessus n'est qu'un exemple simple que j'ai personnellement donné, et il existe de nombreuses situations similaires à celles ci-dessus dans des situations réelles. Mais cette méthode de contournement m'intrigue beaucoup. Comment le fonctionnement et le traitement internes de PHP affectent-ils la logique du code de la couche supérieure pour provoquer une telle situation magique (BUG). Ensuite, j'effectuerai une analyse de débogage dynamique du noyau PHP. Explorez cette question.
Cette vulnérabilité (CVE-2016-7124) affecte les versions des séries PHP5 antérieures à 5.6.25 et les séries 7.x antérieures à 7.0.10. Nous compilerons donc deux versions plus tard : l'une est la version 7.3.0 qui n'est pas affectée par cette vulnérabilité, et l'autre version est la version 5.6.10 où la vulnérabilité existe. Comparez les deux versions pour en savoir plus sur les différences.
2. Construction de l'environnement de débogage du code source PHP
Nous savons tous que PHP est développé en langage C, car l'environnement que j'utilise est WIN 10. Par conséquent, nous introduisons principalement la construction de l'environnement sous Windows. Nous avons besoin du matériel suivant :
PHP源码 PHP SDK工具包,用于构建PHP 调试所需要IDE
Le code source peut être téléchargé sur GITHUB, lien : https://github.com/php/php-src, vous pouvez sélectionner la version requise à télécharger.
Adresse de téléchargement de la boîte à outils PHPSDK : https://github.com/Microsoft/php-sdk-binary-tools La boîte à outils téléchargée à partir de cette adresse ne prend en charge que VC14 et VC15. Bien sûr, vous pouvez également trouver VC11, VC12, etc. qui prennent en charge les versions inférieures de PHP sur https://windows.php.net/downloads/ Avant d'utiliser le SDK PHP, vous devez vous assurer que VS est installé avec le correspondant. version du composant SDK Windows.
PHP7.3.0 et 5.6.10 seront utilisés dans l'article suivant. La compilation du code source de ces deux versions sera présentée ci-dessous. Les méthodes pour les autres versions sont similaires.
2.1 Compiler Windows PHP 7.3.0
Environnement natif WIN10 X64, le SDK PHP est téléchargé à partir du lien github ci-dessus. Entrez dans le répertoire SDK et recherchez 4 fichiers batch phpsdk-vc15-x64
ici.
Entrez ensuite phpsdk_buildtreephp7
dans ce shell, vous constaterez que le dossier php7 apparaît dans le même répertoire, et le répertoire du shell a également changé.
Ensuite, nous plaçons le code source décompressé sous php7vc15x64, shell dans ce dossier, et utilisons la commande phpsdk_deps–update–branchmaster
pour mettre à jour et télécharger les composants dépendants pertinents.
Après avoir attendu la fin, entrez dans le répertoire du code source et double-cliquez sur le fichier batch buildconf.bat
Il libérera les deux fichiers configure.bat
et configure.js
. cli– dans le shell. Enable-debug–enable-phar configure les options de compilation correspondantes. Si vous avez d'autres besoins, vous pouvez exécuter configure –help pour afficher
Selon. les invites, utilisez nmake pour compiler directement.
La compilation est terminée et le répertoire du fichier exécutable se trouve dans le dossier php7vc15x64php-srcx64Debug_TS. Nous pouvons entrer php -v pour afficher les informations pertinentes.
2.2 Compiler Windows PHP 5.6.10
La méthode est la même que celle de la version 7.3.0, notez simplement que PHP5.6 utilise La version du composant WindowsSDK est VC11, vous devez télécharger VS2012 et vous ne pouvez pas utiliser le SDK PHP téléchargé depuis github pour la compilation. Vous devez sélectionner le SDK PHP VC11 et les composants dépendants associés sur https://windows.php.net/. téléchargements/pour compilation Le reste C'est exactement la même chose que ci-dessus et ne sera pas répété ici.
2.3 Configuration du débogage
Parce que nous avons compilé l'interpréteur PHP ci-dessus, nous utilisons VSCODE directement pour le débogage ici.
Installez l'extension de débogage C/C++ une fois le téléchargement terminé.
Ensuite, ouvrez le répertoire du code source, cliquez sur Debug -> Open Configuration, le fichier launch.json s'ouvrira.
Selon l'image ci-dessus, après avoir configuré ces trois paramètres, vous pouvez écrire du code PHP en 1.php dans le répertoire courant et définir des points d'arrêt dans le code source PHP pour débogage direct.
L'environnement de débogage est mis en place.
3. Analyse du code source de la désérialisation PHP
De manière générale, en matière de désérialisation PHP, il existe généralement deux fonctions, sérialiser et désérialiser, qui apparaissent par paires, qui sont bien sûr pas nécessaire. Il existe également deux méthodes magiques __sleep() et __wakeup(). Comme nous le savons tous, la sérialisation signifie simplement que les objets sont stockés dans des fichiers, tandis que la désérialisation est tout le contraire. Les objets sont lus à partir du fichier et instanciés.
Ensuite, sur la base de l'environnement de débogage configuré ci-dessus, nous utilisons le débogage dynamique pour refléter intuitivement ce que la sérialisation et la désérialisation sont effectuées en PHP (version 7.3.0).
3.1 sérialiser l'analyse du code source
Écrivons d'abord une démo simple qui ne contient pas la __sleep
fonction magique :
Ensuite, nous recherchons globalement la fonction serialize
dans le code source et localisons cette fonction dans le fichier var.c. Nous plaçons un point d'arrêt directement sous l'en-tête de la fonction et commençons le débogage.
Nous pouvons voir qu'après avoir fait quelques travaux de préparation, nous commençons à entrer dans la fonction de traitement de sérialisation, et nous enchaînons avec la fonction php_var_serialize
.
Nous continuerons à suivre la fonction php_var_serialize_intern
ici. Ce qui suit est la fonction de traitement principale. Parce qu'il existe de nombreux codes de fonction, nous n'avons supprimé que les codes de fonction. éléments clés ici. La fonction est toujours dans le fichier var.c.
La structure de la fonction entière est un cas de commutation, et le type de la variante struc est analysé via la macro Z_TYPE_P (cette macro se développe en struc-> ;u1.v.type) , pour déterminer le type à sérialiser, puis entrez la partie CASE correspondante pour l'opération. La figure ci-dessous montre la définition du type.
D'après le chiffre 8 dans l'encadré rouge ci-dessus, on sait qu'il faut le sérialiser en un objet IS_OBJECT
et saisir la branche CASE correspondante :
Nous voyons le timing d'appel de la fonction magique __sleep
dans l'image ci-dessus Parce que cette fonction n'existe pas dans la démo que nous avons écrite, le processus n'entrera pas dans cette branche. Différentes branches représentent différents flux de traitement. Nous examinerons le processus avec la fonction magique __sleep plus tard.
Comme il n'y a aucun processus atteint dans la branche IS_OBJECT du cas ci-dessus, et qu'il n'y a pas d'instruction break dans le cas, l'exécution continue vers la branche IS_ARRAY, où le La classe est extraite du nom de la structure struc, calcule sa longueur et l'attribue à la structure buf, extrait la structure à sérialiser dans la classe et la stocke dans le tableau de hachage.
L'étape suivante consiste à utiliser la fonction php_var_serialize_intern
pour analyser de manière récursive l'intégralité du tableau de hachage, en extraire le nom et la valeur de la variable, effectuer une analyse du format, et terminez l'analyse. La chaîne est fusionnée dans la structure buf. Enfin, lorsque l'ensemble du processus est terminé, la chaîne entière est entièrement stockée dans la structure de tableau flexible buf.
Comme le montre le cadre rouge sur l'image ci-dessus, cela est cohérent avec le résultat final. Ensuite, nous modifions légèrement la démo et ajoutons la fonction magique __sleep
Selon la documentation officielle, la fonction __sleep
doit renvoyer un tableau. Nous avons également appelé une fonction membre de classe dans cette fonction. Observez son comportement spécifique.
Le processus précédent est exactement le même et ne sera pas répété ici. Commençons par le point de branchement.
On suit directement la fonction php_var_serialize_call_sleep
.
Continuons ici call_user_function
Selon la définition de la macro, elle appelle en fait la fonction _call_user_function_ex
et effectue quelques actions de copie ici, donc non. des captures d'écran sont prises et le processus passe à l'appel de la fonction zend_call_function
.
fonction zend_call_function
, dans des circonstances réelles, nous devons faire certaines de nos propres choses dans __sleep
, où PHP poussera les opérations à effectuer en PHP Dans votre propre pile de moteurs zend_vm
, il sera analysé un par un plus tard (c'est-à-dire que l'OPCODE correspondant sera analysé).
Le processus ici atteindra cette branche, nous suivons la fonction zend_execute_ex
.
Nous pouvons voir ici que dans ZEND_VM, le flux de traitement global est une boucle while(1), qui analyse en continu les opérations dans la pile ZEND_VM. Le moteur ZEND_VM dans la case rouge de l'image ci-dessus utilisera la méthode ZEND_FASTCALL
pour envoyer à la fonction de traitement correspondante.
Parce que nous avons appelé la fonction membre show dans __sleep
, show est positionné en premier, puis les opérations suivantes continuent à pousser dans la pile ZEND_VM pour le prochain cycle de nouvelle analyse (ici, les opérations du show sont traitées) jusqu'à ce que l'opération entière soit analysée. Nous ne reviendrons pas plus loin ici.
Vous souvenez-vous encore du paramètre sortant retval ci-dessus, qui est la valeur de retour de __sleep L'image ci-dessus montre le premier élément x du tableau renvoyé Bien sûr, vous. peut également vérifier directement dans la variable.
Après un si grand cercle, différents chemins mènent au même objectif. Après avoir traité une série d'opérations dans la fonction _sleep, la fonction php_var_serialize_class est utilisée pour sérialiser le nom de la classe et sérialiser récursivement la structure dans la valeur de retour de son _sleep. fonction. Enfin, les résultats sont stockés dans la structure buf. À ce stade, tout le processus de sérialisation est terminé.
3.1.1 Résumé du processus de sérialisation
Résumons le processus de sérialisation :
Quand il n'y a pas de fonction magique, sérialisez le nom de la classe –> ; Utilisez la récursivité pour sérialiser la structure restante
Lorsqu'il y a une fonction magique, appelez la fonction magique __sleep–>Utilisez le moteur ZEND_VM pour analyser les opérations PHP—>Renvoyer un tableau de structures qui doivent être sérialisées– >Nom de la classe de sérialisation -> Utiliser la sérialisation récursive de la structure de valeur de retour de __sleep.
3.2 Analyse du code source de désérialisation
Après avoir lu le processus de sérialisation, nous examinerons ensuite le processus unserialize
de la démo la plus simple. Cet exemple ne contient pas de fonctions magiques.
La méthode est la même que ci-dessus, unserialize
Le code source est également dans le fichier var.c.
L'image ci-dessus concerne les nouvelles fonctionnalités de PHP7, la désérialisation filtrée, selon allowed_classes
Définir la situation pour filtrer les objets PHP correspondants pour empêcher l’injection illégale de données. Les objets filtrés seront convertis en objets __PHP_Incomplete_Class对
et ne pourront pas être utilisés directement, mais cela n'a aucun impact sur le processus de désérialisation et ne sera pas abordé en détail ici. Nous enchaînons avec la fonction php_var_unserialize.
Nous continuons à suivre la fonction php_var_unserialize_internal
ici.
Le principal processus de fonctionnement interne de cette fonction consiste à analyser la chaîne, puis à passer au processus de traitement correspondant. La première lettre 0 est analysée dans la figure ci-dessus, qui représente la désérialisation en un objet.
Ici, le nom de l'objet sera d'abord analysé et une opération de recherche dans la table sera effectuée pour confirmer que l'objet existe.
Après avoir terminé les opérations ci-dessus, nous avons créé notre propre nouvel objet basé sur le nom de l'objet new et l'avons initialisé, mais notre opération de désérialisation a toujours échoué. Terminé, suivons avec la fonction object_common2
.
Ici, nous voyons le jugement et la détection des fonctions magiques, mais la partie appelante n'est pas là. Continuons à suivre la fonction process_nested_data.
Il semble que cette fonction utilise une boucle WHILE pour analyser imbriquéement les parties restantes. ·Elle contient deux fonctions php_var_unserialize_internal, le premier analysera le nom et le second analysera la valeur correspondant au nom. Une fois l'exécution de la fonction process_nested_data terminée, l'analyse de la chaîne est terminée, le contenu principal de l'opération de désérialisation est terminé et le processus est sur le point de se terminer.
Retour à la fonction d'origine PHP_FUNCTION
couche par couche On voit qu'un travail de finition est effectué, l'espace appliqué est libéré et la désérialisation est terminée. Notre fonction magique __wakeup
n'est pas appelée ici. Afin de connaître le timing d'appel de __wakeup
, nous modifions la Démo ici.
Une nouvelle série de débogage commence ici. Il a été constaté qu'une fois la sérialisation terminée, l'appel que nous nous attendions à voir est apparu dans l'espace de publication PHP_VAR_UNSERIALIZE_DESTROY
.
Vous souvenez-vous encore de l'indicateur VAR_WAKEUP_FLAG lorsque __wakeup est trouvé pendant le processus de désérialisation ? Ici, lors de la traversée du tableau bar_dtor_hash et de la rencontre de cet indicateur, l'appel à __wakeup est officiellement lancé. La méthode d'appel ultérieure est la même que la précédente. . La méthode d'appel __sleep introduite est exactement la même et ne sera pas répétée ici. À ce stade, tous les processus de désérialisation sont terminés.
3.2.1 Résumé du processus de sérialisation
Nous pouvons voir de ce qui précède que le processus de désérialisation ne dépend pas de l'apparition ou non de la fonction magique par rapport au processus de sérialisation .pour créer des différences dans le processus. Le processus de désérialisation est le suivant :
Obtenez la chaîne désérialisée –> Désérialisez en fonction du type –> Recherchez dans le tableau la classe de désérialisation correspondante –> –> new crée une nouvelle instance -> analyse la chaîne restante de manière itérative -> détermine s'il existe une fonction magique __wakeup et la marque ->
4. Vulnérabilité de désérialisation PHP
Avec la base de code source ci-dessus, explorons maintenant la vulnérabilité CVE-2016-7124 (contournement de la fonction magique __wakeup).
Par conséquent, la vulnérabilité a certaines exigences de version. Nous utilisons une autre version de PHP (5.6.10) compilée ci-dessus pour reproduire et déboguer cette vulnérabilité.
Nous reproduisons d'abord la vulnérabilité :
On voit ici que la classe TEST ne contient qu'un seul élément $a, et nous le déséquençons ici lors de la modification la valeur représentant le nombre d'éléments dans la chaîne d'éléments, cette vulnérabilité sera déclenchée. Cette classe évite l'appel de la fonction magique __wakeup
.
Bien sûr, un phénomène intéressant a également été découvert lors du processus de déclenchement de la vulnérabilité. Ce n'est pas la seule méthode de déclenchement.
Les opérations de désérialisation correspondant aux quatre charges utiles dans l'image ci-dessus déclencheront cette vulnérabilité. Bien que les quatre ci-dessous déclenchent des vulnérabilités, il existe quelques différences mineures. Ici, nous modifions légèrement le code :
Nous pouvons voir sur la figure ci-dessus que dans la chaîne désérialisée, tant que les éléments de la classe d'analyse apparaissent Cette vulnérabilité sera déclenché chaque fois qu’une erreur se produit. Cependant, la modification des opérations internes de l'élément de classe (telles que la modification de la longueur de la chaîne, du type de variable de classe, etc. dans la figure ci-dessus) entraînera l'échec de l'affectation de la variable du membre de classe. Ce n'est que lorsque le nombre de membres de la classe est modifié (plus grand que le nombre initial de membres) que le succès de l'affectation des membres de la classe peut être garanti.
Examinons le problème via le débogage :
D'après notre analyse du code source de la désérialisation dans la troisième partie, nous devinons qu'il peut se produire dans la version finale analysée problème variable. Ici on passe directement au débogueur pour un débogage dynamique :
On voit que par rapport au code source de la version 7.3.0, cette version n'a pas de paramètres de filtre et a été adopté. Avec autant de versions d'itérations, le processus de traitement des versions inférieures semble désormais relativement simple. Cependant, la logique harmonique globale n'a pas changé.Nous suivons ici directement la fonction php_var_unserialize. La même logique ne sera pas répétée ici.Nous suivrons directement la différence (fonction object_common2), qui est le code de traitement des variables membres dans la classe <.>
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!