
CVE-2026-25895 FUXA 인증되지 않은 경로 탐색 -> 임의 파일 쓰기 -> RCE 를 위한 POC 익스플로잇
인증되지 않은, 사전 인증 임의 파일 쓰기: Node.js 기반 SCADA/HMI 플랫폼인 FUXA를 대상으로 합니다. 여러 가지 별개의 사후 쓰기 프리미티브를 통해 원격 코드 실행으로 이어집니다. 취약한 엔드포인트에 미들웨어가 연결되어 있지 않기 때문에 secureEnabled = true(인증 켜짐) 상태에서도 작동합니다.
ExploitDB에서도 확인 가능: https://www.exploit-db.com/exploits/52568
| 필드 | 값 |
|---|---|
| CVE ID | CVE-2026-25895 |
| 영향을 받는 버전 | FUXA <= 1.2.9 |
| 패치 버전 | FUXA 1.2.10 |
| 공급업체 | frangoteam / FUXA |
| 공격 벡터 | 네트워크 (HTTP/HTTPS) |
| 인증 | 필요 없음 |
| 영향 | 임의 파일 쓰기, 원격 코드 실행 |
| CVSS v3.1 | 9.8 (Critical) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 연구자 | Anthony Cihan (Hann1bl3L3ct3r) |
FUXA의 POST /api/upload 엔드포인트(server/api/projects/index.js:193)는 미들웨어 없이 등록되어 있어, 다른 모든 프로젝트 관리 엔드포인트에 적용되는 secureFnc JWT/API 키 확인 및 관리자 권한 게이트를 모두 우회합니다. 핸들러 내부에서 JSON 본문 필드 destination은 정규화나 포함 여부 검사 없이 선행 밑줄만 붙어 파일 시스템 경로에 연결됩니다.
let destinationDir = path.resolve(runtime.settings.appDir, `_${destination}`);
filePath = path.join(destinationDir, fullPath || fileName);
fs.writeFileSync(filePath, basedata, encoding);
a/../../../../../etc 형태의 destination 값(여기서 a는 선행 밑줄 접두사를 흡수)은 Node의 path.resolve가 appDir을 벗어나 FUXA 프로세스가 쓸 수 있는 모든 위치로 이동하게 만듭니다. fs.writeFileSync 앞에는 조건부 fs.mkdirSync(dir, { recursive: true })가 있으므로 공격자는 필요에 따라 상위 디렉터리도 생성할 수 있습니다.
그 결과 기본 HTTP 포트(1881)에 도달 가능한 인증되지 않은 임의 파일 쓰기 프리미티브가 생성되며, FUXA 관리자가 로그인을 활성화했는지 여부와 관계없이 사전 인증 상태에서 악용 가능합니다.
인증되지 않은 원격 공격자는 다음을 수행할 수 있습니다:
settings.js를 교체하여 다음 FUXA 재시작 시 코드 실행 달성root로 실행될 때(공급업체 Docker 이미지의 기본값) 60초 이내에 코드 실행을 위한 크론 작업(/etc/cron.d/<file>) 배치/root/.ssh/authorized_keys 또는 모든 사용자의 ~/.ssh/authorized_keys에 배치이는 산업 공정을 운영하는 데 사용되는 ICS/SCADA 플랫폼에 대한 사전 인증 중요도 심각도 취약점입니다.
| 버전 | 상태 |
|---|---|
<= 1.2.9 | 취약 |
1.2.10+ | 패치됨 |
Ubuntu Server에 FUXA 1.2.9를 깨끗하게 설치한 환경에서 악용 가능함을 확인했습니다.
단일 파일 Python 3 스크립트, 서드파티 종속성 1개(requests).
pip install requests
python3 fuxapwn.py --help
비정상적인 내용을 쓰지 않고 실행 중인 OS 사용자, Node-RED 노출 여부, 호스트의 다른 계정을 식별합니다:
python3 fuxapwn.py -u http://target:1881 --mode recon \
--probe-root --probe-home
중립적인 /tmp/healthcheck 마커 쓰기 (파일 이름이나 내용에 CVE 특정 IOC 없음):
python3 fuxapwn.py -u http://target:1881 --mode canary
FUXA가 root로 실행 중인 경우, 60초 이내에 실행되는 크론 파일을 배치하고 FUXA 재시작 불필요:
python3 fuxapwn.py -u http://target:1881 --mode cron \
--cron-cmd 'id > /tmp/fx.txt 2>&1'
settings.js 교체를 통한 지속적인 웹쉘FUXA Node 프로세스 내부에 HTTP 웹쉘 리스너를 설치하고(다음 콜드 스타트 시 활성화, require()가 모듈을 캐시하므로), FUXA가 재시작되면 대화형 REPL로 진입합니다:
# 페이로드 준비 — settings.js를 교체하지만 대상의 실제 설정(uiPort, allowedOrigins, secureEnabled 등)은 유지하여
# 애플리케이션이 정상적으로 계속 서비스되도록 합니다.
python3 fuxapwn.py -u http://target:1881 --mode webshell \
--appdata /opt/FUXA/server/_appdata --ws-port 31337
# FUXA가 재시작되면 설치된 웹쉘에 연결합니다.
python3 fuxapwn.py -u http://target:1881 --mode webshell-exec \
--ws-host target --ws-port 31337 \
--ws-path /_abc123 --ws-token <printed-above> --interact
각 모드별 전체 플래그 참조: python3 fuxapwn.py --help
GET /api/settings (server/api/index.js:103)는 미들웨어 없이 등록되어 있으며, 실시간 런타임 설정을 약간 수정된 상태로 반환합니다(서버는 전송 전에 secretCode와 smtp.password를 삭제). 나머지 필드는 일반적으로 서비스 사용자를 식별하는 절대 경로(appDir, workDir, userSettingsFile, logsDir, uploadFileDir)와 nodeRedEnabled를 유출하며, 이는 두 번째 인증되지 않은 RCE 경로(아래 참조)에 대한 직접적인 단서입니다.
FUXA가 npm start를 통해 로컬 사용자 계정으로 실행되고 설치 경로가 사용자를 인코딩하지 않는 경우(예: 설치가 /opt, /tmp 또는 일반적인 /app 아래에 있는 경우), 그리고 /root/에 쓸 수 없는 경우, POC는 0바이트 쓰기로 /home/<후보>/를 반복합니다.
FUXA 업로드 핸들러는 fs.writeFileSync 전에 조건부로 fs.mkdirSync(parent, { recursive: true })를 호출하여 EACCES 모호성을 생성합니다: 존재하지 않는 /home/<user>/는 mkdir 시도에서 EACCES로 실패하고(프로세스가 root 소유의 /home/ 아래에 디렉터리를 생성할 수 없음), 존재하는 /home/<other>/(모드 0700)는 쓰기 자체에서 EACCES로 실패합니다. 같은 errno, 다른 의미. POC는 서버가 전달한 err.message(libuv 형식 "<CODE>: <reason>, <syscall> '<path>'")에서 syscall 토큰을 구문 분석하고, mkdir 이외의 syscall에서 발생한 EACCES만 "다른 사용자가 존재함"으로 보고하여 순진한 errno 전용 프로브가 생성할 가양성 측면 이동 목록을 제거합니다.
유출된 설정에서 nodeRedEnabled = true인 경우, FUXA에 내장된 Node-RED 관리 엔드포인트(/nodered/flows/deploy 등)는 Referer 헤더 화이트리스트 검사(node-red/index.js:134-136) 때문에 인증되지 않은 상태로 접근 가능합니다. 이 검사는 Referer에 /editor, /viewer 또는 /lab이 포함된 모든 요청을 수락합니다. 이는 재시작 없이 함수 노드를 통해 즉각적인 인증되지 않은 RCE를 제공합니다. recon 모드가 이 조건을 플래그로 표시합니다; 운영자는 가능할 때 이 경로를 선호해야 합니다.
settings.js 페이로드는 대상 설정을 보존합니다settings.js 교체를 생성할 때(모드 settings-rce 및 webshell), POC는 먼저 /api/settings를 통해 라이브 설정을 가져와 JSON 직렬화하여 module.exports 본문으로 사용합니다(JSON은 유효한 JavaScript 객체 리터럴 하위 집합). 이렇게 하면 대상의 uiPort, allowedOrigins, secureEnabled 및 기타 런타임 설정이 보존되어 교체 후에도 애플리케이션이 정상적으로 계속 서비스됩니다. POC는 대상에 secureEnabled = true 또는 smtp 블록이 있는 경우 명시적으로 경고합니다. 서버 측 수정으로 secretCode(JWT 폴백이 작동함)와 smtp.password(수동으로 복원할 때까지 이메일이 중단됨)가 제거되기 때문입니다.
POST /api/upload, 특히 응답에 .. 또는 절대 파일 시스템 경로가 포함된 destination 값이 있는 경우GET /api/settings (일반적으로 인증된 UI만 사용)/tmp/healthcheck*, /tmp/.fuxa-probe-* 또는 /home/*/.fuxa-probe-*와 일치하는 파일 (기본 카나리 및 프로브 마커 파일 이름 — POC는 명백한 IOC를 피하기 위해 이를 재정의할 수 있으므로, 파일이 없다고 해서 악용이 배제되지는 않음)settings.js, /etc/cron.d/* 또는 ~/.ssh/authorized_keys의 수정된 타임스탬프/api/upload 엔드포인트는 표준 미들웨어 체인으로 보호됩니다./api/upload 및 /api/settings에 대한 인증되지 않은 요청을 차단하는 리버스 프록시를 배치하십시오./root/ 쓰기 불가, /etc/cron.d/ 쓰기 불가, 크론을 통한 호스트 전체 지속성 제거).nodeRedEnabled = false).1.2.101.2.9의 server/api/projects/index.js:193연구, PoC 및 문서 작성: Anthony Cihan (Hann1bl3L3ct3r), Offensive Security 리드.
이 저장소는 ICS/SCADA 제품의 중요 취약점에 대한 기능적 익스플로잇 코드를 포함합니다. 방어자(탐지 작성자, 사고 대응자) 및 공인된 보안 테스터의 이익을 위해 공급업체 패치 후 책임 있는 공개 원칙에 따라 게시됩니다.
소유한 시스템 또는 명시적이고 서면으로 된 테스트 허가를 받은 시스템에 대해서만 사용하십시오. 제3자 시스템에 대한 이 코드의 무단 사용은 대부분의 관할권에서 불법이며 작성자는 그렇게 간주합니다. 작성자는 오용에 대한 책임을 지지 않습니다.
FUXA 운영자로서 통제된 조건에서 이 PoC에 대한 패치 수준을 검증하는 데 도움이 필요하면 작성자에게 연락하십시오.
공인된 보안 테스트 및 방어 연구 목적으로 공개됩니다. 전체 조건은 LICENSE를 참조하십시오.
| 모드 | 목적 |
|---|
recon | GET /api/settings를 통한 인증되지 않은 정보 유출; 절대 경로에서 실행 중인 OS 사용자 추론; Node-RED 상태 보고; 선택적 --probe-root 및 --probe-home 능동 프로브 |
canary | 중립적인 기본 경로를 사용한 파일 쓰기 프리미티브 증명 |
settings-rce | settings.js를 교체하여 다음 FUXA 콜드 스타트 시 설정 가능한 명령을 실행하는 페이로드 배치 |
ssh-key | 대상 사용자의 authorized_keys에 공개 키 쓰기 |
drop | 임의의 절대 경로에 임의 파일 배치 |
cron | /etc/cron.d/<name>에 배치하여 FUXA 재시작을 기다리지 않고 60초 이내에 RCE 달성 (FUXA가 root로 실행 중이어야 함) |
webshell | settings.js 교체를 통해 FUXA Node 프로세스 내부에 HTTP 웹쉘 리스너 설치 (다음 콜드 스타트 시 활성화) |
webshell-exec | 이미 설치된 웹쉘용 클라이언트; 단일 명령 또는 --interact REPL |