Recherche de site Web

Création d'un scanner de sécurité d'application Web simple avec Python : guide du débutant


Dans cet article, vous allez apprendre à créer un outil de sécurité de base qui peut être utile pour identifier les vulnérabilités courantes dans les applications Web.

J'ai deux objectifs ici. La première consiste à vous donner les compétences nécessaires pour développer des outils qui peuvent contribuer à améliorer la sécurité globale de vos sites Web. La seconde est de vous aider à pratiquer la programmation Python.

Dans ce guide, vous allez créer un scanner de sécurité basé sur Python capable de détecter les XSS, les injections SQL et les PII (informations personnellement identifiables) sensibles.

Types de vulnérabilités

Généralement, nous pouvons classer les vulnérabilités de sécurité Web dans les catégories suivantes (pour encore plus de catégories, consultez le Top 10 de l'OWASP) :

  • Injection SQL : technique permettant aux attaquants d'insérer du code SQL malveillant dans des requêtes SQL via des entrées non validées, leur permettant de modifier/lire le contenu de la base de données.

  • Scripting inter-sites (XSS) : une technique où les attaquants injectent un JavaScript malveillant dans des sites Web de confiance. Cela leur permet d'exécuter le code JavaScript dans le contexte du navigateur et de voler des informations sensibles ou d'effectuer des opérations non autorisées.

  • Exposition aux informations sensibles : un problème de sécurité où une application révèle involontairement des données sensibles comme les mots de passe, les clés d'API, etc. à travers des journaux, un stockage sans sécurité et d'autres vulnérabilités.

  • Médiction de sécurité commune : problèmes de sécurité qui se produisent en raison de la configuration incorrecte des serveurs Web - comme les informations d'identification par défaut pour les comptes d'administrateur, le mode de débogage activé, les tableaux de bord administrteurs accessibles au public avec des informations d'identification faibles, etc.

  • Faiblesses de base de l'authentification : problèmes de sécurité qui surviennent en raison de lacunes dans les politiques de mot de passe, les processus d'authentification des utilisateurs, une gestion de session inappropriée, etc.

Table des matières

  • Condition préalable

  • Configuration de notre environnement de développement

  • Construire notre classe de scanner de base

  • Implémentation du robot d'exploration

  • Conception et implémentation des contrôles de sécurité

    • Vérification de la détection des injections SQL

    • Vérification XSS (script inter-sites)

    • Vérification sensible de l'exposition aux informations

  • Implémentation de la logique de numérisation principale

  • Extension du scanner de sécurité

  • Emballage

Conditions préalables

Pour suivre ce tutoriel, vous aurez besoin:

  • Python 3.x

  • Compréhension de base des protocoles HTTP

  • Compréhension de base des applications Web

  • Compréhension de base du fonctionnement des attaques XSS, d'injection SQL et de base

Configuration de notre environnement de développement

Installons nos dépendances requises avec la commande suivante:

pip install requests beautifulsoup4 urllib3 colorama

Nous utiliserons ces dépendances dans notre fichier de code:

# Required packages
import requests
from bs4 import BeautifulSoup
import urllib.parse
import colorama
import re
from concurrent.futures import ThreadPoolExecutor
import sys
from typing import List, Dict, Set

Construire notre classe Core Scanner

Une fois que vous avez les dépendances, il est temps d'écrire la classe de scanner de base.

Cette classe servira de classe principale qui gérera les fonctionnalités de numérisation de sécurité Web. Il suivra nos pages visitées et stockera également nos résultats.

Nous avons la fonction normalize_url que nous utiliserons pour nous assurer que vous ne réanalyserez pas les URL qui ont déjà été vues auparavant. Cette fonction supprimera essentiellement les paramètres HTTP GET de l'URL. Par exemple, https://example.com/page?id=1 deviendra https://example.com/page après l'avoir normalisé.

class WebSecurityScanner:
    def __init__(self, target_url: str, max_depth: int = 3):
        """
        Initialize the security scanner with a target URL and maximum crawl depth.

        Args:
            target_url: The base URL to scan
            max_depth: Maximum depth for crawling links (default: 3)
        """
        self.target_url = target_url
        self.max_depth = max_depth
        self.visited_urls: Set[str] = set()
        self.vulnerabilities: List[Dict] = []
        self.session = requests.Session()

        # Initialize colorama for cross-platform colored output
        colorama.init()

    def normalize_url(self, url: str) -> str:
        """Normalize the URL to prevent duplicate checks"""
        parsed = urllib.parse.urlparse(url)
        return f"{parsed.scheme}://{parsed.netloc}{parsed.path}"

Implémentation du robot d'exploration

La première étape de notre scanner consiste à implémenter un robot d'exploration Web qui découvrira les pages et les URL d'une application cible donnée. Assurez-vous d'écrire ces fonctions dans notre classe WebSecurityScanner.

def crawl(self, url: str, depth: int = 0) -> None:
    """
    Crawl the website to discover pages and endpoints.

    Args:
        url: Current URL to crawl
        depth: Current depth in the crawl tree
    """
    if depth > self.max_depth or url in self.visited_urls:
        return

    try:
        self.visited_urls.add(url)
        response = self.session.get(url, verify=False)
        soup = BeautifulSoup(response.text, 'html.parser')

        # Find all links in the page
        links = soup.find_all('a', href=True)
        for link in links:
            next_url = urllib.parse.urljoin(url, link['href'])
            if next_url.startswith(self.target_url):
                self.crawl(next_url, depth + 1)

    except Exception as e:
        print(f"Error crawling {url}: {str(e)}")

Cette fonction crawl nous aide à effectuer une analyse approfondie d'un site Web. Il explorera toutes les pages d'un site Web tout en restant dans le domaine spécifié.

Par exemple, si vous envisagez d'utiliser ce scanner sur https://google.com, la fonction obtiendra d'abord toutes les URL, puis vérifiera une par une si elles appartiennent au domaine spécifié ( c'est-à-dire google.com). Si tel est le cas, il continuera à analyser de manière récursive l'URL vue jusqu'à une profondeur spécifiée qui est fournie avec le paramètre profondeur comme argument de la fonction. Nous avons également une gestion des exceptions pour nous assurer que nous traitons les erreurs en douceur et signalons toute erreur lors de l'exploration.

Conception et implémentation des contrôles de sécurité

Passons enfin à la partie juteuse et mettons en œuvre nos contrôles de sécurité. Nous allons commencer par l’injection SQL.

Vérification de la détection des injections SQL

def check_sql_injection(self, url: str) -> None:
    """Test for potential SQL injection vulnerabilities"""
    sql_payloads = ["'", "1' OR '1'='1", "' OR 1=1--", "' UNION SELECT NULL--"]

    for payload in sql_payloads:
        try:
            # Test GET parameters
            parsed = urllib.parse.urlparse(url)
            params = urllib.parse.parse_qs(parsed.query)

            for param in params:
                test_url = url.replace(f"{param}={params[param][0]}", 
                                     f"{param}={payload}")
                response = self.session.get(test_url)

                # Look for SQL error messages
                if any(error in response.text.lower() for error in 
                    ['sql', 'mysql', 'sqlite', 'postgresql', 'oracle']):
                    self.report_vulnerability({
                        'type': 'SQL Injection',
                        'url': url,
                        'parameter': param,
                        'payload': payload
                    })

        except Exception as e:
            print(f"Error testing SQL injection on {url}: {str(e)}")

Cette fonction effectue essentiellement des vérifications d'injection SQL de base en testant l'URL par rapport aux charges utiles d'injection SQL courantes et en recherchant les messages d'erreur pouvant faire allusion à une vulnérabilité de sécurité.

Sur la base du message d'erreur reçu après avoir effectué une simple demande de GET sur l'URL, nous vérifions si ce message est une erreur de base de données ou non. Si c'est le cas, nous utilisons la fonction report_vulnerabilité pour signaler cela en tant que problème de sécurité dans notre rapport final que ce script générera. Pour le bien de cet exemple, nous sélectionnons quelques charges utiles d'injection SQL couramment testées, mais vous pouvez étendre cela pour tester encore plus.

Vérification XSS (Cross-Site Scripting)

Maintenant, mettons en œuvre le deuxième chèque de sécurité pour les charges utiles XSS.

def check_xss(self, url: str) -> None:
    """Test for potential Cross-Site Scripting vulnerabilities"""
    xss_payloads = [
        "<script>alert('XSS')</script>",
        "<img src=x onerror=alert('XSS')>",
        "javascript:alert('XSS')"
    ]

    for payload in xss_payloads:
        try:
            # Test GET parameters
            parsed = urllib.parse.urlparse(url)
            params = urllib.parse.parse_qs(parsed.query)

            for param in params:
                test_url = url.replace(f"{param}={params[param][0]}", 
                                     f"{param}={urllib.parse.quote(payload)}")
                response = self.session.get(test_url)

                if payload in response.text:
                    self.report_vulnerability({
                        'type': 'Cross-Site Scripting (XSS)',
                        'url': url,
                        'parameter': param,
                        'payload': payload
                    })

        except Exception as e:
            print(f"Error testing XSS on {url}: {str(e)}")

Cette fonction, tout comme le testeur d'injection SQL, utilise un ensemble de charges utiles XSS communes et applique la même idée. Mais la principale différence ici est que nous recherchons notre charge utile injectée pour apparaître non modifiée dans notre réponse plutôt que de rechercher un message d'erreur.

Si vous parvenez à voir notre charge utile injectée, elle sera très probablement exécutée dans le contexte du navigateur de la victime comme une attaque XSS réfléchie.

Vérification de l'exposition aux informations sensibles

Maintenant, mettons en œuvre notre dernier chèque pour les PII sensibles.

def check_sensitive_info(self, url: str) -> None:
    """Check for exposed sensitive information"""
    sensitive_patterns = {
        'email': r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
        'phone': r'\b\d{3}[-.]?\d{3}[-.]?\d{4}\b',
        'ssn': r'\b\d{3}-\d{2}-\d{4}\b',
        'api_key': r'api[_-]?key[_-]?([\'"|`])([a-zA-Z0-9]{32,45})\1'
    }

    try:
        response = self.session.get(url)

        for info_type, pattern in sensitive_patterns.items():
            matches = re.finditer(pattern, response.text)
            for match in matches:
                self.report_vulnerability({
                    'type': 'Sensitive Information Exposure',
                    'url': url,
                    'info_type': info_type,
                    'pattern': pattern
                })

    except Exception as e:
        print(f"Error checking sensitive information on {url}: {str(e)}")

Cette fonction utilise un ensemble de modèles regex prédéfinis pour rechercher des e-mails PII, des numéros de téléphone, des SSN et des touches API (qui sont préfixées avec API-Key- ).

Tout comme les deux fonctions précédentes, nous utilisons le texte de réponse pour l'URL et nos modèles Regex pour trouver ces PII dans le texte de réponse. Si nous en trouvons, nous les signalons avec la fonction report_vulnerability. Assurez-vous que toutes ces fonctions sont définies dans la classe WebSecurityScanner.

Implémentation de la logique de numérisation principale

Faisons enfin tout assembler en définissant la fonction scan et report_vulnerabilité dans la classe webSecurityScanner :

def scan(self) -> List[Dict]:
    """
    Main scanning method that coordinates the security checks

    Returns:
        List of discovered vulnerabilities
    """
    print(f"\n{colorama.Fore.BLUE}Starting security scan of {self.target_url}{colorama.Style.RESET_ALL}\n")

    # First, crawl the website
    self.crawl(self.target_url)

    # Then run security checks on all discovered URLs
    with ThreadPoolExecutor(max_workers=5) as executor:
        for url in self.visited_urls:
            executor.submit(self.check_sql_injection, url)
            executor.submit(self.check_xss, url)
            executor.submit(self.check_sensitive_info, url)

    return self.vulnerabilities

def report_vulnerability(self, vulnerability: Dict) -> None:
    """Record and display found vulnerabilities"""
    self.vulnerabilities.append(vulnerability)
    print(f"{colorama.Fore.RED}[VULNERABILITY FOUND]{colorama.Style.RESET_ALL}")
    for key, value in vulnerability.items():
        print(f"{key}: {value}")
    print()

Ce code définit notre fonction scan qui invoquera essentiellement la fonction crawl et commencera à explorer le site Web de manière récursive. Avec le multithreading, nous appliquerons les trois contrôles de sécurité sur les URL visitées.

Nous avons également défini la fonction report_vulnerabilité qui imprimera efficacement notre vulnérabilité à la console et les stockera également dans notre tableau vulnérabilités .

Utilisons enfin notre scanner en l'enregistrant sous scanner.py :

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python scanner.py <target_url>")
        sys.exit(1)

    target_url = sys.argv[1]
    scanner = WebSecurityScanner(target_url)
    vulnerabilities = scanner.scan()

    # Print summary
    print(f"\n{colorama.Fore.GREEN}Scan Complete!{colorama.Style.RESET_ALL}")
    print(f"Total URLs scanned: {len(scanner.visited_urls)}")
    print(f"Vulnerabilities found: {len(vulnerabilities)}")

L'URL cible sera fournie comme argument système et nous obtiendrons le résumé des URL numérisées et des vulnérabilités trouvées à la fin de notre scan. Discutons maintenant de la façon dont vous pouvez étendre le scanner et ajouter plus de fonctionnalités.

Extension du scanner de sécurité

Voici quelques idées pour étendre ce scanner de sécurité de base à quelque chose de plus avancé:

  1. Ajoutez plus de vérifications de vulnérabilité comme la détection CSRF, la traversée du répertoire, etc.

  2. Améliorez les rapports avec une sortie HTML ou PDF.

  3. Ajoutez des options de configuration pour l'intensité de l'analyse et la portée de la recherche (en spécifiant la profondeur des analyses via un argument CLI).

  4. Implémentation de limitation de taux appropriée.

  5. Ajout de la prise en charge de l'authentification pour tester les URL qui nécessitent une authentification basée sur la session.

Conclusion

Vous savez maintenant comment créer un scanner de sécurité de base ! Ce scanner démontre quelques concepts de base de la sécurité Web.

Gardez à l'esprit que ce tutoriel ne doit être utilisé qu'à des fins éducatives. Il existe plusieurs applications de qualité d'entreprise conçues par des professionnels comme Burp Suite et OWASP ZAP qui peuvent vérifier des centaines de vulnérabilités de sécurité à une échelle beaucoup plus grande.

J'espère que vous avez appris les bases de la sécurité Web ainsi qu'un peu de programmation Python.