Nicolas SURRIBAS

Sécurité Informatique / Capture The Flag / Développement / Réseaux / PenTest

One crazy month of web vulnerability scanning

Rédigé par devloop - -

Présentation

Le 10 décembre 2019 j'ai présenté à la conférence de sécurité Hack-IT-N les résultats de mes travaux qui ont consisté à scanner massivement le web à la recherche de vulnérabilités XSS et Open Redirect.

Cette conférence, 4ème édition lors de ma présentation, est organisée conjointement par l'école d'ingénieurs ENSEIRB MATMECA et par la société Tehtris qui développe une solution EDR (Endpoint Detection and Response) permettant de détecter des activités suspectes sur un système informatique.

Ayant uniquement réalisé des slides pour la présentation je pense qu'il était bon que je réalise aussi un article qui serve de whitepaper pour tous ceux qui n'ont pas eu la chance (ça c'est mon côté humble) d'assister à la conférence et aussi pour tous ceux qui se demandent pourquoi je scanne leur site Internet pour ensuite les inciter à corriger les failles de sécurité détectées :D

La présentation s'est faite en se basant sur les résultats d'un mois de scan d'où le titre du talk mais pour le présent article je prendrais en compte toute la période de scan de début septembre jusqu'au moment de ces lignes pour faire part de mon expérience (les statistiques resteront elles sur la période initiale).

Me, Myself and I

Pour ceux qui débarqueraient sur ce site sans trop me connaître, je suis passionné par la sécurité informatique depuis 20 ans maintenant, j'évolue dans le milieu à titre professionnel depuis 4 ans chez CybelAngel et je suis l'auteur du logiciel libre Wapiti, un scanner de vulnérabilités web écrit en python et débuté en 2006.

Wapiti

Ce logiciel libre, gratuit et open-source a débuté sous la forme d'un simple crawler et a rapidement évolué en un scanner de vulnérabilité assez minimaliste, à l'origine pour détecter des failles web bien connues (XSS, injection SQL, inclusion de fichier).

Depuis il s'est étoffé et de nombreux modules d'attaque sont venus se greffer avec dernièrement la détection des failles XXE et SSRF.

Il y a encore de nombreuses améliorations à apporter au logiciel malgré sont grand age (il est aussi âgé que sqlmap) mais j'y reviendrais plus tard.

Vulnérabilités XSS

Avant d'aller plus loin il est préférable de faire un rappel sur ce qu'est une faille XSS.

On dit qu'il y a une vulnérabilité XSS quand il est possible d'injecter sur un site Internet du code HTML (et par extension du code Javascript) qui sera rendu par le navigateur de la victime.

Cela implique des points très particuliers, premièrement son exploitation nécessite une action de la victime qui doit se rendre sur la page où le code sera affiché. Il faut donc potentiellement user d'ingénierie sociale pour inciter la victime à suivre un lien.

Ce sera en particulier le cas pour les attaques dites reflected (réfléchies ou non stockées) où le navigateur de la victime est involontairement à l'origine de l'injection.

A l'opposé, on parle de XSS permanent (ou stored donc stocké ou encore second order) quand le code injecté est stocké (de manière permanente dans une base de donnée ou temporairement dans une session) et affiché dans une page (potentiellement différente) de celle où l'injection a eu lieu.

Wapiti tente de détecter ces deux cas de XSS en effectuant un nouveau scan du site audité et en conservant en mémoire quel a été le point d'injection pour chaque payload généré.

L'approche de Wapiti n'est toutefois pas complète car il devrait refaire son scan à zéro au lieu de reprendre les pages connues étant donné que le code injecté se retrouvera potentiellement dans une URL qui n'existait pas initialement (exemple: ajout d'un nouveau produit sur un site de e-commerce ou nouvel article sur un blog).

C'est bien beau de parler de tout ça mais dans le fond, exploiter une faille XSS ça sert à quoi ?

L'exploitation la plus connue consiste à injecter un code Javascript qui va exfiltrer le cookie (identifiant de session) de l'utilisateur que l'attaquant pourra alors charger dans son navigateur afin accéder au site vulnérable comme s'il était connecté depuis le navigateur de la victime.

On peut très bien empêcher ce scénario avec l'instruction HttpOnly qui bloquera l'utilisation du cookie par le moteur javascript du navigateur.

Mais l'intérêt du XSS ne s'arrête pas là : son nom complet Cross-Site Scripting fait référence à la possibilité de passer outre un principe fondamental de la sécurité du web à savoir la same-origin policy.

La same-origin policy est une règle qui spécifie qu'un site A qui émettrait des requêtes vers un site B ne peut pas lire les réponses à ses requêtes.
Si sur un site A il y a des images de B chargées depuis la page web alors aucun problème, c'est le navigateur qui s'occupe de gérer les ressources et A ne reçoit jamais les données provenant de B.

Si le site A force l'envoi d'un formulaire à destination de B à l'aide de javascript (déclenchement de l'action submit ou utilisation de XHR) il n'a aucun moyen d'obtenir le contenu de la réponse de B.
C'est fondamental car sans la same-origin n'importe quel site pourrait fouiller vos informations privées sur les réseaux sociaux comme si vous y étiez connecté vous même.

Au passage l'envoi de requêtes à destination d'un site tiers, même sans obtention de la réponse, peut permettre certaines attaques que l'on qualifie de CSRF (Cross Site Request Forgery) mais c'est une autre histoire.

Pour revenir au XSS, un attaquant n'a donc pas nécessairement besoin d'exfiltrer le cookie, il peut profiter du fait que l'injection de code JS dans la page passe outre la same-origin policy (parce que c'est défini comme ça) et émettre une requête dont il pourra obtenir la réponse (à quoi bon passer par un cookie si on peut récupérer directement les informations sensibles et les exfiltrer en jouant avec le DOM).

La détection des XSS par Wapiti

Dans son fonctionnement global Wapiti fonctionne en trois phases : la découverte (scan du site Internet pour trouver URLs et formulaires), l'attaque (l'injection de payloads dans les paramètres des URLs et formulaires, suivi de l'analyse des réponses) et enfin la génération d'un rapport de vulnérabilités dans un format au choix (texte, HTML, JSON ou XML).

Le module d'attaque dédié aux XSS n'injecte pas immédiatement des payloads (charges) dans les paramètres.
Il se contente d'abord d'injecter une chaîne alphanumérique aléatoire pour voir si une réflexion a lieu (le fait que la chaîne soit aléatoire évite aussi des collisions et donc des faux positifs si le contenu est en réalité stocké).

Si cette chaîne est bien réfléchie alors pas si vite ! Il faut d'abord déterminer quelle est sa position dans le DOM (la structure de la page). Ainsi dans la balise suivante :

<a href="home.html">Homepage</a>

On peut imaginer naturellement que l'injection se situe à la place de Homepage entre la balise ouvrante et la balise fermante mais elle pourrait tout aussi bien avoir lieu à la place de home.html en valeur de l'attribut href ou encore à la place de href lui même, voire carrément à la place du nom de la balise (a).

En fonction de cela, Wapiti va avoir du travail d'échappement à effectuer comme fermer un guillemet, fermer la balise, potentiellement fermer une balise parente (si la balise courante est fille d'une balise ne permettant pas l'exécution de javascript comme noscript).

Une fois qu'il a généré une liste de séquences d'échappement pour chaque occurrence de la réflexion (parce que le paramètre peut très bien apparaître à plusieurs reprises dans la page), il accole pour chaque séquence un payload et envoie la requête correspondante.
Si on retrouve bien le tag correspondant au payload (exemple simple: une balise script effectuant un alert() alors la vulnérabilité est très certainement avérée.

Pour info, les payloads XSS sont définis dans un simple fichier INI donc très simple à personnaliser. Voici deux définitions pour l'exemple :

; Look mah! No [double|simple]quotes!
[case_script_alert_regex]
payload = <ScRiPt>alert(/__XSS__/)</sCrIpT>
tag = script
attribute = full_string
value = alert(/__XSS__/)
case_sensitive = yes

; Look mah! No whitespace! No script tags!
[svg_onload_alert_regex]
payload = <SvG/oNloAd=alert(/__XSS__/)>
tag = svg
attribute = onload
value = alert(/__XSS__/)
case_sensitive = yes
La chaîne __XSS__ sera remplacée par la chaîne aléatoire au moment de l'injection.

Le but

Arrivé à ce stade de l'article les plus observateurs se disent, à juste titre, qu'ils ne connaissent toujours pas la raison de ce projet.

La raison est toute simple : améliorer Wapiti en corrigeant les bugs.

Je vais ouvrir une parenthèse car en tant que développeur c'est un sujet qui me passionne.

D'où viennent les bugs ? Bien sûr il y a l'erreur humaine toute simple, la faute de frappe, la fatigue, le manque d'attention ou de connaissance mais tout cela peut selon moi avoir une cause initiale qui est la complexité.

Je ne parle pas de complexité mathématique (la fameuse notation O) mais de complexité au sens large.
Quand, en tant que développeur, vous vous retrouvez face à du code spaghetti, peu aéré, des variables aux noms peu explicites, des boucles qui ne vous semblent pas nettes (sans pouvoir trop mettre le doigt dessus) alors votre cerveau perçoit de la complexité et se met en mode panique Houla galère en perspective et à votre tour vous risquez vous aussi de coder avec le cul et donc d'introduire de nouveaux bugs.

Je code avec le cul

Et si le code original était complexe, hormis le fait que le développeur a une faible hygiène de programmation (PEP8 kékessé?), c'est sans doute parce que le code tentait de résoudre un problème qui lui-même était complexe.

La solution aurait alors été de simplifier le problème initial (le découper en plusieurs problèmes de moindre complexité) pour faire un code moins bugué.

Le module XSS de Wapiti a eu de nombreuses réécritures mais par toutes les étapes nécessaires à son fonctionnement il reste à ce jour le plus complexe et donc celui qui nécessitait le plus de corrections de bugs.

Par bug je ne parle pas forcément de crash mais plutôt de cas de faux positifs, c'est à dire que Wapiti reporte une vulnérabilité XSS qui n'est en réalité pas présente.

Maintenant, comment trouver ces bugs ?
La base c'est bien sûr de tester toute fonctionnalité que l'on ajoute au logiciel.
On peut aussi être plus assidu et mettre en place des tests unitaires pour s'assurer du bon fonctionnement du code et empêcher les régressions.

Tout ça c'est bien mais on ne compte que sur soit même pour trouver les bugs. La solution est de permettre aux utilisateurs de soumettre les bugs via un bugtracker et ainsi découvrir des bugs à côté desquels on a pu passer.

Toutefois compter sur ces rapports de bugs est illusoire : l'immense majorité des utilisateurs d'un logiciel ne savent pas de qu'est un bugtracker et ne sauront pas forcément décrire convenablement les causes du bug.

On peut prendre les devant en soumettant automatiquement un rapport au bugtracker au moment où l'utilisateur rencontre le bug. C'est une solution utilisée par les grands éditeurs de logiciel (Microsoft, Ubuntu, Google, etc) et aussi Wapiti depuis des versions récentes.

Mais si on veut vraiment être proactif il faut détecter le bug avant l'utilisateur et non en même temps que lui.

Sachant que les bugs de Wapiti surviennent en scannant des sites Internet qui sont autant de cas particuliers il faut alors scanner le site internet avant l'utilisateur.

Et pour scanner le site avant l'utilisateur... il faut scanner tous les sites Internet :D

Pas con le mec

C'est cette réflexion amusante qui m'a donc amené à automatiser Wapiti pour effectuer des scans massifs.
Cette approche n'est toutefois pas applicable à tous les domaines. Ainsi si demain un constructeur automobile se mettait à provoquer des accidents sur l'autoroute en entrant dans des personnes choisies aléatoirement au prétexte d'améliorer la sécurité de tous... pas sûr que sa vision soit bien comprise.

Les précurseurs

Je suis potentiellement le premier à automatiser des scans de vulnérabilités dans une logique d'amélioration d'un logiciel libre mais pas le premier ni le dernier à la faire par amusement (beaucoup d'autres le font à des fins moins avouables aussi).

Ainsi en 2013 à la conférence ShmooCon, l'entreprise alors naissante Hyperion Gray a dévoilé son projet PunkSPIDER : un robot scanneur de vulnérabilités web utilisant différentes technologies Java pour fonctionner.

La principale différence entre leur projet et le mien (en dehors de la finalité) est le fait qu'ils avaient mis en place un moteur de recherche permettant de retrouver les vulnérabilités détectées sur tel ou tel site Internet.


N'ayant ni l'envie ni le besoin de mettre en place une telle solution je me suis toutefois tourné vers la plateforme OpenBugBounty pour indexer l'ensemble des vulnérabilités que j'ai pu trouver (sur cette période et après).

OpenBugBounty est une plateforme de divulgation responsable de vulnérabilités web mettant en relation les chasseurs de bugs avec les gestionnaires de site.

Le parcours d'une faille dans OpenBugBounty est le suivant :
  • Un chasseur de bugs rapporte la vulnérabilité un postant un PoC (typiquement une URL avec le payload XSS)
  • OpenBugBounty vérifie l'existence de la vulnérabilité
  • OpenBugBounty publie un rapport public mais restreint de la vulnérabilité mentionnant le site concerné, le découvreur de la faille, le type de faille et le date du rapport
  • Dans la foulée OpenBugBounty tente de contacter le gestionnaire du site par différents moyens (adresses emails classiques, compte Twitter, présence du security.txt, etc)

OpenBugBounty bu reporting process

S'ensuit une période de non divulgation de la vulnérabilité de 3 mois qui doit permettre au gestionnaire du site de corriger la faille (divulgation responsable veut aussi dire responsabiliser le gestionnaire du site vis à vis des données de ses clients / utilisateurs / visiteurs potentiels).

Au bout de 3 mois si la vulnérabilité n'est pas corrigée le rapport public est complété avec l'URL du PoC.
Example of OpenBugBounty disclosure timeline

Les objectifs

Trouver des failles et les faux positifs c'est bien. Mais on mise sur combien de failles exactement ?
Histoire de rajouter un peu de challenge j'ai regardé dans le classement des meilleurs chasseurs de bugs et leurs performances sur les 6 derniers mois (seule fenêtre de statistique accessible sur OBB).

Pour chacun j'en ait extrait la plus grande quantité de vulnérabilités reportées sur un mois.
On a par exemple :

  • Random_Robbie qui a rapporté 13730 vulnérabilités en février 2019
    Etant donné que presque toutes les vulnérabilités ont été reportées le même jour on peut imaginer qu'il les a accumulé pour les reporter en masse.
    Dans son profil il indique noir sur blanc faire du scan de masse et visiblement chercher des vulnérabilités déjà connues.
  • Renzi qui a rapporté 7373 vulnérabilités en juillet 2019
    J'ai eu l'occasion de discuter avec lui et il se sert de dorks pour trouver de potentiels sites vulnérables puis teste ensuite les vulnérabilités avec un scanner maison.
    Là encore l'utilisation de dorks laisse entendre la recherche de vulnérabilités connues.
  • calv1n qui a rapporté 6413 failles en mai 2019
  • login_denied avec 4582 failles en mars 2019
  • geeknik avec 2146 vulnérabilités en avril 2019
Dans l'ensemble si on regarde les rapports complets de ces chercheurs on peut clairement retrouver des patterns d'URLs qui se répètent donc clairement un scan de failles via dork ou via une liste de paths comme le ferait Nikto.

Mon approche est clairement différente puisque avec Wapiti je vais scanner des sites utilisant du code de source inconnu et par conséquent trouver des vulnérabilités qu'aucun scanner aura préalablement découvert. Je vais aussi certainement scanner beaucoup de sites sans trouver de vulnérabilités (sites sûrs ou potentiels faux négatifs qui ne sont pas le sujet de l'article).

Leur approche est toutefois efficace en terme de résultats car la possibilité qu'une URL résultante d'un dork soit réellement vulnérable est élevée.

L'architecture

Pour mettre en place mes scans et paralléliser efficacement des instantes de Wapiti j'ai eu recours à un duo bien connu : Python (pour Wapiti) et RabbitMQ.

RabbitMQ est un message broker mais pour simplifier je dirais ici qu'il s'agit d'un système de files FIFO (First In, First Out).

L'idée est que je pousse sur ces files des URLs de sites à scanner et les instantes de Wapiti se chargent de les traiter (scanner le site et générer un rapport de vulnérabilités).

Pourquoi utiliser RabbitMQ et pas le système de queues du module multiprocessing de Python ?
Premièrement ça fait moins de code de mon côté, je n'ai pas à me soucier de l’ordonnancement ou de l'utilisation de verrous et mécanismes de synchronisations.
Ensuite RabbitMQ permet une vrai scalabilité, je peux ajouter ou supprimer des instance de Wapiti à la volée sans problèmes (avec plus de ressources je pourrais largement multiplier par 10 la quantité de sites scannés).
Enfin le contenu de la queue est persisté si je stoppe ma machine.

C'est une techno vraiment intéressante, facile à installer, qui nécessite un plugin additionnel pour avoir une jolie interface web de monitoring et côté code on trouve rapidement des exemples qui correspondent aux besoins que l'on peut avoir. Tout bénéf :)

Inputs

Comment alimenter cette file RabbitMQ ? Où trouver les sites Internet à scanner ?
Pour cela je me suis penché sur deux solutions que je connaissais.

La première, CertStream, est un service qui publie un flux en temps réel de toutes les émissions et renouvellements de certificats cryptographiques.
En écoutant ce flux je peux obtenir le nom DNS mentionné dans le certificat et faire l'hypothèse que s'il y a un certificat alors il y a un port 443 (TLS) ouvert sur le nom de domaine correspondant.
Cette source permet donc d'obtenir des URLs aléatoirement à l'inverse de la seconde source que j'ai utilisé pour effectuer des scans ciblés.

CommonCrawl(.org) est en quelque sorte un équivalent d'Archive.org à la différence qu'il est plus orienté données brutes : au lieu de proposer une jolie interface pour fouiner dans l'historique d'un site Internet, il est possible de récupérer des archives compressées d'un site à un moment donné.
Une fois par mois, CommonCrawl publie un nouvel index qui est en fait une API permettant de questionner les données qu'il a pour un site donné.
Le projet CommonCrawl a accumulé des pétaoctets de données compressées, heureusement il n'est pas nécessaire de les télécharger pour obtenir des informations intéressantes : on peut simplement soumettre un domaine (exemple: google.com) à l'API qui retournera la liste de toutes les URLs qu'il connait pour le domaine ainsi que ses sous-domaines.

Là où ça devient carrément intéressant c'est que j'ai découvert que l'on peut aller encore plus loin dans les requêtes en spécifiant juste un suffixe (.gouv.fr, .gov, .edu, etc) ce qui permet de scanner par exemples un (petit) pays entier ou encore les sites gouvernementaux dudit pays.

A titre d'exemple j'ai extrait tous les domaines en .edu répertoriés par CommonCrawl pour les scanner ce qui représente environ 190000 domaines.

Le code

Vous trouverez ci-dessous le code qui écoute le flux CertStream et envoie les URLs vers la file RabbitMQ.
Le code est très simple mais s'est vite agrémenté de petits détails pour s'assurer qu'on ne rescanne pas un site plusieurs fois (vérification de l'existence d'un rapport généré par Wapiti) ou encore pour éviter les domaines protégés par CloudFlare (inutile de s’attarder sur des sites qui vont nous renvoyer sur un captcha), idem pour certains hébergeurs Cloud ou la ribambelle de sites correspondant en réalité à Blogspot.
#!/usr/bin/env python
from urllib.parse import urlparse
from pathlib import Path
import ipaddress
from socket import gethostbyname

import certstream
import pika

connection = pika.BlockingConnection(
    pika.ConnectionParameters(host="localhost"))
channel = connection.channel()

channel.queue_declare(
    queue="websites",
    durable=True
)

BASE = Path("/my/path/to/reports")
known_urls = set()

cloudflare_ranges = [
    "173.245.48.0/20",
    "103.21.244.0/22",
    "103.22.200.0/22",
    "103.31.4.0/22",
    "141.101.64.0/18",
    "108.162.192.0/18",
    "190.93.240.0/20",
    "188.114.96.0/20",
    "197.234.240.0/22",
    "198.41.128.0/17",
    "162.158.0.0/15",
    "104.16.0.0/12",
    "172.64.0.0/13",
    "131.0.72.0/22"
]

ghs_ranges = [
    "64.18.0.0/20",
    "64.233.160.0/19",
    "66.102.0.0/20",
    "66.249.80.0/20",
    "72.14.192.0/18",
    "74.125.0.0/16",
    "173.194.0.0/16",
    "207.126.144.0/20",
    "209.85.128.0/17",
    "216.239.32.0/19"
]


def was_scanned(url):
    if url in known_urls:
        return True

    parts = urlparse(url)
    if (BASE / "{}_{}.json".format(parts.scheme, parts.netloc)).exists():
        return True
    if (BASE / "positives" / "{}_{}.json".format(parts.scheme, parts.netloc)).exists():
        return True
    if (BASE / "negatives" / "{}_{}.json".format(parts.scheme, parts.netloc)).exists():
        return True
    if (BASE / "false_positives" / "{}_{}.json".format(parts.scheme, parts.netloc)).exists():
        return True

    return False


def must_be_skipped(hostname):
    try:
        ip = ipaddress.IPv4Address(gethostbyname(hostname))
    except Exception:
        return False

    for ip_range in cloudflare_ranges + ghs_ranges:
        if ip in ipaddress.IPv4Network(ip_range):
            return True
    return False


def scan_callback(message, context):
    if message["message_type"] == "heartbeat":
        return

    if message["message_type"] == "certificate_update":
        all_domains = message["data"]["leaf_cert"]["all_domains"]

        if all_domains:
            domain = all_domains[0]
            if domain.startswith(("cpanel.", "autodiscover.")):
                return

            if domain.endswith((".plex.direct", ".azure.com", ".cloudshell.dev", ".clickmeeting.com")):
                return

            if "google." in domain:
                return

            if domain.startswith("*."):
                domain = "www." + domain[2:]

            if must_be_skipped(domain):
                print("[!] Discarding because of CloudFlare/Google")
                return

            url = "https://{}/".format(domain)
            if was_scanned(url):
                return

            known_urls.add(url)

            channel.basic_publish(
                exchange='',
                routing_key="websites",
                body=url,
                properties=pika.BasicProperties(
                    delivery_mode=2,  # make message persistent
                )
            )
            print("[i] Sent \"{}\"".format(url))

            if domain.count(".") != 1:
                return

            # Also try with www + domain
            domain = "www." + domain
            url = "https://{}/".format(domain)
            if was_scanned(url):
                return

            known_urls.add(url)

            channel.basic_publish(
                exchange='',
                routing_key="websites",
                body=url,
                properties=pika.BasicProperties(
                    delivery_mode=2,  # make message persistent
                )
            )
            print("[i] Sent \"{}\"".format(url))


try:
    certstream.listen_for_events(scan_callback, url="wss://certstream.calidog.io/")
except KeyboardInterrupt:
    print("[i] Leaving...")
    connection.close()
Le code qui suit permet quand à lui de rechercher via CommonCrawl tous les sous-domaines existant correspondant à un suffixe ou domaine.
La liste générée est stockée dans un fichier texte et non envoyée directement à RabbitMQ pour des raisons de temps d'inactivité du script.

J'avais donc un autre script pour charger les URLs d'un fichier et les transmettre à RabbitMQ.
Une solution plus élégante sera laissée en exercice au lecteur (comprendre: j'ai eu la flemme mais vous apprendrez quelque chose).
import re
from urllib.parse import urlparse
import logging
import json
import sys

import requests
from requests.exceptions import RequestException
from bs4 import BeautifulSoup

CC_URL = "https://index.commoncrawl.org/CC-MAIN-2019-47-index?url={}&pageSize=2&output=json&matchType=domain&page={}&fl=url"

sess = requests.session()


def find_subdomains(domain):
    idx = 0
    domains = set()
    while True:
        url = CC_URL.format(domain, idx)
        try:
            response = sess.get(url, timeout=20)
            soup = BeautifulSoup(response.text, "lxml")
        except RequestException as exception:
            logging.exception(exception)
        else:
            for line in response.text.splitlines():
                try:
                    infos = json.loads(line.strip())
                except json.decoder.JSONDecodeError:
                    return domains
                else:
                    if "url" in infos:
                        domains.add(urlparse(infos["url"]).netloc)
        idx += 1


domain = sys.argv[1]
subdomains = find_subdomains(domain)

total = 0
with open("/tmp/{}_subdomains.txt".format(domain), "w") as fd:
    for domain in sorted(subdomains):
        if not domain.startswith("www.") and "www." + domain in subdomains:
            print("Skipped {} as we have www version".format(domain))
            continue

        url = "https://{}/".format(domain)
        print(url, file=fd)
        total += 1

print("Found {} subdomains".format(total))
Maintenant reste la partie centrale, c'est à dire le worker qui se chargera de piocher les URLs sur la file et de lancer les attaques de notre choix (XSS et Open Redirect dans mon cas).
Le code n'est pas très long car Wapiti peut s'utiliser facilement comme une librairie :
#!/usr/bin/env python
# http://www.rabbitmq.com/tutorials/tutorial-two-python.html
from urllib.parse import urlparse
import traceback
from pathlib import Path

import pika

from wapitiCore.main.wapiti import Wapiti

BASE = Path("/my/path/to/reports")


def was_scanned(url):
    if ".clickmeeting.com" in url:
        return True

    return False

    if "stream.laut.fm" in url or ".podster.fm" in url or ".web.tv" in url or ".hamazo.tv" in url or ".vhx.tv" in url:
        return True

    parts = urlparse(url)
    if (BASE / "{}_{}.json".format(parts.scheme, parts.netloc)).exists():
        return True
    if (BASE / "positives" / "{}_{}.json".format(parts.scheme, parts.netloc)).exists():
        return True
    if (BASE / "negatives" / "{}_{}.json".format(parts.scheme, parts.netloc)).exists():
        return True
    if (BASE / "false_positives" / "{}_{}.json".format(parts.scheme, parts.netloc)).exists():
        return True

    return False


def scan(url: str):
    wapiti = Wapiti(url)
    # wapiti.verbosity(2)
    wapiti.set_bug_reporting(False)
    # wapiti.set_proxy("socks://127.0.0.1:9050/")
    wapiti.set_timeout(20)
    wapiti.set_attack_options({"timeout": 20, "level": 1})
    wapiti.set_max_scan_time(2)
    wapiti.set_modules("xss,redirect")
    wapiti.set_report_generator_type("json")
    parts = urlparse(url)
    wapiti.set_output_file("/my/path/to/reports/{}_{}.json".format(parts.scheme, parts.netloc))
    wapiti.flush_session()
    wapiti.browse()
    wapiti.attack()


def callback(ch, method, properties, body):
    body = body.decode()
    if not was_scanned(body):
        print("[x] Scanning {}".format(body))
        try:
            scan(body)
        except Exception:
            message = traceback.format_exc()
            print("[x] Scanning {} failed".format(body))
            channel.basic_publish(
                exchange='',
                routing_key="error_websites",
                body="Error scanning {}:\n{}".format(body, message),
                properties=pika.BasicProperties(
                    delivery_mode=2,  # make message persistent
                )
            )
        else:
            print("[x] Scanning {} succeed".format(body))
    ch.basic_ack(delivery_tag=method.delivery_tag)


if __name__ == "__main__":
    connection = pika.BlockingConnection(
        pika.ConnectionParameters(host="localhost", heartbeat=0)
    )
    channel = connection.channel()

    channel.queue_declare(
        queue="websites",
        durable=True
    )

    channel.queue_declare(
        queue="error_websites",
        durable=True
    )

    channel.basic_qos(prefetch_count=1)
    channel.basic_consume("websites", callback)

    print("[*] Waiting for messages. To exit press CTRL+C")
    channel.start_consuming()
That excalated quickly

Là encore une attention a été apportée à ne pas rescanner un même site. La raison de ce doublon de vérification vient du fait qu'un domaine peu apparaître sur CertStream à des intervalles proches (sans doute du à une erreur puis rectification humaine) et que potentiellement un des workers aura traité le site entre temps.

J'évite aussi certains domaines de streaming qui risquent de bloquer Wapiti sur la lecture des données.
Dans le futur j'ajouterais sans doute une vérification dans Wapiti qui vérifiera lors d'un ping du site qu'on a pas affaire à un serveur de ce type (Icecast ou autre) qui retourne un type mime audio ou vidéo pour sa page d'index.

Parmi les points importants à noter ici il y a :
  • l'instanciation de la classe Wapiti qui prend en paramètre l'URL qui servira de référence au périmètre de scan (le périmètre de scan est folder ce qui signifie que l'on utilise toutes les URLs commençant par cette URL de référence)
  • la définition d'un temps maximum de 2 minutes accordé à la phase de scan (si le site est vulnérable c'est généralement suffisant pour trouver une page faillible)
  • la définition des modules d'attaque à utiliser. On peut aussi restreindre un module à une méthode HTTP (ex: xss:get pour éviter de soumettre des formulaires qui ont souvent beaucoup de paramètres et génèrent plus de bruit comme les formulaires de contacts)
  • le choix du format de rapport et son emplacement

La partie la moins fun

Evidemment pour vérifier si une faille détectée est avérée ou non (faux positif), il n'y a pas 36 façons de s'en assurer : on vérifie à la main.
Tous les rapports étant placés dans le même dossier j'ai d'abord créé un script qui écarte les rapports vides (aucune faille détectée) en les déplaçant vers un dossier negatives.
Pour les rapports restants je vérifie à la main (copie de l'URL et ouverture dans Firefox) et déplace le rapport soit dans un sous-dossier positives soit vers un sous-dossier false_positives.

Faux départ

False positives everywhere

Dès le lancement des workers j'ai remarqué un faux positifs dont je connaissais l'existence mais dont j'ignorais l'ampleur.
En effet j'ai péché par fainéantise pour les cas où l'injection a lieu entre les balises ouvrantes et fermantes d'un script me disant (bien évidemment à tort) que si on pouvait injecter une petite poignée de caractères ça suffirait à trouver un moyen de rendre un payload fonctionnel.

XSS false positive in script tag

Sauf qu'en réalité il faudrait au moins être en mesure d'injecter un quote (apostrophe) ou double-quote (guillemet) et que de toute façon à défaut d'avoir un réel parser Javascript à disposition c'est impossible de déterminer si notre bout de javascript sera bien exécuté (il suffit d'une condition dont on ignore l'état).

Le fix rapide et vraiment efficace a consisté à retirer ce cas particulier et à la place fermer proprement la balise script pour en ouvrir une autre entièrement sous contrôle.

Maintenant ça peut sembler surprenant qu'une donnée reçue en paramètre se retrouve au milieu d'un code Javascript mais en réalité c'est très fréquent dans le sens où Wordpress le fait et que à notre époque le web s'apparente à une armée de Wordpress (à la prochaine faille critique Wordpress on s'assistera à une cyber-apocalypse).

Performances des scans

Avec mes faibles moyens à disposition je pouvait faire tourner jusqu'à 12 workers sur chacune des deux machines que j'ai pu utiliser.
J'ai commencé réellement les scans le 8 septembre 2019 (le temps de corriger les derniers petits bugs) et j'ai stoppé le 30 septembre 2019 afin de générer les statistiques.

Au total j'ai scanné 410288 sites Internet soit 18649 sites par jour.

J'ai obtenu 2906 positifs pour 232 faux positifs soit 92% de failles avérées.
A noter que par faille j'entends ici une faille sélectionnée parmi toutes celles d'un rapport de vulnérabilité (OBB permet de reporter plusieurs failles pour un site mais il faut laisser 24 heures entre chacune donc je m'en suis tenu à une unique faille reportée par site).

Le taux de failles avérées grimpe à 95% si on retire les cas liés au bug mentionné plus tôt (injection au milieu de la balise script).

Par rapport à la quantité de sites scannés on peu en déduire que au minimum 0.7% des sites Internet du web sont vulnérables à des failles XSS ou Open Redirect.
Au minimum car ces statistiques ne tiennent pas compte des faux négatifs non traités par Wapiti. De plus le scan n'est que partiel (durée d'exploration limitée et le fait qu'on n'attaque que les URLs visibles).

Au final j'ai soumis sur ce mois 2126 rapports valides à OpenBugBounty. Je suis donc légèrement en dehors des chiffres espérés sauf que :
  • une partie de mes rapports est invalidé par OBB : vulnérabilité déjà reportée par une autre personne, erreur SQL dans la page vulnérable (OBB écarte les failles qui sont aussi des failles SQL), problème de reproduction de la vulnérabilité chez eux (ex: site n'acceptant que les IPs françaises)
  • je n'ai soumis qu'une faille par site alors que je pouvais en avoir une dizaine pour certains
  • le scan n'a pas été fait sur un mois complet
En prenant tout cela en compte je suis donc largement dans la fourchette de chiffres que je m'étais fixé.

En terme de remédiation seulement 18 sites ont été patchés sur septembre et ça montait à 32 à la date du 3 octobre.
Cette quantité n'est pas représentative car OBB n'est pas en mesure de vérifier en permanence chaque faille reportée (ce sont les robots de OBB qui sont passées par hasard dessus pour les vérifier).

Plus tard j'ai utilisé une meilleure technique de vérification : récupérer la liste des failles sur mon profil OBB et les vérifier à l'aide d'un browser headless. Si elles sont corrigées (après seconde validation manuelle toutefois) j'utilise un bouton Verify patch sur OBB pour déclencher la vérification de l'absence de vulnérabilité. Ainsi sur le mois de janvier 2020 j'ai 580 sites qui ont eu le statut patché :)

Wall of shame

Histoire de rigoler un peu j'ai fait un classement des pays les plus vulnérables en comptant la quantité de sites faillibles sur leur suffixe gouvernemental :
  • Allemagne (.bund.de) : 1
  • Algérie (.gov.dz) : 1
  • Belgique (.fgov.be) : 1
  • Chine (.gov.cn) : 7
  • Maroc (.gov.ma) : 17
  • Espagne (.gov.es) : 18
  • France (.gouv.fr) : 19
  • Italie (.gov.it) : 19
  • Inde (.gov.in) : 50
  • Argentine (.gov.ar) : 55
  • Brésil (.gov.br) : 298
Dans la vie un homme a souvent des choix à faire...

Votez Brésil !

Dans l'ensemble il en retourne que nos pays européens s'en sortent bien en matière de sécurité.

On peut aussi prendre en compte le temps de réponse vis à vis de la réception d'un rapport de vulnérabilité.
Ainsi alors que les CERT allemands et français sont des bons élèves en la matière c'est à l'opposé un désastre pour les CERT indiens, argentins et brésiliens.

Ces chiffres sont toutefois à prendre avec du recul car les suffixes brésiliens et argentins sont plus permissifs sur quelles entités peuvent les utiliser (municipalités, régions, etc) ce qui n'est pas notre cas en France.

Malheureusement ce côté trop permissif fait que l'on rencontre de vrais horreurs pour ces deux pays d’Amérique du sud.
J'ai croisé des failles SQL, des failles de directory traversal, failles d'inclusions et depuis (via une autre méthode de scan qui fera potentiellement l'objet d'un article) des identifiants exposés ou encore du code (oui oui) PHP digne d'un débutant où un simple grep permet de trouver immédiatement une faille d'exécution de commande (de quoi faire le bonheurs de script kiddies).

Le CERT argentin se tourne visiblement les pouces (en même temps vu la quantité de sites faillibles je peux comprendre leur absence de motivation) et ne m'a jamais contacté pour les rapports soumis sur OBB (même en ayant spécifié sur OBB leur adresse email comme adresse à contacter pour l'envoi du rapport).
Pour parvenir à les contacter il faut y aller de son adresse personnelle sans quoi il ne prennent pas la peine de contacter les auteurs de rapports de vulnérabilité. Une bien mauvaise pratique de mon point de vue.

Sur les mois suivants j'ai eu des contacts avec d'autres CERTs et je ne peux que saluer l'efficacité et le sérieux des CERTs taiwanais et australiens qui sortent décidément du lot.
Le conseil que je donnerais à ces pays c'est de connaître leurs ennemis (et aussi leurs amis, surtout s'il s'agit des U.S.A.) pour avoir un niveau de sécurité à la hauteur des attaques de leurs adversaires...

Réception

En me lançant dans ce projet j'avais deux peurs.
La première de recevoir des emails de personnes mécontentes que je scanne leur site, la seconde de me retrouver à expliquer en permanence ce qu'est une faille XSS et comment et pourquoi il faut la corriger (c'est maintenant fait avec cet article).

En règle générale les personnes qui prennent contact comprennent déjà l'importance et l'impact de la vulnérabilité (ou ils se renseignent un peu avant ce contact) du coup je n'ai pas spécialement d'explication à donner, juste une URL pour reproduire la vulnérabilité.

Deuxièmement les personnes se sont toutes montrées reconnaissantes et polies.

Je n'ai eu qu'une exception plus tard en dehors de cette période d'étude alors que je venais de finir de scanner le suffixe .ws (Samoa mais le suffixe est ouvert à tous).
La personne n'a même pas mentionné le site concerné mais ce suffixe est plein de commerces plus ou moins louches, je pense plutôt que la personne étant mécontente d'avoir eu une soudaine exposition sur ces affaires pas forcément légitimes.
Le page rank des sites sous ce suffixe montre clairement l'utilisation de techniques dites blackhat SEO pour amener le plus de visiteurs possibles sur leurs sites malgré le peu d'intérêt quand au contenu hébergé.

Le premier contact que j'ai eu suite à un rapport envoyé à OBB fut avec l'université japonaise de Gunma et plus précisément l'IMCR (Institute for Molecular and Cellular Regulation). Cette première expérience a été très positive.

Par la suite j'ai été en contact avec de nombreux CERT et SOC et à défaut d'avoir un contrôle sur chacun qui sites faillibles que j'ai pu leur remonter ils ont pu transférer les détails des vulnérabilités aux personnes en charge de chaque site.

Do no try this at home

Pour des raisons de performances la grande majorité des scans a été effectué depuis ma réelle adresse IP.
Certains scans plus ciblés ont été relayés à travers le réseau d'anonymisation Tor.
Toutefois certains réseaux se protègent de Tor et j'ai pu constater la présence du fameux Great Firewall of China quand j'ai commencé à scanner les sites du gouvernement chinois.

Scanner ainsi le web à la recherche de failles web depuis son IP est forcément à risque : le risque e se retrouver fissa dans des blocklists et de n'être plus autorisé à accéder à certains sites.

Le plus comique a été de ne plus être en mesure d’accéder au site de la conférence à laquelle j'allais exposer mes travaux, ce site étant protégé par le WAF Wordfence.

Website protected by Wordfence

Faux positif : le cas des redirections

Le principal cas de faux positif rencontré concernait les redirections.

HTTP/1.1 302 Found
Date: Wed, 06 Nov 2019 15:06:43 GMT
Server: Apache/2.2.3 (Red Hat)
X-Powered-By: PHP/5.3.3
Location: http://google.com/search?q="></iframe><script>alert('ww2tdrwl31')</script>&sitesearch=decisionlab.harvard.edu
Content-Length: 4397
Content-Type: text/html; charset=UTF-8

 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
--- snip ---
<iframe src="http://google.com/search?q="></iframe><script>alert('ww2tdrwl31')</script>&sitesearch=decisionlab.harvard.edu" height="400" width="940">
--- snip ---

En effet même si toutes les conditions semblent bonne pour que notre code Javascript soit exécuté (le content-type est HTML, le payload correctement reflété) il suffit que le site fasse une redirection (présence de l'entête Location) pour que le navigateur ignore totalement le contenu de la page et suive l'URL spécifiée dans l'entête.

Les cas où le serveur web retourne un contenu alors qu'il effectue une redirection sont en réalité assez courant et l'expérience m'a montré que c'est souvent lié à une mauvaise de pratique de sécurité qui peut se résumer à :
if (!$authenticated) {
  header('Location: login.php');
}

// Some admin controls here
En bref si l'utilisateur n'est pas authentifié on indique à son navigateur de suivre la redirection tout en lui envoyant pourtant le contenu qu'il n'est pas sensé voir.

A l'aide d'un proxy interceptant comme OWASP ZAP on peut très vite retirer l'entête de redirections des réponses et naviguer dans la zone comme si on était connecté en tant qu’administrateur.
La page de documentation PHP de la fonction header() donne pourtant un exemple bien propre où l'on peut voir un exit juste après l'envoi de l'entête.

Si vous auditez la sécurité d'un site Internet gardez toujours un œil sur la taille des réponses effectuant une redirection, il y a régulièrement des perles ;-)

Faux positif : le cas du mauvais content-type

Comme mentionné plus tôt, pour que notre payload javascript soit exécuté il faut que la page spécifie un type MIME l'autorisant.
Le module XSS de Wapiti vérifie que le content-type est valide avant de s'attaquer à une URL mais j'ai croisé certains cas où face à la réception de code Javascript dans ses paramètres un script modifie dynamiquement son content-type pour text/plain pour afficher le contenu invalide sans risque d'exécution.
HTTP/1.1 404 Not Found
Date: Wed, 06 Nov 2019 16:45:51 GMT
Server: Oracle-Application-Server-11g
X-Frame-Options: SAMEORIGIN
Content-Length: 3007
Content-Type: text/plain


Failed to execute target procedure ORA-20002: Javascript not allowed in a URL
ORA-06512: at "WTAILOR.TWBKLIST", line 220
ORA-06512: at line 31

  DAD name: B570
  PROCEDURE  : twbkwbis.P_GenMenu
  URL        : http://--- snip ---/
  PARAMETERS :
  ===========
  name:
   <ScRiPt>alert('wya66g1ywj')</sCrIpT>

Faux positif : Errare humanum est

Un autre cas de faux positif vient d'une erreur de programmation que j'ai faite. En effet parmi les payloads JS injecté certains ont des attributs dont la valeur était vérifiés partiellement au lieu d'exactement.
Ainsi dans l'exemple suivant on trouve bien la valeur attendue dans le paramètre src mais elle est précédée et suivie de données qui ne sont pas sous contrôle.

<script type="text/javascript"
id="bcs_js_snippet" src="https://ui.customsearch.ai/api/ux/render?customConfig=2120877801&market=&safeSearch=0&q=&quot;/&gt;&lt;script src=https://wapiti3.ovh/w49op972ouz.js&gt;&lt;/script&gt;">
</script>

Faux positif : Content-Security Policy

Sans trop de surprise on retrouve dans les cas de faux positifs la présence de l'entête Content Security Policy.
CSP est un mécanisme de sécurité permettant de définir une liste blanche des origines à partir desquelles on autorise l'exécution des scripts.
Le comportement souvent rencontré consiste à bloquer tous les scripts inline (présents directement dans le code source de la page) et à restreindre la liste de ceux chargés via l'attribut src des balises scripts.

Pour autant la présence de l'entête CSP n'est pas un faux positif en soit : si on trouve parmi la whitelist une ressource permettant de refléter du contenu on peu alors bypasser CSP.
Il existe par exemple une technique connue de bypass pour les cas où google.com est présent dans cette whitelist.

Pas de correctif à apporter à Wapiti mais peut être une amélioration consistant à indiquer la présence de CSP si une faille XSS est détectée.

Effets collatéraux

Wapiti a très longtemps utilisé le parser HTML lxml (avec le wrapper BeautifulSoup).

Le choix de lxml est historique puisque à l'époque du lancement de Wapiti le parser HTML de Python n'offrait que peu d'intérêt et lxml était une alternative performante et peu gourmande en ressources.

J'ai eu l'occasion de le comparer à un autre parser entièrement Python nommé html5lib (lxml se base sur du code C) qui bien que plus avancé montrait de réels problèmes de performances.

Depuis, le parseur HTML de Python a largement évolué et est une alternative qui s'est révélée efficace face à lxml comme les exemples suivant peuvent le montrer.

Cas #1 de la balise script fermante

Un navigateur n'a pas de méthode évoluée pour déterminer s'il est arrivé sur une balise script fermante où si cette balise fermante est en réalité le contenu d'une variable du code javascript : le comportement d'un navigateur sera de fermer à la première instance de </script> trouvée, quitte à se tromper.

Il y a toutefois des cas particuliers d'échappement permettant d’empêcher que le navigateur fasse une erreur.
>>> page = """<script>jQuery('#head_solr_search_input').val('</script\><script\>alert(/w9zwjdf99w/)</script\>')</script>"""

>>> BeautifulSoup(page, "lxml").find_all("script")
[<script>jQuery('#head_solr_search_input').val('</script>, <script>alert(/w9zwjdf99w/)</script>]

>>> BeautifulSoup(page, "html.parser").find_all("script")
[<script>jQuery('#head_solr_search_input').val('</script\><script\>alert(/w9zwjdf99w/)</script\>')</script>]

>>> BeautifulSoup(page, "html5lib").find_all("script")
[<script>jQuery('#head_solr_search_input').val('</script\><script\>alert(/w9zwjdf99w/)</script\>')</script>]
Ici on voit que le parser lxml considère que l'injection a fonctionné et qu'il y a désormais deux balises scripts dont une sous notre contrôle.
Ce n'est pourtant pas le cas à cause de la présence des antislashs et les deux autres parsers testés ne s'y sont pas trompés.

Cas de la balise ne permettant pas l'exécution de code

Il existe des balises HTML dans lesquelles tout code javascript ne sera pas exécuté. On peut mentionner title, textarea, noscript, iframe, etc.

Dans le cas qui nous intéresse l'injection avait lieu au milieu d'une balise title que Wapiti a correctement fermée dans son payload.

Malheureusement le site ciblé ici se protégeait en retirant notre balise fermante. Comme Wapiti vérifie juste la présence de la balise script pour valider la vulnérabilité il n'a pas vu que l'échappement de la balise title a échoué d'où le faux négatif.

Le comportement attendu pour un parser vraiment orienté HTML serait de considérer comme du texte tout ce qui se trouve dans la balise title. Seule la librairie html5lib a le comportement adéquat.
>>> page = """<title>&#8216;<script>alert('wge1jfc2ed')</script>&#8216; at Blogs @ censored</title>"""

>>> BeautifulSoup(page, "lxml")
<html><head><title>‘<script>alert('wge1jfc2ed')</script>‘ at Blogs @ censored</title></head></html>

>>> BeautifulSoup(page, "html.parser")
<title>‘<script>alert('wge1jfc2ed')</script>‘ at Blogs @ censored</title>

>>> BeautifulSoup(page, "html5lib")
<html><head><title>‘&lt;script&gt;alert('wge1jfc2ed')&lt;/script&gt;‘ at Blogs @ censored</title></head><body></body></html>

Cas #2 de la balise script fermante

Dans le cas qui suit on trouve dans commentaires HTML directement à l'intérieur de la balise script. Ceux-ci semblent avoir un fonctionnement similaire au CDATA du XML et permettent de s'affranchir de la présence d'antislashs pour empêcher la fermeture de la balise script.
<SCRIPT LANGUAGE="JavaScript"> <!-- 
function monthchange(sel) {
	var idx = sel.selectedIndex;
 	window.location='https://yolo.com/eframe.Hope?webcss=<script>alert('wz0ak37dge')</script>&imon='+sel[idx].value;}
function yearchange(sel) {
	var idx = sel.selectedIndex;
 	window.location='https://yolo.com/eframe.Hope?webcss=<script>alert('wz0ak37dge')</script>&imon='+sel[idx].value;}
// --></SCRIPT>

Une nouvelle fois seul html5lib est capable de délimiter correctement la vrai fermeture de balise.

Un faux négatif bien embêtant

Le module XSS de Wapiti a une liste de payloads définis dans un fichier .ini.
Parmi ces payloads l'un était placé en bonne position pourtant il ne ressortait jamais dans les vulnérabilités trouvées...

Après vérification il s'est avérée que lxml était tout simplement incapable de parser correctement de payload qui utilise un / comme séparateur entre la balise et son attribut au lieu d'un espace :
>>> page = """<html><head><meta type='yolo' /><body><a><SvG/oNloAd=alert(/__XSS__/)></body></html>"""  

>>> BeautifulSoup(page, "lxml")
<html><head><meta type="yolo"/></head><body><a><svg></svg></a></body></html>

>>> BeautifulSoup(page, "html5lib")
<html><head><meta type="yolo"/></head><body><a><svg onload="alert(/__XSS__/)"></svg></a></body></html>

>>> BeautifulSoup(page, "html.parser")
<html><head><meta type="yolo"/><body><a><svg onload="alert(/__XSS__/)"></svg></a></body></head></html>

Payloads les plus utilisés

Voici la liste des payloads qui sont le plus remontés dans les failles découvertes.
Le premier cas s'explique tout simplement par le fait que quand un contenu est mal protégé c'est tout simplement qu'il ne l'est pas du tout :
<ScRiPt>alert('__XSS__')</sCrIpT>
Le second cas correspond simplement aux webmasters pensant que le Javascript ne peut être appelé que via une balise script, oubliant la longue liste des événements que l'on peut spécifier sur les balises HTML :
<ImG src=z oNeRror=alert("__XSS__")> 
Le troisième cas a l'avantage de pouvoir fonctionner même si le payload est passé en majuscule ou capitalisé (première lettre de chaque mot en majuscule). Il passe aussi les cas où le mot <script> est bloqué mais pas sa version avec un espace après script.
<ScRiPt src=https://wapiti3.ovh/__XSS__z.js></ScRiPt>
Les deux cas suivants permettent de passer certains WAF (Web Application Firewall)
<details open ontoggle=confirm(/__XSS__/)>
<object data=javascript:alert(/__XSS__/)/>
Et enfin le petit dernier (ou plutôt le grand) passe à travers certaines vérifications par expressions régulières. Il n'est pas soumis aux changements de casse et a tendance aussi à se retrouver dans des messages d'erreur SQL :
<script>[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]][([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((![]+[])[+!+[]]+(![]+[])[!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]+(!![]+[])[+[]]+(![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]]+[+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]])()</script>

Vulnérabilités retrouvées ou découvertes

Mes scans n'étaient pas basés sur des dorks toutefois j'ai pu retrouver des vulnérabilités connues lors de mes scans. De quoi démontrer l'efficacité de Wapiti pour découvrir des vulnérabilités.

C'est difficile de faire une liste exhaustive des applications web faillibles que j'ai croisé mais j'ai notamment croisé :
  • Wordpress 1.2
  • WP-Solr (plugin wordpress)
  • Archon
  • htsearch (ht://Dig)
  • TWiki
  • SPIP (XSS et Open Redirect)
  • MajorCool (Majordomo)
  • Jenzabar
  • Shibboleth (Open Redirect lié à un défaut de configuration)
  • uPortal
  • Lon-Capa
  • WP Mimbo Pro (plugin wordpress)
  • DSpace (DuraSpace)
  • Koha
ainsi que des nombreux thèmes et plugins pour ces CMS bien connus (Wordpress, Drupal, Joomla)

J'ai certainement découvert des failles inconnues parmi les plugins Wordpress mais leur quantité est telle que je n'ai pas eu le courage de chercher s'il existait des CVEs pour chacun.
Je suis toutefois sûr à 100% d'être le découvreur de certaines vulnérabilités car j'ai pu me retrouver en contact direct avec les auteurs du logiciel faillible.

C'est le cas par exemple du CMS Xoops que j'ai scanné par le plus grand des hasards :

XOOPS website

Email received from XOOPS

Idem pour PMB, logiciel libre scanné grâce à sa présence sur l'université Paris 8 :

PMB website

Email received from PMB mainteners

Futurs travaux

Voilà pour la présentation des mes recherches et leurs résultats.

Les améliorations à venir prochainement dans Wapiti consistent bien évidemment à corriger en priorité les différents cas de faux positifs rencontrés.

Je souhaite aussi effectuer une recherche similaire mais cette fois pour les faux négatifs.

D'autres idées d'améliorations pourraient venir dans un futur plus lointain comme :
  • étudier les injections dans les paths en plus des paramètres d'URLs
  • intégrer une fonctionnalité de proxy intercepteur
  • ajouter un module d'attaque SQL en aveugle basé sur des tests booléens (contrairement au module existant qui est basé sur les temporisations)

Classé dans : Non classé - Mots clés : aucun

Les commentaires sont fermés.