취약점
https://nvd.nist.gov/vuln/detail/cve-2026-92263
해당 서비스는 피트니스 데이터를 기록하고 관리해주는 오픈소스이고 AI 기능을 제공하고 자신의 서버에서 설치해서 운영하는 셀프 호스팅 서비스임.
발견된 취약점은 OS Command Injection (CWE-78) to RCE 취약점이다.
백업 파일을 복원하는 기능에서 발생하였고 전제 조건은 인증된 전역 관리자 권한이 필요하다.
root cause
취약점과 관련된 코드 파일은 다음 2개의 파일이었음
- backupRoutes.ts → 사용자가 업로드한 백업 파일명 처리
- backupService.ts → 백업 파일을 압축 해제하면서 OS 명령어 실행
backupService.ts
해당 코드에서 패치 diff 를 보면 다음과 같은 줄이 나옴

1
await executeCommand(`tar -xzf ${backupFilePath} -C ${tempRestoreDir}`);
executeCommand 함수는 다음과 같이 내부적으로 exec() 함수를 사용해서 명령어를 실행하기 때문에 파일 경로에 셸 문자가 들어가면 명령어 실행이 가능함
1
2
3
4
5
async function executeCommand(command: any, options = {}) {
return new Promise((resolve, reject) => {
exec(command, /* ... */);
});
}
backupRoutes.ts
그럼 공격자 입력은 어디서 들어오는지 보면 다음 코드임

1
2
3
4
5
const originalFileName = req.file.originalname;
...
const finalBackupPath = path.join(BACKUP_DIR, originalFileName);
다음과 같이 입력된 파일명을 그대로 저장하고 해당 파일을 불러오기 기능에서 executeCommand 함수를 통해서 처리함 originalFileName 은 admin 이 백업 파일을 불러올 때 사용되는 값이므로 요청을 보내는 사용자가 제어할 수 있다.
PoC
따라서 다음과 같은 파일명을 가지고 있으면 임의 명령어 실행이 가능함
1
2
3
poc.tar.gz; id > /tmp/sparky_rce; #
-> tar -xzf /backups/poc.tar.gz; id > /tmp/sparky_rce; # -C /tmp/restore
위 명령 문자열이 셸에서 처리되면 tar 명령 뒤의 id 가 별도 명령으로 해석되므로 서버 프로세스 권한으로 임의 OS 명령 실행이 가능함.
패치
backupService.ts
다음과 같이 인자를 안전하게 전달하고 executeCommand 에서 실행되던 내부 함수도 execFile()로 변경함.
execFile() 함수는 기본적으로 셸을 사용하지 않고 실행 파일과 인자를 분리해서 전달하기 때문에 안전함.
1
2
3
4
5
6
await executeCommand('tar', [
'-xzf',
backupFilePath,
'-C',
tempRestoreDir
]);
또한 비슷하게 executeCommand() 를 사용하던 코드들도 모두 안전하게 인자 배열 방식으로 수정했음
backupRoutes.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { randomUUID } from 'crypto';
...
const uploadedFilePath = req.file.path;
// @ts-expect-error TS(2339): Property 'file' does not exist on type 'Request<{}... Remove this comment to see the full error message
const originalFileName = req.file.originalname;
// Never derive the on-disk path from the uploaded filename. A server-generated
// name keeps shell metacharacters and `..` traversal sequences out of the path
// entirely; tar reads the archive by content, so the name does not matter.
const safeFileName = `restore_${randomUUID()}.tar.gz`;
...
const finalBackupPath = path.join(BACKUP_DIR, safeFileName);
서버는 클라이언트가 제공한 원본 파일명을 저장 경로에 사용하지 않고 restore_<UUID>.tar.gz 형식의 서버 생성 파일명으로 교체한다. 따라서 셸 메타문자나 .. 같은 경로 조작 문자열이 들어가지 않음
시나리오
내가 찾은 취약점에서 나올 수 있는 시나리오는…. ㅠㅠ
악성 사용자가 조작된 백업 파일을 관리자한테 전달하고 관리자가 해당 파일을 복원하면서 RCE가 발생하는 시나리오
또는 관리자 권한을 이미 가지고 있는 공격자가 파일 만들고 복원하는 실행
내가 찾았지만 매우 이상한 시나리오 ;;