Failles et vulnérabilités du Web

Anatomie d’un shortcode accompagné de ses failles

Blog Failles et vulnérabilités du Web Anatomie d’un shortcode accompagné de ses failles
0 commentaire

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)

<?php
add_shortcode( 'title', 'sc_post_title' );
function sc_post_title( $attr ) {
$atts = shortcode_atts( [ 'id' => 0, 'bold' => 0 ], $attr, 'date' );
$post_id = $atts['id'];
if ( ! $post_id ) {
return "Erreur $post_id incorrect.";
};
$title = get_the_title( $post_id );
if ( $atts['bold'] ) {
$title = '<strong>' . str_replace( [ '<b>', '</b>', '<strong>', '</strong>' ], '', $title ) . '</strong>';
}
return $title;
}
Shortcode post title

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 :

<?php
add_shortcode( 'title', 'sc_post_title' );
function sc_post_title( $attr ) {
$atts = shortcode_atts( [ 'id' => 0, 'bold' => 0 ], $attr, 'date' );
$post_id = $atts['id'];
if ( ! $post_id || ! is_post_publicly_viewable( $post_id ) ) {
return "Erreur $post_id incorrect.";
};
$title = get_the_title( $post_id );
if ( $atts['bold'] ) {
$title = '<strong>' . str_replace( [ '<b>', '</b>', '<strong>', '</strong>' ], '', $title ) . '</strong>';
}
return $title;
}
Shortcode post title corrigé

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.

add_shortcode( 'title', 'sc_post_title' );
function sc_post_title( $attr ) {
global $wpdb;
$atts = shortcode_atts( [ 'id' => 0 ], $attr, 'date' );
$post_id = $atts['id'];
if ( ! $post_id || ! intval( $post_id ) ) {
return "Erreur ID incorrect.";
};
$sql = "SELECT * FROM {$wpdb->posts} WHERE ID = $post_id LIMIT 1";
$req = $wpdb->get_row( $sql );
if ( empty( $req ) ) {
return "Aucun résultat.";
}
return esc_html( $req->post_title );
}
Shortcode custom title

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 :

Démo sql injection

Ce qui donne en front :

sqli demo 2

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 !

sqli demo 3

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 ?

sqli demo 4

Ç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 :

sqli demo 5

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 :

sqli demo 6

"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 ?

sqli demo 7

Voici la requête injectée dans mon shortcode et voici le résultat en front :

sqli demo 8

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 !

sqli demo 9

Si notre utilisateur a les droits, il peut aller lire dans la table INFORMATION_SCHEMA ! Ce qui nous donne :

sqli demo 10

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.

sqli demo 11

Qui donne enfin

sqli demo 12

“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 !

<?php
add_shortcode( 'title', 'sc_post_title' );
function sc_post_title( $attr ) {
global $wpdb;
$atts = shortcode_atts( [ 'id' => 0 ], $attr, 'date' );
$post_id = (int) $atts['id'];
if ( ! $post_id ) {
return "Erreur ID incorrect.";
};
$sql = "SELECT * FROM {$wpdb->prefix}custom_table WHERE ID = %d LIMIT 1";
$req = $wpdb->get_row( $wpdb->prepare( $sql, $post_id ) );
if ( empty( $req ) ) {
return "Aucun résultat.";
}
return esc_html( $req->custom_title );
}
Shortcode titre corrigé

Lisez la documentation complète de WordPress sur $wpdb et lisez aussi celle de SQL !

Tout le monde compte sur vous.

0 commentaire