
ProFTPD에서 CVE-2026-42167을 입증하기 위한 PoC
CVE-2026-42167에 대한 개념 증명(PoC) 데모입니다. 이는 ProFTPD의 mod_sql 로깅 파이프라인에 존재하는 SQL 인젝션 취약점으로, 인증되지 않은 공격자가 임의의 SQL을 실행하고 FTP 인증 데이터베이스에 백도어 사용자를 주입하거나 데이터베이스 호스트에서 원격 코드 실행을 달성할 수 있게 합니다.
모든 PoC는 PostgreSQL 전용이지만, 백도어 사용자 관련 PoC는 주입 쿼리를 약간 수정하면 MySQL 및 sqlite 백엔드에서도 동작합니다(수정 방법은 독자의 연습 문제로 남겨 둡니다).
is_escaped_text() 우회 (CVE-2026-42167, CWE-89)ProFTPD의 mod_sql은 SQLLog / SQLNamedQuery 메커니즘을 통해 모든 FTP 명령을 기록합니다. %U(원래 사용자 이름) 또는 %{basename}(파일명 구성 요소) 같은 형식 변수를 확인할 때 프레임워크는 이스케이프가 필요한지 결정하기 위해 is_escaped_text()를 호출합니다. 그리고 단일 따옴표로 시작하고 끝나며 내부에 단일 따옴표가 포함되지 않은 모든 값(예: '|| (SELECT 1) ||')에 대해서는 이스케이프를 완전히 건너<...>니다. 이 검사는 순전히 구문적이며 FTP 세션에서 공격자가 제어하는 원시 입력에 대해 실행되므로 "신뢰할 수 있는 코드에 의해 사전 이스케이프된 값"과 "사전 이스케이프된 것처럼 보이도록 공격자가 조작한 값"을 구분할 수 없습니다.
문서화된 표준 패턴은 SQL 안전을 위해 형식 변수를 단일 따옴표로 감쌉니다:
SQLNamedQuery log_activity INSERT "'%U', '%r', '%m'" activity_log
SQLLog * log_activity
SQLLog ERR_* log_activity
공격자가 '<payload>' 형태의 값을 제공하면 치환 결과 최종 SQL에는 ''<payload>''가 생성됩니다. 빈 문자열 리터럴이 주변 따옴표를 닫고 페이로드가 원시 SQL로 실행됩니다. PostgreSQL(PQexec) 또는 SQLite(sqlite3_exec)에서는 스택 쿼리(stacked query) 지원 덕분에 페이로드가 완전한 INSERT, UPDATE, CREATE TABLE 또는 COPY TO PROGRAM 문이 될 수 있습니다.
취약한 코드는 특정 백엔드가 아닌 contrib/mod_sql.c(공유 SQL 프레임워크)에 있으므로 모든 SQL 백엔드가 영향을 받습니다. 그러나 공격 표면은 관리자가 로깅을 어떻게 구성했는지에 따라 달라집니다. 서버는 다음 두 가지 조건이 모두 충족될 때 공격 가능합니다:
관리자가 SQLNamedQuery INSERT(또는 UPDATE)를 정의했는데, 해당 형식 문자열이 아래 공격자 제어 변수 중 하나를 단일 따옴표로 감싸 삽입하는 경우 — 예: "'%U', '%m'". 공격자 입력에서 비롯된 변수는 다음과 같습니다:
이 우회는 로깅 경로를 백엔드의 임의 SQL 프리미티브로 전환합니다. 영향력이 가장 큰 두 가지 시나리오는 다음과 같습니다:
임의 권한을 가진 백도어 사용자 주입(인증 우회).
ProFTPD의 SQL 인증 백엔드는 사용자 이름, 비밀번호 해시, uid, gid, 홈 디렉터리, 셸을 로깅 INSERT가 이제 쓸 수 있는 동일한 users 테이블에서 읽습니다. 스택된 INSERT INTO users는 공격자가 선택한 계정(uid=0, homedir=/, 평문 비밀번호)을 심고, 공격자는 이후 FTP 데몬을 통해 전체 파일 시스템 접근 권한을 갖고 정상적으로 로그인합니다. %U + SQLLog ERR_* 경로를 통한 인증 전(pre-auth) 또는 공격자가 발행할 수 있는 명령에 바인딩된 공격자 제어 변수(예: %{basename} + SQLLog STOR)를 통한 인증 후(post-auth) 도달 가능합니다. PostgreSQL과 SQLite에서 동작합니다(둘 다 스택 쿼리를 지원).
COPY TO PROGRAM을 통한 데이터베이스 호스트 원격 코드 실행.
PostgreSQL의 COPY (SELECT …) TO PROGRAM '<cmd>'는 데이터베이스 서버의 셸을 통해 <cmd>를 실행합니다. COPY TO PROGRAM을 발행하는 스택 쿼리 인젝션은 postgres OS 사용자 권한으로 임의 OS 명령 실행을 가능하게 하며, 이는 자격 증명 유출, 수평 이동, 지속성 확보에 충분합니다. 백도어 사례와 동일한 트리거 경로(인증 전 또는 인증 후 등)를 통해 도달 가능합니다. PostgreSQL 전용이며, ProFTPD DB 역할에 슈퍼유저 권한(의 전제 조건)이 필요합니다. 단일 테넌트 배포에서 역할이 DB 소유자인 경우 흔합니다.
이 저장소의 PoC는 특히 PostgreSQL을 대상으로 합니다. PQexec()가 스택 쿼리를 지원하고 COPY TO PROGRAM이 직접적인 OS 명령 실행을 제공하므로 가장 강력한 사례입니다. 우회 자체는 공유 SQL 프레임워크에 있으며 모든 백엔드에 영향을 미칩니다:
PQexec를 통한 스택 쿼리, COPY TO PROGRAM을 통한 RCE.sqlite3_exec를 통한 스택 쿼리, proftpd 워커에서 PRIVS_ROOT로 실행 — 백도어 주입은 동일하게 동작합니다. RCE 프리미티브는 다릅니다(COPY TO PROGRAM에 해당하는 기능은 없지만, 루트 프로세스 SQL 백엔드의 쓰기 가능한 users 테이블만으로 충분합니다).users 테이블 또는 OS 실행에 도달하는 것은 더 어렵습니다. 단일 문 조작(VALUES 슬롯에서의 서브쿼리 유출, 시간 기반 블라인드, 오류 기반 블라인드)은 간단합니다. 이를 다른 테이블에 대한 쓰기로 전환하려면 몇 가지 장애물을 우회해야 합니다:
mod_sql_mysql은 CLIENT_MULTI_STATEMENTS 없이 mysql_real_query()를 호출하므로 뒤에 오는 ; INSERT INTO users …는 연결에서 거부됩니다.users 테이블이나 디스크의 파일에 쓰기 위해 이 제한을 우회하는 방법을 알아내야 합니다.PostgreSQL 설정 내에서 PoC는 두 가지 대표적인 트리거 경로를 보여줍니다. 위 표의 다른 조합도 원칙적으로 동일합니다:
%U + USER를 통한 인증 전. SQLLog ERR_* 덕분에 완전히 인증되지 않은 상태에서 가능합니다.%{basename} + STOR를 통한 인증 후. 임의의 FTP 자격 증명이 필요하지만 파일 업로드 능력 이상의 권한은 필요하지 않습니다.setup/ — 자동 환경 설정. 이 발견 사항이 보고된 커밋에 고정된 ProFTPD 소스 트리를 클론하고, mod_sql + mod_sql_postgres로 데몬을 빌드하며, 시드 데이터와 취약한 SQLLog 구성을 포함한 Docker Compose 클러스터(ProFTPD + PostgreSQL)를 구성합니다.
pocs/ — 다섯 개의 익스플로잇 스크립트. 모두 표준 라이브러리만 필요한 독립형 Python 스크립트입니다.
preauth_user_backdoor.py — 인증 전 %U 트리거 → 인증 DB에 백도어 사용자(uid=0, homedir=/)가 주입됩니다. 자격 증명이나 DB 슈퍼유저가 필요 없습니다. 이 PoC는 PostgreSQL 전용이지만 MySQL 또는 sqlite 백엔드에서도 이 문제를 악용할 수 있습니다.preauth_user_rce.py — 인증 전 %U 트리거 → COPY TO PROGRAM을 통해 PostgreSQL 호스트에서 RCE. 자격 증명 불필요. ProFTPD DB 역할이 PostgreSQL 슈퍼유저여야 합니다.postauth_stor_backdoor.py — 인증 후 %{basename} 트리거 → 백도어 사용자 주입. 임의의 인증된 FTP 사용자가 필요합니다. 이 PoC는 PostgreSQL 전용이지만 MySQL 또는 sqlite 백엔드에서도 이 문제를 악용할 수 있습니다.전제 조건: Docker, Git, Python 3.10+ 및 uv.
cd setup
./setup.sh
첫 실행은 ProFTPD 소스를 클론하고 서버를 빌드합니다(약 2-3분). 이후 실행은 빌드 캐시를 재사용하므로 몇 초 안에 시작됩니다.
설정이 완료되면 전체 환경 세부 정보(FTP 및 DB 엔드포인트, 테스트 자격 증명, 복사해서 붙여넣을 수 있는 PoC 명령)를 출력합니다. 해당 지침에 따라 PoC를 실행하세요.
정리하려면:
cd setup
./teardown.sh
| 변수 | 의미 |
|---|
%A | 익명 로그인 비밀번호 문자열 |
%J | 명령 매개변수(동사 뒤의 모든 것) |
%S | 응답 메시지 문자열(오류에 에코된 공격자 입력이 포함될 수 있음) |
%U | USER의 원래 사용자 이름(인증 전에 설정되며 실패한 로그인에서도 사용 가능) |
%d | 디렉터리 이름(마지막 경로 구성 요소) |
%l | RFC 1413 ident 응답(공격자가 identd를 실행하면 제어 가능) |
%m | FTP 메서드/동사(공격자가 보낼 명령을 선택) |
%r | 전체 FTP 명령(동사 + 인자) |
%u | 인증된 사용자 이름 |
%{basename} | 경로 인자의 파일명 구성 요소, 디렉터리 접두사 없음 |
%f, %F, %D는 공격자 제어처럼 보이지만 항상 /로 시작하는 절대 경로로 확인되므로 is_escaped_text()의 ' 시작 요건을 충족할 수 없습니다.
관리자가 해당 SQLNamedQuery를 공격자가 도달할 수 있는 FTP 명령(또는 명령 클래스)에 대한 SQLLog 지시문에 바인딩한 경우. 널리 문서화된 SQLLog * 및 SQLLog ERR_* 와일드카드가 가장 광범위한 사례이며, %U 경로를 인증 전(pre-auth)으로 만드는 요소입니다. ERR_*는 실패한 USER에서 실행되므로 자격 증명이 필요 없습니다. SQLLog STOR 같은 명령별 지시문은 모든 인증된 사용자를 대상으로 합니다.
%U%{basename}COPY TO PROGRAMpostauth_stor_rce.py — 인증 후 %{basename} 트리거 → PostgreSQL 호스트에서 RCE. 임의의 인증된 FTP 사용자와 PostgreSQL 슈퍼유저 DB 역할이 필요합니다.postgres_blind_dump.py — 인증 전 %U 트리거 → 인증 users 테이블의 시간 기반 블라인드 추출. 스택 쿼리를 사용하지 않으므로 proftpd DB 역할에 최소 권한(로그 테이블에 대한 INSERT만)만 있는 배포에서도 동작하며, 위의 백도어/RCE PoC는 이러한 환경에서 안전하게 실패합니다. pg_sleep()을 통해 한 번에 한 비트씩 이진 탐색하여 모든 열의 모든 바이트(passwd 열 포함)를 추출합니다. sqlmap이 자동화할 작업을 제3자 의존성 없이 직접 구현한 것입니다.