PHP2Shell 취약점(CVE-2026-109618·109619)의 원리와 대응 방법

웹 애플리케이션의 보안은 보통 입력값 검증이나 인증 로직, 프레임워크 설정을 중심으로 다뤄진다. 하지만 애플리케이션 코드가 실행되기도 전에 PHP 인터프리터 자체의 요청 파싱과 메모리 정리 과정에서 문제가 발생한다면 어떨까?

지난 10월 21일 공개된 두 건의 PHP 보안 취약점(CVE-2026-109618, CVE-2026-109619, 일명 PHP2Shell)이 바로 그런 사례다. 두 취약점 모두 CVSS v4.0 9.2(Critical)로 평가됐으며, 조건이 맞으면 인증되지 않은 요청만으로 원격 코드 실행(RCE)이 가능하다. 이번 글에서는 두 취약점이 발생하는 구조적 원리와 보안상 위험, 그리고 실제 운영 환경에서 확인해야 할 점검 기준과 완화 설정을 살펴본다.

(참고: Debian 12 + Apache + PHP-FPM 환경에서 실제로 PHP 8.4.27 업그레이드를 진행한 작업 기록은 이전 글인 PHP 긴급 취약점 대응부터 Debian 자동 보안 업데이트까지를 참고하자.)


공개된 두 취약점과 패치 버전

이번에 공개된 취약점은 CVE-2026-109618과 CVE-2026-109619 두 가지다. 전자는 메모리 부족(Out-of-Memory, OOM) 상황의 예외 처리에서 메모리 손상을 일으킬 수 있는 문제이고, 후자는 해제된 메모리를 잘못 참조하는 Use-After-Free 계열의 문제다.

구분CVE-2026-109618CVE-2026-109619
심각도CVSS v4.0 9.2 (Critical)CVSS v4.0 9.2 (Critical)
취약점 유형OOM 예외 처리 중 메모리 손상요청 간 잔류 상태 참조 (Use-After-Free)
주요 발생 구간multipart/form-data 요청 파싱 및 정리 과정세션 업로드 진행률(upload_progress), Fatal Error 백트레이스 등
영향 범위PHP 5.4 ~ 8.5.11PHP 5.4 ~ 8.5.11

공식 패치가 적용된 PHP 계열별 해결 버전은 아래와 같다.

PHP 계열해결 버전
PHP 8.28.2.35 이상
PHP 8.38.3.36 이상
PHP 8.48.4.27 이상
PHP 8.58.5.12 이상

PHP 8.1 이하의 업스트림 보안 지원은 종료됐다. 다만 Debian이나 Ubuntu, RHEL처럼 배포판 공급자가 별도 백포트 패치를 제공하는 경우도 있으므로, 단순히 php -v의 버전 숫자만이 아니라 설치된 패키지의 보안 패치 내역도 함께 확인해야 한다.

2026년 10월 23일 추가 권고: PHP 프로젝트는 위 두 CVE의 패치 발표 이후, 별도의 조건부 문제인 CVE-2026-115560에 대해서도 안내했다. 애플리케이션이 session.serialize_handler를 php_binary 또는 php_serialize로 설정해 두었다가 실행 중 ini_set()으로 php로 변경하고 session_start()를 호출하는 특수한 조합에서 추가적인 RCE 위험이 남을 수 있다는 내용이다. 따라서 다음 정기 수정 릴리스까지 session.upload_progress.enabled=0으로 설정하는 것을 권고한다. 이 문제는 앞서 나열한 두 CVE와는 별개이며, PHP 공식 추가 권고에서 최신 상황을 확인할 수 있다.


왜 위험한가: 애플리케이션 실행 이전의 메모리 문제

일반적인 웹 취약점은 특정 URL 라우트나 파일 업로드 기능, 데이터베이스 쿼리처럼 애플리케이션 코드가 동작하는 과정에서 발생한다. 반면 이번 두 취약점은 HTTP 요청이 PHP 엔진에 전달되어 본문을 파싱하거나 요청을 마무리(Cleanup)하는 코어 레벨에서 발생한다. 특히 Apache mod_php 환경에서는 애플리케이션 코드가 실행되기도 전에 취약점을 악용하는 공격이 실제 입증됐다. PHP-FPM 등 다른 실행 방식에도 영향을 미칠 수 있지만, 입증된 공격 경로와 필요한 애플리케이션 조건은 다르다.

이 구조적 특징 때문에 운영 환경에서는 다음과 같은 보안 문제가 생긴다.

  • 애플리케이션 보안 로직 우회: mod_php에서 입증된 공격은 로그인 여부나 프레임워크 자체의 CSRF·입력값 필터링이 동작하기 전에 트리거된다. 다만 모든 SAPI에서 공개된 PHP 엔드포인트만 있으면 곧바로 동일한 RCE가 성립한다는 뜻은 아니다.
  • 워커 프로세스의 메모리 침해: PHP-FPM이나 Apache mod_php 환경에서는 하나의 워커 프로세스가 여러 요청을 순차적으로 처리한다. 메모리가 오염되거나 해제된 영역이 재참조되면 워커 권한으로 코드가 실행될 수 있고, 그 권한으로 접근 가능한 세션 정보, 데이터베이스 계정, 클라우드 환경 변수 등이 노출될 위험이 있다.
  • 에러 로그 기반 탐지의 한계: display_errors나 log_errors 설정을 꺼두더라도 내부 메모리 상태 손상을 막을 수는 없다. 애플리케이션 로그만으로 탐지하기 어려울 수 있으므로 운영 환경에서는 PHP-FPM·웹 서버·시스템 로그까지 함께 확인해야 한다.

취약점 발생 원리

CVE-2026-109618: OOM 예외 처리와 불완전 초기화된 구조체 해제

PHP는 요청을 처리하다가 memory_limit에 도달하면 내부적으로 longjmp()를 호출해 실행을 중단하고 요청 정리(Cleanup) 단계로 넘어가는 경로가 있었다.

문제는 RFC 1867 기반의 multipart/form-data 요청을 파싱하는 도중 내부 데이터 구조체가 절반만 초기화된 상태에서 메모리 부족(OOM)이 발생할 때 나타난다. 정리 루틴은 아직 초기화가 끝나지 않은 데이터를 유효한 상태로 판단해 접근하거나 잘못된 해제를 시도할 수 있다. 공격자가 이전 할당을 통해 해당 메모리 영역의 값을 정교하게 배치해 두었다면, 이런 비정상적인 정리 과정이 메모리 손상과 원격 코드 실행으로 이어질 수 있다.

멀티파트 요청 파싱 중 완전히 초기화되지 않은 메모리 구조를 정리하는 과정의 위험을 나타낸 삽화

메모리 부족으로 요청 데이터가 불완전하게 초기화된 상태에서 정리 루틴이 실행되는 상황

CVE-2026-109619: 잔류 전역 상태(Stale Global State)와 Use-After-Free

성능을 위해 워커 프로세스를 재사용하는 SAPI 환경(PHP-FPM, mod_php, FrankenPHP 등)에서는 요청이 끝날 때마다 내부 전역 상태가 깨끗하게 초기화되어야 한다.

하지만 세션 업로드 진행 상태를 추적하는 session.upload_progress 처리 과정이나 PHP 8.5에 도입된 Fatal Error 백트레이스 생성 경로 등에서는 요청이 끝난 뒤 실제 메모리는 해제됐지만 이를 가리키는 전역 포인터가 남는 문제가 있었다. 같은 워커가 다음 요청을 받아 초기화 루틴이 이 포인터를 덮어쓰기 전에 해당 참조에 접근하면, 이미 해제된 메모리를 다시 읽거나 쓰는 Use-After-Free가 발생한다.

PHP 워커가 이전 요청의 잔류 메모리 상태를 다음 요청에서 참조하는 Use-After-Free 위험을 표현한 삽화

같은 PHP 워커에서 이전 요청의 잔류 상태를 다음 요청이 참조하는 문제

다만 PHP-FPM은 요청 초기화 중 치명적인 오류가 나면 해당 워커를 종료하므로 mod_php와 같은 사전 실행 공격 경로가 그대로 재현되지는 않는다. 공개된 PHP-FPM 공격 경로는 애플리케이션 실행 과정에서 특정 상태가 남고 이후 요청이 이를 참조하는 등의 조건이 필요하다.


운영 환경에서 확인해야 할 사항

실제 서버를 점검할 때는 단순히 패키지 버전만 보는 것보다 현재 PHP가 어떤 방식으로 실행되고 있는지, 그리고 기존에 적용해 둔 보안·리소스 설정이 오히려 취약점 트리거 조건을 만들고 있지 않은지를 함께 봐야 한다.

1. 실행 중인 SAPI 방식과 패키지 패치 상태 확인

CLI 환경의 php -v 출력과 실제 웹 서버가 연결된 PHP-FPM 소켓이나 Apache 모듈의 버전이 다를 수 있다. 실행 중인 프로세스와 로드된 모듈, 패키지 버전을 먼저 확인한다.

# OS 및 실행 중인 PHP 버전, 패키지 상태 확인
cat /etc/os-release
php -v
dpkg-query -W 'php*'

# Apache 모듈 및 PHP-FPM 소켓 연결 설정 확인
sudo apache2ctl -M 2>/dev/null | grep -E 'mpm_|proxy_fcgi|php.*module'
sudo grep -RInE 'php8\.|php-fpm\.sock|SetHandler' /etc/apache2/sites-enabled/ /etc/apache2/conf-enabled/
systemctl status 'php*-fpm'

2. .user.ini와 ini_set() 설정의 한계

요청 파싱 단계나 요청 시작 직후에 발생하는 코어 취약점은 애플리케이션 스크립트에서 실행하는 ini_set()만으로 방어하기 어렵다. .user.ini는 CGI/FastCGI에서 디렉터리별 설정을 적용할 수 있지만, 모든 옵션을 설정할 수 있는 것은 아니고 애플리케이션 경로에 따라 적용 범위가 달라진다. 따라서 이번 취약점의 핵심 임시 완화 설정은 애플리케이션에서 변경할 수 있는 설정에 의존하지 않고, 전역 php.ini 또는 PHP-FPM 풀 설정(pool.d/www.conf)의 php_admin_value로 강제하는 편이 안전하다.

3. OS 레벨의 RLIMIT 메모리 제한 여부

프로세스 메모리를 제한하기 위해 RLIMIT_AS나 RLIMIT_DATA를 설정해 둔 환경이 있다면 주의해야 한다. RLIMIT에 의해 시스템 콜(mmap 등) 수준에서 메모리 할당이 실패하면 PHP 내부의 OOM longjmp() 경로가 호출되면서 CVE-2026-109618의 트리거 조건을 더 쉽게 만들 수 있다. 메모리 통제가 필요하다면 프로세스 RLIMIT 대신 컨테이너(cgroup) 단위의 메모리 제한을 검토할 수 있지만, 이 역시 완전한 취약점 완화 수단은 아니다.


패치 적용과 임시 완화 조치

이번 취약점은 설정 변경만으로는 모든 공격 경로를 완벽히 차단할 수 없으므로 패치 적용이 가장 확실한 해결책이다.

1. 보안 패치 적용과 워커 프로세스 재시작

각 브랜치의 패치 버전(8.2.35, 8.3.36, 8.4.27, 8.5.12 이상)으로 패키지를 업데이트하거나 상위 지원 버전으로 전환한다. 패치를 설치한 뒤에는 메모리에 남아 있는 기존 워커 프로세스가 완전히 내려가도록 단순 reload가 아니라 restart를 수행하는 것이 확실하다.

# PHP-FPM 재시작 및 상태 확인 (PHP 8.4 기준)
sudo systemctl restart php8.4-fpm
systemctl is-active php8.4-fpm

Apache mod_php를 사용하는 환경이라면 Apache 데몬 자체를 재시작(systemctl restart apache2)해야 새로운 라이브러리가 메모리에 올라간다.

2. 즉시 패치가 어려운 환경을 위한 완화 설정

운영 일정상 당장 패키지 업그레이드가 어렵다면, 앞단 웹 서버와 PHP-FPM 전역 설정에서 공격 표면을 줄이는 조치를 먼저 적용할 수 있다.

불필요한 멀티파트 요청 차단
파일 업로드를 사용하지 않는 API 서버나 페이지라면 웹 서버(Nginx, Apache) 또는 WAF 앞단에서 multipart/form-data 요청을 차단한다. PHP 8.4부터는 request_parse_body()를 통해 POST 외의 메서드에서도 멀티파트 파싱이 일어날 수 있으므로, 요청 메서드와 무관하게 Content-Type 헤더를 기준으로 제한하는 것이 안전하다.

# Nginx 예시: 파일 업로드가 필요 없는 경로에서 멀티파트 요청 차단
location ~ \.php$ {
    if ($content_type ~* "multipart/form-data") {
        return 403;
    }
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
}

PHP-FPM 풀 설정(php_admin_value) 조정
CVE-2026-109619의 주요 진입점인 업로드 진행률 추적 기능을 비활성화하고, CVE-2026-109618의 OOM 유도를 어렵게 하기 위해 요청 크기를 제한한다. 아래는 PHP 8.4를 기준으로 한 예시다.

; /etc/php/8.4/fpm/pool.d/www.conf 예시
; 애플리케이션 코드에서 덮어쓰지 못하도록 php_admin_value 사용
php_admin_value[session.upload_progress.enabled] = 0

; 요청 본문 및 업로드 크기를 서비스에 필요한 최소 수준으로 제한
php_admin_value[post_max_size] = 8M
php_admin_value[upload_max_filesize] = 4M

; PHP 8.5 환경에서만, 패치 전 추가 완화 설정으로 고려
; php_admin_value[fatal_error_backtraces] = 0

특히 fatal_error_backtraces 설정은 PHP 8.5의 기능이므로 PHP 8.4 이하의 설정 예시에 무조건 추가해서는 안 된다. 또한 설정만으로 취약점이 제거되는 것은 아니므로 패치를 우선해야 한다.

만약 트래픽 여유가 있는 내부 관리 시스템이라면 PHP-FPM 풀 설정에서 pm.max_requests = 1을 지정해 워커 프로세스가 요청을 하나만 처리하고 재생성되도록 구성할 수도 있다. 워커 재사용 자체를 막아 CVE-2026-109619와 같은 요청 간 메모리 잔류 문제를 줄여주지만, 프로세스 생성 비용이 커지므로 트래픽이 많은 서비스에는 신중히 적용해야 한다.

또한 memory_limit = -1로 PHP 내부 OOM 발생을 막는 방법도 거론되지만, 이 경우 대용량 요청이 들어왔을 때 호스트 물리 메모리가 고갈될 위험이 있다. 따라서 memory_limit = -1은 웹 서버 앞단의 요청 크기 제한(client_max_body_size 등)과 별도의 리소스 격리·모니터링을 갖춘 환경에서만 제한적으로 검토해야 한다. 컨테이너 cgroup 메모리 제한 역시 메모리 할당 실패 자체를 완전히 없애지는 못한다.


정리

CVE-2026-109618과 CVE-2026-109619는 애플리케이션 코드가 아무리 안전하게 작성되어 있어도 런타임 엔진의 메모리 처리 결함 하나로 원격 코드 실행까지 이어질 수 있음을 보여준다.

운영 중인 서버가 있다면 먼저 php -v와 패키지 패치 내역을 함께 확인해 패치 버전(8.2.35, 8.3.36, 8.4.27, 8.5.12 이상)을 적용하고, PHP-FPM이나 웹 서버 워커 프로세스가 실제로 재시작됐는지까지 점검하자. 또한 10월 23일 추가된 session.upload_progress.enabled 권고도 확인해야 한다. 이번 기회로 EOL(지원 종료) 단계에 들어간 구버전 PHP를 사용 중이라면 장기적인 유지보수를 위해 상위 버전으로의 전환과 자동 보안 업데이트 체계를 함께 정비해 두는 것이 좋다.

참고 자료: PHP 공식 보안 공지, CVE-2026-109618 기술 권고, CVE-2026-109619 기술 권고, 10월 23일 추가 안내