Au détour de mes recherches dans des extensions gratuites, ou lors des audits de code commandés par des clients, je trouve de temps en temps des choses si simples à corriger et pourtant si dévastatrices que j’avais envie de vous en montrer une, une belle.
Le Shortcode
Un shortcode – ou en français un “code court” mais je vais utiliser shortcode dans cet article – prend cette simple forme dans la rédaction d’un article depuis WordPress 2.5 (mars 2008) : [[shortcode param=""]]
ou [[shortcode]]content[[/shortcode]]
.
Le shortcode a cet avantage d’être simple d’usage, d’éviter de laisser un accès au code PHP, ou HTML, ou JavaScript pour utiliser une fonction avec des paramètres.
Prenons un exemple simple de shortcode qui est par défaut dans WordPress, le shortcode video : [video]
, démo :
[video src="https://www.youtube.com/watch?v=7gicRzTTC9Q"]
Avec cette façon de faire, seules les vidéos provenant des fournisseurs reconnus pourront être chargées, ou des vidéos locales.
Autre exemple avec [gallery]
qui affiche des medias de notre site : [[galleryids="5119257,5119131,5358857,16607"]]
, démo :
Ici un peu plus compliqué dans le coeur, mais pour un utilisateur il suffit de donner les IDs des médias pour que WordPress aille cherche ces articles de type media pour les afficher.
Sans shortcode, il faudrait alors créer une fonction PHP qui fait une boucle, affiche les images, etc, il faut donc du PHP et permettre que l’éditeur de texte deviennent aussi un éditeur de code, et ça c’est non.
Laisser la possibilité de créer du code PHP depuis l’éditeur de code est juste impensable !
Créer un Shortcode
On peut aussi créer très facilement un shortcode, c’est vraiment simple, en voici 2 exemples (spoiler alert, je mets des failles dedans, je le dis avant pour celles et ceux qui vont copier/coller pour les utiliser) :
Shortcode “titre d’article” (avec faille gratuite)
Avez-vous la première faille ? Non ? Ce n’est pas très méchant (à priori) mais on peut donc afficher le titre de n’importe quel post, privé, de tout type, en corbeille, etc. Il s’agit alors d’une simple “Information Disclosure“. Certaines extensions utilisent le post_title
pour stocker d’autres choses, par exemple un plugin de newsletter peut très bien créer ses abonnés en post_type “my_subscribers” et le post_title
est leur adresse email ! Je l’ai déjà vu donc oui ça existe… On va alors parler de “Private Data Disclosure” maintenant, la RGPD n’aime pas ça !
Ce qu’il manque ici c’est la vérification que ce type d’article est prévu pour diffuser publiquement son contenu. Voici donc le fix :
On vérifie maintenant que le post en question est public et visible, donc son type et status avec la fonction WordPress dédié is_post_publicly_viewable()
.
Une autre faille ? Oui, il y en a bien 2 autres, des failles XSS. Il suffit de tapper ça dans l’éditeur de l’article :
[[title id="Hello there"]]
et s’affiche à l’écran “Erreur Hello there incorrect.” On ne peut tout de même pas mettre de balises “script” car cela reste l’éditeur qui sera filtré car je suis Contributeur, mais il faut tout de même afficher ce genre de contenu dans un esc_html()
! Toute donnée provenant d’un utilisateur doit être traitée comme malicieuse, la base.
La seconde c’est le fait de vouloir retirer les balises de gras pour encadrer le titre de gras. Le soucis est qu’on peut maintenant afficher des balises script… Il suffit de pointer vers l’article qu’on est en train de rédiger et d’y mettre en titre “<sc<b>ript>alert(123);</sc<b>ript>
“, maintenant je peux enregistrer mon brouillon, il n’y a pas de balise script valide à supprimer, et les balises <b>
sont autorisées. Mais lors du passage dans shortcode, les balises <b>
sont être supprimées, laissant alors la place à la concaténation du reste qui donnera finalement "<script>alert(123);</script>"
. XSS.
Shortcode “titre d’article 2” (avec faille gratuite)
Et si on avait besoin d’afficher le titre d’un élément d’une table maison, on ne peut alors pas utiliser “get_post()
” ou autre, il faut créer sa requête SQL soit même. Créons ce nouveau shortcode.
Haaa enfin de la sécurité ! On affiche pas l’ID, on utilise intval()
pour s’assurer que c’est un chiffre, on fait même un esc_html()
!
Et pourtant… il y a bien une faille en bonus et pas des moindres car on va ici parler de faille SQL Injection. Voyons comment on peut réussir à exploiter cela.
Mais d’abord si vous ne savez pas de quoi on parle merci de lire le site de l’OWASP. En gros, il s’agit de réussir à manipuler une requête en base de données afin d’en sortir/entrer/modifier/supprimer des données théoriquement non accessibles dans le scénario d’origine.
Nous partons du principe que je suis un simple Contributeur sur ce site, que n’a pas accès au code source du shortcode et ne peut donc pas en connaitre les failles potentielles. Je vais devoir tester et tenter, voici comment fait un Consultant en Sécurité Web (ou Chercheur en Sécurité ou Ethical Hacker aussi).
Voici déjà la base, son usage normal :
Ce qui donne en front :
Ok on a bien le titre demandé, l’article 1 de ma table maison.
Comment savoir si on a affaire à une faille SQLi ? On teste d’autres valeurs !
Je tente avec "-1"
puis avec "-1 or 1=1"
, si le titre s’affiche de nouveau pour mon 2è essai, j’aurais bien injecté du code SQL (le OR) dans la requête qui ne devait pourtant prendre que les chiffres, non ?
Ça fonctionne ! Mais alors, comment juste ça a outrepassé les tests et vérifications ?
Relisez le code du shortcode, il n’y en a pas. Le intval()
ne fait que tester que ce qui est entré est bien un chiffre entier s’il est cast en tant que nombre et c’est le cas de 1 or 1=1
qui donne “1” tout comme 123hello
donne “123”. Mais c’est juste une condition ici, la valeur de $post_id
n’a pas été affectée, c’est l’erreur.
Je peux donc aller lire une autre table ou pas ? Et n’importe quel champ ou pas ? Oui et non, “ça dépend”.
Si on veut lire une autre table, il faut en connaitre le nom. Et vous allez me dire “mais on connait le nom des tables puisqu’on connait WordPress :D” oui mais et le préfixe ? Si c’est wp_
comme c’est par défaut, ok, mais si ça a été changé ?
(Rassurez-moi, vous l’avez changé ? Sinon faites le avant de lire la suite, je ne suis pas responsable de la prochaine crise cardiaque que vous allez avoir en continuant la lecture de l’article avec un préfixe wp_
… SecuPress Pro peut vous le faire !)
Comment dans une requête – dont nous n’avons pas connaissance du contenu – réussir à aller lire une seconde table dans cette même requête ? Avec le mot clé UNION
! Cependant, il faut bien entendu que les 2 requêtes aient le même nombre de champs dans leur sortie, sinon, erreur… Commençons par trouver combien de champs sont dans la table d’origine en joutant avec le SELECT
dans notre shortcode vulnérable :
Je fais un SELECT
dans aucune table mais je choisis directement la valeur de mon champ, oui on peut faire ça. Je l’ai fait autant de fois qu’il fallait jusqu’à ce que le résultat en front change et m’affiche :
"f"
! Donc la table dans laquelle la requête est faite contient 6 colonnes si c’est un “SELECT *
” ou simplement la requête fait un SELECT
de 6 champs, même résultat, on sait donc que je peux afficher ici ce que je veux dans le 6è champ.
Alors on tente de lire la table wp_users
et le user_email
du compte ID=1
?
Voici la requête injectée dans mon shortcode et voici le résultat en front :
Déception ! Le préfixe de la table n’est pas wp_
! Ici s’arrêtent la majorité des piratages automatisés.
Mais… si votre utilisateur SQL n’est pas bien paramétré et que le pirate vous cible, alors il se peut qu’il puisse aller lire le préfixe autrement !
Si notre utilisateur a les droits, il peut aller lire dans la table INFORMATION_SCHEMA
! Ce qui nous donne :
Tadaa, voici notre préfixe de la première table trouvée, 99% de chances (donnée arbitraire) que ça soit avec le préfixe de ce WP, au pire on décale la requête d’une table jusqu’à ce que. Voilà nous allons pouvoir recommencer à aller lire notre user_email
.
Qui donne enfin
“contact@secupress.me” le mail de l’administrateur 1 !
Ok, ce mail n’est pas forcément secret, il ne représente pas une grande avancée, mais je vous laisse imaginer ce qu’un pirate peut aller lire en base de données dans la tables des options (des tokens ? des licences ?), dans les posts metas (des données de ventes ? des emails ?), etc etc
Correction du shortcode
La correction est de nouveau très simple, ne pensez qu’à ça s’il vous plait : prepare.
J’y ajoute tout de même un vrai cast en integer pour notre post_id
mais si c’était du texte il faudrait de toutes façons faire le prepare !
Lisez la documentation complète de WordPress sur $wpdb
et lisez aussi celle de SQL !
Tout le monde compte sur vous.