21 mai 2025 Astuces Florent Montel

Pentest sécurisé : le référentiel Patrowl

Tout ce qui touche aux tests de sécurité d’une application web, qu’il s’agisse de scans de vulnérabilités, de fuzzing, de brute force contrôlé ou encore de tests d’intrusion automatisés est souvent perçu comme risqué. Et c’est compréhensible : personne n’a envie de voir son application planter à cause d’un test mal encadré.

Chez Patrowl, nous prenons cette préoccupation très au sérieux. C’est pourquoi nous avons mis en place un référentiel interne strict pour garantir que nos contrôles restent toujours sûrs. Chaque nouveau test est validé selon une règle simple : il ne doit jamais générer plus de trafic qu’un vieux smartphone, comme un Samsung Galaxy S6 Edge.

Pourquoi prendre ce modèle comme exemple ?

Parce qu’il représente un niveau de puissance et de trafic très basique comparé aux standards actuels. Il embarquait un processeur Exynos 7420 (8 cœurs, 2.1 GHz max), 3 Go de RAM. À l’époque, c’était du haut de gamme. Aujourd’hui, il est largement dépassé par n’importe quel téléphone d’entrée de gamme.

Autrement dit, si votre application tombe sous une charge équivalente à celle générée par un Galaxy S6 Edge, c’est qu’elle est en danger… tout le temps. Un enfant, un script kiddie ou une application météo mal conçue pourraient tout aussi bien la faire planter.

Pour simuler ainsi ce comportement, nous avons créé un script Python de h@x0r qui se contente de réaliser des requêtes en asynchrone vers une cible, et nous lançons ce script depuis une simple application Termux (même pas besoin d’être root sur le téléphone) depuis notre Samsung (script en annexe).

Tests de sécurité : les étapes de notre méthodologie

Pour réaliser nos tests, nous avons mis en place dans notre lab un panel de ce qui se fait de pire en termes d’hébergement de sites Internet. Pour simuler des hébergeurs peu scrupuleux, nous avons commencé par créer un template de machine AWS très simple qui se contente de créer une machine, avec les caractéristiques suivantes :

  • Une base de données MariaDB dernière version écoutant sur localhost

  • Un Service Apache2 qui écoute sur le port 80 (pas de HTTPs histoire de ne pas complexifier les choses)

  • Une instance Wordpress avec quelques plugins simples :

    • BackWPup

    • Contact Form 7

    • Google for WooComerce

    • WooComerce

    • WPCode Lite

      Ces plugins n’ont pas été choisis par hasard. Certains contiennent des faux positifs relevés par de nombreux scanners, d’autres peuvent entraîner de graves problématiques de sécurité s’ils sont mal configurés et/ou utilisés, etc.

    Cependant, il ne serait pas honnête de faire une comparaison publique de ce que peut trouver Patrowl versus les autres outils du marché sur cet échantillon. Il est en effet beaucoup trop facile de tromper les outils à notre guise pour ressortir des résultats qui nous conviendraient, et d’obtenir une analyse biaisée qui tromperait nos utilisateurs !

    Nous utilisons cependant ce template à des fins de formations internes, notamment pour montrer l’efficacité de notre produit.

Voilà notre site web prêt à l’emploi :

Il est à noter que nous n’avons changé aucune configuration des services installés. L’idée est vraiment de simuler ce qui se fait de pire en termes d’hébergement et de montrer les risques inhérents à exposer ce type de service directement sur Internet.

Puis, nous avons dupliqué ces configurations sur 5 machines types AWS :

  • 🎤 Micro → t2.micro : 1vCPU, 1Go RAM, 20Gb SSD Storage

  • 👕 Small → t2.small : 1vCPU, 2Go RAM, 20gb SSD Storage

  • 🥈 Medium → t2.medium : 2vCPU, 4Go RAM, 20gb SSD Storage

  • 💨 Large → t2.large : 2vCPU, 8Go RAM, 20Gb SSD Storage

Et nous avons lancé 4 jeux de scans bien distincts, permettant de couvrir et d’analyser les cas d’usage classiques :

  • 📱 Samsung Galaxy Edge 6 Script Kiddies → le script python async_flood.py (en annexe) lancé depuis le Samsung sur la target

  • 👾 Open-source scanners used by world-wide hackers (nuclei, feroxbuster, dirbuster) → Un nuclei/feroxbuster/dibuste lancé avec les configurations par défaut sur une machine Standard (un Mac 2020)

  • 🦉 Patrowl Offensive Scans → La batterie complète des scans offensifs de Patrowl lancés depuis notre plateforme iso-prod

  • 🦉 Patrowl Passives Scans → La batterie complète des scans passifs de Patrowl lancés depuis notre plateforme iso-prod

Zoom sur les résultats

Pour analyser le comportement des différentes machines, nous avons déployé un simple script de monitoring Python qui calcule les temps de réponse de chacun des services web, et les causes et durées de potentielles indisponibilités. Nous avons ainsi pu détecter 4 types de comportements sur les différents serveurs :

  • 💀 Massive crash : Le serveur crash instantanément et plus aucun service ne répond (même SSH). Une intervention sur AWS par l’administrateur est nécessaire avec un soft-reboot de la machine.

  • 🤕 Web Service Unreachable : Le service web n’arrive plus à gérer les requêtes qu’il reçoit durant le scan, mais le serveur est encore up. Le site devient donc inaccessible uniquement durant le temps du scan et revient à la normale une fois le scan terminé.

  • 🕠 Latencies : Le site web subit des latences importantes dans les temps de réponse durant le scan. Le site reste accessible mais difficilement.

  • Nothing happens : Aucune perturbation détectée durant le scan pour tous les utilisateurs.

Les résultats, soyons honnêtes, nous ont assez impressionnés.

Il est étonnamment facile de faire tomber un serveur mal configuré ou mal dimensionné : une simple ligne de commande suffit parfois à le faire vaciller complètement. Les cas de type 💀 Massive crash sont finalement bien plus fréquents qu’on ne le pense.

Comme attendu, les scans réalisés avec Patrowl se montrent nettement plus respectueux des applications qu’un simple script Python lancé depuis un vieux téléphone de 2015. La seule machine qui n’a pas résisté, c’est le 🎤 micro. Cela dit, même quelques F5 depuis un navigateur suffisent à faire vaciller ce service : un serveur aussi fragile n’a tout simplement rien à faire en ligne.


Pour le reste — small, medium ou large — on constate que Patrowl a un impact très limité sur les services testés, et ce malgré des configurations particulièrement modestes (on parle ici des machines d’entrée de gamme chez AWS). Ces machines vont beaucoup moins résister aux outils très classiques utilisés lors des tests d’intrusion ou de Bug Bounty, où les risques de crash seront alors bien plus importants.

La promesse d’être moins agressif qu’un Samsung Galaxy S6 Edge est donc bel et bien tenue !

Les hackers ne demanderont pas, et ils s'en moqueront

Bien sûr, le scan avec Patrowl vous permet de minimiser le risque de crashs potentiels pendant les scans contrôlés, mais soyez sûrs que les hackers du monde entier ne prendront pas soin de vos serveurs aussi soigneusement que le font les fournisseurs professionnels d'offensives.

Nous avons également analysé ce qui s'est passé sur notre serveur exposé sans nos scans. Nous maintenons une instance 🥈 Medium nettoyée pendant quelques jours pour voir ce qui se passe.

Ce site web est normalement inconnu du reste de l'internet : pas de référencement, pas de https/certificat (donc pas d'entrées crt.sh), il ne peut pas être détecté par les outils EASM classiques. C'est donc l'un des sites les moins détectables sur internet, et pourtant...

  • Nos micro et Small instance ont planté plusieurs fois pendant nos analyses mais pas à cause de nos scans (beaucoup de scans inconnus de fournisseurs connus et inconnus). Nous avons dû redémarrer l'AWS plusieurs fois.

  • Nous avons enregistré environ 100k requêtes web provenant de plus de 1500 IPs inconnues (AWS, Scaleway, Netiface ?, Contabo, Yandex)

  • Quelques commentaires inattendus dans un de nos articles provenant d'IPs inconnues (il s'agit d'un modèle de noyau intrusif bien connu, une fois de plus, ils n'ont pas demandé).

Voilà ce qui arrive à une interface à peine visible sur l'internet en moins d'une semaine. Imaginez maintenant ce que subissent chaque jour vos sites web les plus visibles.

Se préparer

Exposer des services web en ligne n'est pas une mince affaire. Vous devez vous préparer à ce qu'ils soient constamment sondés par l'ensemble de l'internet, comme nous l'avons démontré juste avant.

D'après notre point de vue et nos analyses, toute application web exposée devrait, au minimum, être capable de résister à un scan d'un logiciel libre de base tel que Nuclei, exécuté avec les paramètres par défaut à partir d'un poste de travail standard (un test qui est assez facile à réaliser, soit dit en passant).

Notre benchmark Patrowl Galaxy Edge 6 utilise des volumes beaucoup plus faibles qu'un scan Nuclei, mais nous pensons qu'il représente un cas d'utilisation solide pour tester si votre infrastructure peut supporter le type d'abus auquel elle est susceptible d'être confrontée sur Internet (Nuclei mal configuré lancé par un script-kiddies)

Bien sûr, dans la vie réelle des entreprises, de nombreux serveurs web exposés sont protégés par des Load-Balancer, Reverse-Proxy, WAF ou CDN qui sont bien sûr capables de gérer de simples Nuclei Scans (une instance F5 de base est par exemple capable de gérer 500k requêtes HTTP simultanées). C'est l'un des meilleurs moyens (et non le moins cher) de protéger votre site web.

Mais parfois, il n'est pas possible de protéger votre serveur avec ce type d'équipement ou de configuration coûteux, la plupart du temps parce qu'il est hébergé par un fournisseur tiers qui n'offre pas ce type de service, ou parce que vous l'hébergez pour vous-même et que vous ne voulez pas dépenser de l'argent dans de tels services.

Hébergement 101

La principale cause des pannes sur les sites web non protégés est une mauvaise gestion de la mémoire par les services web.

Lorsque vous installez des services web sur un serveur bon marché, chaque requête entrante génère souvent plusieurs processus ou threads. En fonction de la page consultée - comme dans notre installation WordPress - cela peut impliquer PHP et MariaDB. Sous charge, ces processus consomment rapidement la mémoire vive disponible, finissent par épuiser les ressources du système et provoquent le plantage de l'instance entière.

Pour protéger une configuration aussi médiocre, vous pouvez bien sûr augmenter la RAM disponible sur les instances VPS hôtes, mais comme nous pouvons le voir, même une AWS EC2 Large (8 Go de RAM) n'est pas en mesure de traiter correctement un scan Nuclei de base sans latences pour les autres utilisateurs. 8GB RAM est normalement plus que suffisant pour supporter un service web typique et devrait être capable de gérer un scan Nuclei, il suffit de le configurer correctement.

Pour comprendre le problème, il est important de savoir que les serveurs web traditionnels comme Apache ont été conçus à l'origine pour gérer efficacement un nombre modéré de connexions simultanées, mais pas le type de concurrence élevée exigée par le trafic moderne ou les scanners automatisés.

En revanche, les Reverse-Proxy comme Nginx sont construits autour d'une architecture événementielle, ce qui les rend très efficaces pour gérer des milliers de connexions simultanées avec une utilisation minimale des ressources, contrairement à Apache, qui génère un processus ou un thread par requête. Cela permet également de limiter le nombre de demandes avant qu'elles n'atteignent les services qui consomment beaucoup de mémoire vive.

Nginx fonctionne comme un tampon protecteur : il absorbe, gère et filtre le trafic web entrant - empêchant Apache, PHP et la base de données d'être submergés, même sur de petites configurations VPS.

Ensuite, nous avons configuré toutes nos instances derrière une configuration Nginx très simple, en ajoutant proxy_cache et un limit_conn très basique devant tous nos Wordpress. Cela évite qu'une seule IP (comme Nuclei) ne submerge le backend avec trop de connexions simultanées :

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=CACHE:10m max_size=100m inactive=5m use_temp_path=off;

server {
    listen 80;
    server_name medium-aws-website.piedpeper.com;
    location / {
        proxy_pass http://127.0.0.1:8080;  # Apache runs on port 8080
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        limit_conn conn_limit_per_ip 10;
 }
}

Nous effectuons ensuite exactement les mêmes contrôles que précédemment et, sans surprise, les résultats sont bien meilleurs en ce qui concerne la disponibilité de notre Wordpress pendant les analyses :

Sur une instance très bon marché, nous pourrions simplement ajuster la valeur limit_conn . Mais cela montre à quel point il est facile de protéger une infrastructure de base contre un scanner open-source moderne.

Maintenant vous savez

Bien sûr, l'écosystème entourant les applications web exposées est complexe, et simplifier leur protection globale à la seule installation d'un reverse proxy serait réducteur. Cependant, cet article démontre comment, avec des configurations de base et une expertise minimale, vous pouvez protéger efficacement une application des gros volumes de trafic générés par les scanners open-source modernes.

Pour nous, il s'agit de la base minimale nécessaire pour exposer en toute sécurité une application web en ligne.

Patrowl s'assure que tous ses scans respectent des limites de volume strictes, ce qui nous permet de fournir des tests de vulnérabilité efficaces avec un impact minimal sur votre infrastructure, en gardant vos applications sûres et résiliantes sous pression.

Annexes

Aller plus loin dans l'optimisation d'Apache
Quelques étapes ici si vous voulez aller plus loin dans vos ressources d'optimisation de la mémoire par un Apache utilisant un Wordpress.
Vous pouvez :

Voici un exemple de configuration très basique pour une petite instance VPN (apache2.conf ou httpd.conf) .

<IfModule mpm_event_module>
    StartServers             1
    MinSpareThreads         10
    MaxSpareThreads         25
    ThreadLimit             32
    ThreadsPerChild         10
    MaxRequestWorkers       50
    MaxConnectionsPerChild  500
</IfModule>
KeepAlive On
MaxKeepAliveRequests 50
KeepAliveTimeout 1
ServerLimit 1

async_flood.py

Script utilisé pour générer du trafic HTTP depuis notre Samsung Galaxy Edge 6 :

zerolte:/ # cat /sdcard/async_flood.py
import asyncio
import aiohttp
import time
import random
import sys

if len(sys.argv) != 2:
    print("Usage: python async_flood.py https://example.com")
    sys.exit(1)

BASE_URL = sys.argv[1].rstrip('/')
ENDPOINTS = ["/", "/about", "/contact", "/blog", "/products", "/api/data", "/login", "/search?q=test"]

REQUESTS_PER_SECOND = 300
DURATION_SECONDS = 20 

stats = {
    "success": 0,
    "errors": 0,
    "response_times": [],
}

async def send_request(session, i):
    url = BASE_URL + random.choice(ENDPOINTS)
    start = time.perf_counter()

    try:
        async with session.get(url) as response:
            await response.text()
            duration = time.perf_counter() - start
            stats["response_times"].append(duration)
            if response.status == 200:
                stats["success"] += 1
            else:
                stats["errors"] += 1
            print(f"[{i}] {url} → {response.status} ({duration:.3f}s)")
    except Exception as e:
        stats["errors"] += 1
        print(f"[{i}] {url} → ERROR : {e}")

async def main():
    async with aiohttp.ClientSession() as session:
        start_time = time.time()
        total_requests = REQUESTS_PER_SECOND * DURATION_SECONDS

        tasks = []
        for i in range(total_requests):
            elapsed = time.time() - start_time
            expected = i / REQUESTS_PER_SECOND
            delay = expected - elapsed
            if delay > 0:
                await asyncio.sleep(delay)

            task = asyncio.create_task(send_request(session, i + 1))
            tasks.append(task)

        await asyncio.gather(*tasks)

    print("\n=== CHECKED ===")
    total = stats["success"] + stats["errors"]
    avg_time = sum(stats["response_times"]) / len(stats["response_times"]) if stats["response_times"] else 0
    print(f"Total requests : {total}")
    print(f"Success         : {stats['success']}")
    print(f"Errors        : {stats['errors']}")
    print(f"Average Time   : {avg_time:.3f} s")

if __name__ == "__main__":
    asyncio.run(main()