본문으로 건너뛰기

PHP2Shell 분석: PHP 엔진의 RCE 취약점 두 건과 대응 방법

2026년 10월 21일, PHP 프로젝트가 PHP 8.5.12, 8.4.27, 8.3.36, 8.2.35를 긴급 배포했습니다.1 고친 건 PHP 엔진의 치명적 취약점 두 건(CVE-2026-109618, CVE-2026-109619)이고, 연구자들은 이 두 건을 묶어 PHP2Shell이라고 부릅니다.

WordPress나 Laravel 같은 애플리케이션이 아니라 PHP 런타임의 결함이어서, 플러그인과 프레임워크를 최신으로 올려도 해결되지 않습니다. 이 글은 어떤 구조의 문제였고 어떤 실행 방식이 더 위험한지, 그리고 지금 무엇을 해야 하는지를 정리합니다.

PHP2Shell 유출부터 수정 릴리스까지의 타임라인입니다. 세로로 일곱 단계가 위에서 아래로 이어지고, 각 단계에는 UTC 시각과 괄호 안의 KST 시각이 적혀 있습니다. 일곱 단계는 세 구간으로 묶입니다. 첫 구간 '유출과 사전 공지'에는 1단계부터 3단계가, 둘째 구간 '방어 측 준비와 조기 공개'에는 4단계와 5단계가, 셋째 구간 '릴리스와 악용 시도'에는 6단계와 7단계가 들어갑니다. 1단계는 10월 20일 15:00 UTC(10월 21일 00:00 KST)로, CVE 번호가 포럼과 SNS에 유출되었고 잘못된 릴리스 시각과 CVSS 10.0이 함께 돌았습니다. 2단계는 10월 20일 19:30 UTC(10월 21일 04:30 KST)로, PHP가 사전 공지를 게시하고 릴리스를 10월 22일에서 10월 21일 14:00 UTC로 앞당겼습니다. 3단계는 10월 20일 20:00 UTC 무렵(10월 21일 05:00 KST 무렵)으로, PHP 재단에 따르면 Calif가 PoC 시연 영상을 실수로 게시했고 삭제 뒤에도 복사본이 유통되었습니다. 4단계는 10월 21일 03:02 UTC(12:02 KST)로, Cloudflare가 WAF 기본 보호를 적용했습니다. 5단계는 10월 21일 07:41 UTC(16:41 KST)로, PHP 재단에 따르면 Chainguard가 수정이 포함된 이미지를 먼저 공개했습니다. 6단계는 10월 21일 13:30 UTC 무렵(22:30 KST 무렵)으로, 예정보다 먼저 PHP 8.5.12, 8.4.27, 8.3.36, 8.2.35가 릴리스되었습니다. 7단계는 릴리스 이후로, 여러 보안 업체가 익스플로잇 시도를 보고했고 성공 사례는 확인되지 않았습니다. 단계 사이는 위에서 아래로 향하는 화살표로 연결됩니다.


PHP2Shell은 PHP 엔진 자체의 결함입니다

PHP2Shell은 CVE 두 건을 묶어 부르는 이름입니다. PHP 보안팀은 두 건 모두 CVSS v4.0 기준 9.2로 평가했고, 둘 다 인증 없이 원격 코드 실행(Remote Code Execution, RCE) 으로 이어질 수 있습니다. 원격 코드 실행은 공격자가 네트워크를 통해 서버에서 원하는 코드를 실행하는 공격을 말합니다.

CVEPHP 공식 권고내용
CVE-2026-109618GHSA-w8hw-w9fx-j79w, RCE via OOM corruption메모리가 부족해진 순간 요청 데이터가 반쯤 초기화된 채 남고, 요청 정리 단계가 그 데이터를 정상으로 취급합니다
CVE-2026-109619GHSA-6wmf-4rv5-p3rp, RCE via stale global state요청이 끝난 뒤에도 프로세스에 남은 상태가 해제된 메모리를 가리키고, 다음 요청이 그 상태를 사용합니다

점수는 평가 기관마다 다릅니다. PHP는 CVSS v4.0으로 9.2를 매겼고, Tenable의 CVE 페이지에는 v3.1 기준 9.8이, Red Hat 자체 평가에는 v3.1 기준 8.1(Important)이 적혀 있습니다. PHP의 벡터에는 공격 복잡도 높음(AC:H)과 공격 요구 조건 존재(AT:P)가 들어 있어서, 환경에 따라 성공 조건이 달라집니다. PHP 보안팀도 익스플로잇의 이식성과 신뢰도가 PHP 빌드, 확장 모듈, 프런트엔드 동작, 메모리 할당자 배치에 따라 달라진다고 밝혔습니다.

공식 권고의 영향 버전은 8.2.35 미만, 8.3.36 미만, 8.4.27 미만, 8.5.12 미만으로 적혀 있습니다. 지원이 끝난 브랜치는 권고 목록에 없을 뿐, 영향을 받지 않는다는 뜻은 아닙니다. Wiz와 Calif는 취약한 경로가 PHP 5.4까지 거슬러 올라간다고 설명했습니다.

Wiz는 WordPress, Drupal, Joomla의 최신 컨테이너 이미지에서 인증 없는 RCE를 시연했습니다. Calif는 MediaWiki, Roundcube, Magento, phpMyAdmin, Nextcloud, Laravel, Symfony 등에서도 인증 없는 RCE를 확인했다고 보고했습니다. 이 목록은 대표 사례입니다.

PHP는 서버 측 언어가 알려진 웹사이트의 약 70%에서 사용됩니다(Cloudflare가 인용한 W3Techs 집계). 취약한 버전이 널리 퍼져 있어 파급 범위도 넓습니다.


실패한 요청이 남긴 상태를 다음 요청이 믿어버렸습니다

PHP는 요청마다 전용 메모리 영역을 할당하고, 요청이 끝나면 그 영역을 통째로 해제합니다. 한편 요청이 끝나도 종료되지 않고 다음 요청을 이어서 처리하는 프로세스를 워커(worker) 라고 합니다. Apache mod_php의 자식 프로세스와 PHP-FPM의 워커가 여기에 해당합니다.

워커의 변수가 이미 해제된 요청 메모리를 가리킨 채로 남으면, 다음 요청이 그 값을 정상 상태로 믿고 사용하게 됩니다. 이렇게 해제된 메모리를 다시 사용하는 오류를 해제 후 사용(Use-After-Free, UAF) 라고 합니다. 해제된 자리에 공격자가 보낸 데이터가 채워져 있으면 메모리 손상과 코드 실행으로 이어질 수 있습니다.

PHP 워커가 이전 요청이 남긴 상태를 다음 요청에서 사용하는 과정을 보여주는 흐름도입니다. 위쪽 카드는 요청 A, 아래쪽 카드는 요청 B이고, 두 카드 사이에 '같은 워커 프로세스가 살아남음'이라는 띠가 놓여 있습니다. 요청 A 카드는 네 단계입니다. 1단계는 요청 메모리를 가리키는 전역 상태를 만듭니다. 2단계는 처리 도중 OOM이나 치명적 오류로 중단됩니다. 3단계는 정리 코드가 실행되지 않습니다. 4단계는 요청 메모리가 해제되지만 전역 상태는 해제된 주소를 가리킨 채 남습니다. 요청 B 카드는 세 단계입니다. 5단계는 정상 초기화 전에 INI 진단이나 세션 업로드 진행률 처리가 전역 상태를 읽습니다. 6단계는 해제된 메모리를 사용합니다(해제 후 사용). 7단계는 메모리 손상이나 정보 노출로 이어지고 조건이 맞으면 코드 실행에 이릅니다. 도표 아래에는 두 줄의 메모가 있습니다. 위쪽 메모는 Apache mod_php에서는 요청 시작이 실패해도 종료 절차가 실행되어 이 경로가 유지된다는 내용입니다. 아래쪽 메모는 PHP-FPM에서는 요청 시작이 실패하면 워커가 종료되어 이 경로가 끊기고, 시작 이후의 실패는 같은 경로가 가능하다는 내용입니다.

정리가 건너뛰어지는 모양을 단순화한 의사 코드입니다.

// 단순화한 의사 코드이며 실제 PHP 소스가 아닙니다
worker_state = request_memory;   // 요청 메모리를 가리키는 상태를 먼저 공개합니다
do_fallible_work();              // 여기서 OOM이나 치명적 오류가 나면 중단됩니다
worker_state = NULL;             // 이 정리 코드는 실행되지 않습니다

상태를 먼저 공개하고 실패할 수 있는 작업을 거친 뒤에 정리하는 순서에서는, 작업이 중간에 끊기면 정리를 건너뛰게 됩니다. Calif는 상태 공개, 실패할 수 있는 작업, 초기화로 이어지는 패턴을 일반화해 같은 계열의 변종을 찾았다고 설명합니다.

PHP는 memory_limit 같은 한도 때문에 메모리를 더 할당하지 못하는 상태, 즉 메모리 부족(Out-Of-Memory, OOM) 이 되면 longjmp()로 현재 작업을 빠져나가 요청 처리를 중단합니다. longjmp()는 호출 스택을 건너뛰어 미리 저장해 둔 위치로 돌아가는 C 함수입니다. 중단된 위치 뒤에 있던 초기화와 정리 코드는 실행되지 않습니다.

CVE-2026-109618: 초기화가 끝나기 전에 메모리가 바닥나는 경우

요청을 파싱하는 일부 구간에서는 요청 메모리가 완전히 초기화되기 전에 다른 코드가 참조할 수 있습니다. 이 구간에서 할당이 하나라도 실패하면 PHP는 조기 종료를 제대로 처리하지 못해 요청 메모리를 잘못된 상태로 남깁니다. 실패한 할당이 초기화 중이던 메모리와 상관없는 할당이어도 결과는 같습니다. OOM이 나는 순간 처리 전체가 중단되므로, 그때까지 초기화가 덜 끝난 메모리는 그 상태 그대로 남기 때문입니다. 이후 요청 종료 절차가 이 미완성 데이터를 정상 값으로 여기고 읽거나 조건 분기에 사용하면 RCE로 이어집니다.

요청 크기 제한 안에 머무는 요청으로도 메모리 한도에 닿을 수 있다는 점이 문제를 키웁니다. 권고문은 그 경로를 세 가지로 정리합니다.

  • 길이를 선언하지 않은 본문: 사용할 수 있는 Content-Length가 없는 multipart 본문은 post_max_size 검사를 통과한 뒤에도 데이터를 계속 공급할 수 있습니다.
  • 헤더가 많은 파트: multipart 파트 하나에 헤더를 제한 없이 넣을 수 있고, PHP 내부 표현은 전송 크기보다 훨씬 많은 메모리를 차지해 post_max_size 안에서도 memory_limit을 넘길 수 있습니다.
  • 비어 있는 파일 필드와 중첩 이름: 비어 있는 파일 필드는 max_file_uploads 카운트에 들어가지 않으면서도 일반 파일과 같은 메타데이터 배열을 만듭니다. 여기에 필드 이름을 f[a][b][c]처럼 중첩하면 배열을 만드는 작업이 몇 배로 불어나므로, 요청 크기는 작게 유지하면서도 메모리 사용량을 크게 늘릴 수 있습니다.

애플리케이션 고유의 경로에서 큰 할당이 일어나도 OOM에 닿을 수 있다고 권고문은 덧붙입니다.

수정은 OOM 처리 방식을 바꾸는 쪽으로 이뤄졌습니다. longjmp()를 없애고, 다음 VM 안전 지점(PHP 가상 머신이 안전하게 멈출 수 있는 위치)에서 요청을 정리하며 끝내는 소프트 OOM과, 메모리 사용량이 더 높은 두 번째 임계값에 닿으면 _exit()로 즉시 끝내는 하드 OOM으로 바꿨습니다. 종료 절차가 잘못된 상태를 더 이상 보지 않으면서도 PHP의 메모리 제한 기능은 유지됩니다.

CVE-2026-109619: 요청이 끝난 뒤에도 워커에 남는 상태

워커가 요청 사이에도 유지하는 변수를 전역 상태(global state) 라고 합니다. 오류, 세션, 객체, 확장 모듈의 상태가 여기에 들어가고, 그중에는 한 요청의 메모리를 가리키는 값이 있습니다. 이 값은 요청 메모리를 해제하기 전에 초기화되어야 하는데, 치명적 오류가 작업을 중간에 끊으면 초기화가 빠집니다.

권고문은 이 문제를 생산자와 소비자의 조합으로 설명합니다. 생산자는 요청이 소유한 상태를 전역에 남기는 작업이고, 소비자는 다음 요청에서 정상 초기화보다 먼저 그 상태를 읽거나 해제하는 작업입니다. 두 요청이 같은 워커에 도달해야 성립합니다.

치명적 오류로 중간에 끊긴 작업이 생산자가 될 수 있습니다. 파싱과 컴파일 실패, 잡히지 않은 오류, require 실패, 최대 실행 시간 초과가 치명적인 오류로서 중간에 작업을 끊을 수 있고, 이미 상태가 공개된 시점의 OOM도 같은 역할을 합니다. 확인된 소비자는 두 가지입니다.

  • INI 진단: 요청 초기화보다 먼저 INI 설정값을 적용하다가 잘못되었거나 더 이상 쓰지 않는 값이 있으면 경고가 나고, 이 경고가 전역 오류 상태에 접근합니다. 경고를 표시할지 기록할지 정하기 전에 일어나므로 error_reporting, @, display_errors, log_errors로는 막지 못합니다.
  • 세션 업로드 진행률: 기본으로 켜져 있고 session_start() 호출 없이도 동작하는 기능이 정상 초기화보다 먼저 세션 상태에 접근합니다.

확인된 사례도 두 가지입니다. PHP 8.5에서 fatal_error_backtraces가 켜진 상태의 치명적 오류 백트레이스는 시연으로 가장 확실하게 검증된 경로이고, mod_php RCE의 근거입니다. 세션 업로드 진행률의 수명주기는 mod_php와 PHP-FPM 모두에서 검증되었습니다.

요청 경계를 넘어 남을 수 있는 상태를 더 넓은 범위에서 초기화하는 방향으로 수정되었습니다. 권고문은 설정만으로 모든 경로를 막는 방법은 없다고 밝혔습니다.

Wiz는 이 계열에서 악용 가능한 버그 네 가지를 시연했습니다. 세션 업로드 진행률 과정의 역직렬화 UAF, 프로세스 메모리의 원격 노출, 치명적 오류 백트레이스의 UAF, OPcache(컴파일된 PHP 코드를 공유 메모리에 캐시하는 확장)의 타입 혼동이며, 변종이 더 있다고 설명합니다. Wiz는 이번 패치가 근본 원인을 고쳐 변종까지 없앤다고 봅니다. 다른 요청이나 서버가 다루는 비밀번호, 토큰, API 키, 인증서가 들어 있을 수 있는 메모리를 RCE 없이 읽을 수 있다는 점은 따로 봐야 합니다.


mod_php가 가장 위험하고 PHP-FPM도 조건부로 영향을 받습니다

같은 취약점이어도 서버 API(Server API, SAPI) 에 따라 공격 조건이 달라집니다. SAPI는 PHP가 웹 서버와 맞물려 요청을 받는 실행 방식으로, Apache 모듈(mod_php)과 PHP-FPM 등이 있습니다. PHP는 웹 요청을 처리하는 모든 설치를 위험 대상으로 분류했고, 그중 mod_php의 위험을 가장 높게 평가했습니다.

실행 방식공식 권고와 PHP 재단 글에서 확인된 내용
Apache mod_php요청 시작이 실패해도 종료 절차를 무조건 실행하므로, 애플리케이션 코드가 실행되기 전에 인증 없는 요청으로 공격이 성립합니다. 확인된 경로는 PHP 8.5의 fatal_error_backtraces이며, 이 설정은 8.5에서 기본값이 1입니다
PHP-FPM요청 시작이 실패하면 자식 프로세스가 종료되어 같은 경로가 막힙니다. 다만 시작 이후 애플리케이션 경로를 거치는 방식으로 RCE가 확인되었고, CVE-2026-109618만으로 이루어진 완전한 FPM 익스플로잇은 시연되지 않았습니다
FrankenPHP소스 분석상 mod_php처럼 종료 절차를 무조건 실행해 애플리케이션과 무관한 공격이 가능해 보이지만, 시연되지는 않았습니다
LiteSpeed(LSAPI)FPM과 비슷한 모델로, 애플리케이션에 따른 익스플로잇이 가능해 보이나 검증되지 않았습니다
php-cgiPHP가 영향 범위에 포함시켰지만 개별 조건은 공개된 자료에서 확인하지 못했습니다
CLI권고문과 공지가 다루는 범위는 웹 요청을 처리하는 설치입니다. CLI는 요청 하나로 끝나 상태가 다음 요청으로 이어지지 않지만, 같은 패키지에서 나오는 바이너리이므로 함께 업데이트합니다

PHP-FPM이라고 안전하지는 않습니다. PHP 재단은 PHP-FPM에서도 애플리케이션 코드 경로를 거치는 RCE를 확인했다고 밝혔고, Calif는 Nginx와 PHP-FPM 조합의 시연 영상을 공개했습니다. session_start(), session_decode(), unserialize()처럼 시작 이후에 데이터를 디코딩하는 경로가 있는 애플리케이션이 해당한다고 Calif는 설명합니다.

Red Hat은 반대쪽 평가도 함께 냈습니다. 기본 배포가 PHP-FPM인 RHEL에서는 초기화 실패 시 워커가 종료되어 손상된 구조에 닿지 않으므로 실제 RCE가 어렵고, 스크립트 실행 중 두 번째 요청 본문을 파싱하는 비기본 동작이 보통 필요하다고 봤습니다. 그래서 자체 심각도를 Important로 매겼습니다. 어느 쪽이든 mod_php를 사용하지 않는 환경도 패치를 미루지 않아야 한다는 결론은 같습니다.


이 결함은 왜 이렇게 오래 남아 있었습니까

Calif와 PHP 재단이 정리한 경위는 네 시점으로 나뉩니다.

  • 2004: Stefan Esser가 PHP의 메모리 한도와 관련된 원격 취약점을 처음 공개했습니다. Calif의 연구자는 그 후속 결함이 코드베이스에 남아 있는지 찾는 것으로 이번 연구를 시작했다고 합니다.
  • 2020: 버그 #78876이 보고되었습니다. 긴 변수가 OOM과 임시 파일 정리 실패를 일으킨다는 내용이었고, 당시에는 크게 다뤄지지 않았습니다.
  • 2024: OSTIF가 조직하고 Quarkslab이 수행한 PHP 외부 감사에서 같은 계열의 OOM 문제가 다시 나왔습니다. 악용이 증명되지 않아 수정되지 않았습니다.
  • 2026: 7월 30일에 OpenAI가 Calif를 Patch the Planet 프로그램에 초대했고, Calif는 8월 1일에 첫 OOM 트리거를 찾아 8월 11일에 첫 결함을 보고했습니다. PHP 재단은 이 보고를 무조건적인 RCE로 기록했고, OpenAI도 같은 날 같은 근본 원인을 독립적으로 보고했다고 밝혔습니다. 이어서 Wiz가 8월 24일에, OpenAI가 9월 18일에 추가 공격 경로를 보고했습니다.

PHP 재단은 2024년에 이미 OOM 문제의 첫 징후를 발견했지만 수정이 몇 달 동안 멈췄다 재개되기를 반복했다고 설명합니다. 수정안은 여러 개였고, 각각 하위 호환성 훼손, 새로운 서비스 거부(Denial of Service, DoS) 가능성, 서드파티 확장 모듈 다수의 수정이라는 비용이 있었습니다. 합의로 움직이는 보안팀은 그중 하나를 고르지 못했습니다. 재단은 이 지연을 구조적 실패라고 평가하고, 의사결정을 다수결로 하도록 절차를 바꾸고 있다고 밝혔습니다.

네 곳 모두 AI 모델을 사용했다고 밝혔지만 평가는 서로 다릅니다.

  • Wiz: Wiz Atlas, Gemini 4 Argon, GPT-5.6 Sol을 사용했고, 익스플로잇을 여러 대상에 맞게 조정하는 일이 AI 덕분에 몇 시간 단위로 줄었다고 설명합니다. 연구자 한 명이 일주일에 끝낸 일이 이전에는 최고 수준 팀의 몇 달짜리 작업이었다고 평가합니다.
  • Calif: 초기 발견에는 특별한 하네스나 비공개 모델을 사용하지 않았고, 이미 많은 연구소가 AI 보조 감사를 마친 대상이었다고 말합니다. 취약점의 역사를 알고 있어야 질문을 제대로 던질 수 있다는 것이 Calif의 평가입니다.
  • OpenAI: Calif가 GPT-5.6 Sol로 결함을 찾았고 Wiz도 독립적으로 두 취약점을 발견했다고 밝히며, 자신들이 아는 한 프런티어 AI가 공격자보다 먼저 찾았다고 평가합니다.
  • PHP 재단: 올해 초 Alpha-Omega와 함께 Anthropic의 Claude Mythos로 PHP를 스캔했고, 이후 OpenAI의 Daybreak Red 등 도구를 추가했다고 밝혔습니다. AI 보조 연구 덕분에 보고가 늘어 7월 1일 이후 CVE 15건을 발행했고, 100건 넘는 보고는 유효하지 않거나 중복이었다고 합니다.

다만 위 평가는 각 조직의 자기 보고이고 사례도 하나뿐이어서, 일반화에는 신중해야 합니다.


지금 실제로 악용되고 있습니까

2026년 10월 22일 오전 6시(KST) 무렵까지 공개된 내용입니다. 업체마다 관측 범위가 달라서 보고가 엇갈립니다.

출처관측 내용
PHP 재단유출 뒤 등장한 PoC를 살폈지만 릴리스 시점에는 기능하는 익스플로잇을 확인하지 못했다고 밝혔습니다
Cloudflare10월 21일 03:02 UTC부터 WAF 기본 보호를 적용했고, 공개 전부터 공격 트래픽과 페이로드 변형 시도를 관측했다고 설명합니다. CTO Dane Knecht는 초기 공격이 나타나고 있으며 새 벡터에 맞춰 완화책을 갱신 중이라고 밝혔습니다(The Hacker News, Tenable 인용)
watchTowr10월 21일 허니팟에서 두 CVE의 악용 시도를 관측했다고 밝혔고, SecurityWeek에 따르면 성공한 시도는 아직 없다고 말했습니다
Patchstack, VegaSecurityWeek에 따르면 악용 시도를 관측했고, Vega는 공개 PoC를 사용한 시도를 확인했습니다
Cisco Talos10월 21일 14:00 UTC 기준 실제 악용을 관측하지 못했고, Snort 룰 67359와 67360을 배포했습니다
Huntress10월 21일 오전 10시(ET, 14:00 UTC) 기준 파트너 환경에서 악용을 관측하지 못했습니다
CISA경보 발행 시점에 악용을 관측하지 못했다고 밝히며 즉시 패치를 권고했고, 바로 패치할 수 없으면 PHP 기반 시스템을 오프라인으로 내리라고 했습니다. KEV 목록에는 오르지 않았습니다(watchTowr, 10월 21일 기준)
NCSC-NL10월 21일 기준 활발한 악용의 징후는 없지만 단기간에 대규모 악용이 일어날 가능성이 크다고 평가했습니다

대부분은 악용 시도를 관측했다는 보고이고, 성공한 침해 사례를 근거와 함께 공개한 곳은 확인하지 못했습니다. 그렇다고 위험이 낮은 것은 아닙니다. 공개 PoC가 유통되고 있고, Tenable은 검증되지 않은 익스플로잇 코드에 악성코드가 섞여 있을 수 있다고 경고했습니다. 출처가 불분명한 PoC는 직접 실행하지 않아야 합니다.

정부 기관의 권고도 같은 방향입니다. CERT-EU는 10월 21일 권고 2026-018에서 즉시 패치를 권고하고, 패치가 어려우면 인터넷 노출 자원을 공용망에서 격리하라고 했습니다. 릴리스 시각은 이 권고가 13:30 UTC로 기록했습니다. 영국 NCSC는 완화책 적용이 오래 걸리면 시스템을 오프라인으로 내리는 선택지를 언급했고, 즉시 업데이트할 수 없다면 침해 흔적부터 살피라고 권고했습니다. 국내에서는 AhnLab ASEC가 10월 21일 긴급공지를 냈습니다. KISA 보호나라도 같은 날 보안공지 “PHP 제품 보안 업데이트 권고”를 올려 8.2.35, 8.3.36, 8.4.27, 8.5.12 이상으로 업데이트하고 8.1 이하는 지원 버전으로 올리라고 권고했습니다.


내 서버에는 어떤 수정이 필요합니까

공식 수정은 지원 중인 네 브랜치에만 나왔습니다.

사용 중인 브랜치공식 수정대응
8.58.5.128.5.12 이상으로 업데이트합니다
8.48.4.278.4.27 이상으로 업데이트합니다
8.38.3.368.3.36 이상으로 업데이트합니다
8.28.2.358.2.35 이상으로 업데이트합니다. 8.2는 2026년 12월 31일에 지원이 끝나므로 이전 일정도 함께 잡아야 합니다
8.1, 8.0, 7.4공식 릴리스 없음PHP 보안팀이 제공한 최소 소스 백포트(7.4, 8.0, 8.1)를 적용하거나 배포판과 LTS 공급자의 백포트를 확인합니다. 가능하면 지원 브랜치로 이전합니다
7.3 이하, 5.x공식 수정 미확인지원 브랜치로 이전합니다. 이전하기 전까지는 VPN이나 IP 허용 목록으로 인터넷 노출을 줄입니다

웹 요청을 처리하는 PHP는 셸에서 실행하는 php -v와 다를 수 있습니다. 서비스가 실제로 사용하는 바이너리를 확인해야 합니다.

php -v                    # 셸(CLI)의 버전이며 웹 서버의 PHP와 다를 수 있습니다
php-fpm8.3 -v             # FPM 바이너리 이름은 배포판마다 다릅니다
apachectl -M | grep php   # Apache에 mod_php가 로드되어 있는지 확인합니다

OpenAI가 공개한 php2shell-checker는 HTTP 헤더나 php -v의 버전을 수정 버전과 비교해 줍니다. 다만 헤더가 숨겨져 있으면 UNKNOWN을 반환하고, 배포판 백포트는 판별하지 못합니다.

배포판 패키지와 컨테이너 이미지

배포판은 업스트림 버전 번호를 그대로 올리기도 하고, 번호를 유지한 채 수정만 백포트하기도 합니다. 그래서 php -v가 아니라 패키지 버전과 배포판 보안 공지로 확인해야 합니다.

배포판10월 21일 기준 상태출처
Debian 13 (trixie)php8.4 8.4.27-1~deb13u1로 수정(DSA-6574-1)Debian 보안 추적기
Debian 12 (bookworm)php8.2 8.2.35-1~deb12u1로 수정(DLA-4848-1)Debian 보안 추적기
Debian 11 (bullseye)Debian LTS가 2026년 8월 31일에 끝나 무료 수정 없음Mattias Geniar의 정리
Ubuntu전 릴리스 “Needs evaluation”, 우선순위 CriticalUbuntu CVE 추적기
RHEL심각도 Important, 자체 CVSS 3.1 8.1Red Hat CVE 페이지
Remi 저장소10월 21일 갱신 없음. 배포판 패키지로 옮기거나 소스 빌드 권고. 10월 22일 오전까지 블로그에는 8.4.27RC1과 8.5.12RC1만 올라와 있었습니다PHP 공지, Remi 블로그
Docker 공식 php 이미지10월 22일 오전 기준 versions.json이 8.5.12, 8.4.27, 8.3.36, 8.2.35를 가리킵니다docker-library/php

Ubuntu와 Red Hat은 기존 버전 번호에 수정을 백포트하는 경우가 많아, Ubuntu 24.04의 PHP는 수정 후에도 8.3.6으로 표시될 수 있습니다. 패키지 변경 로그에서 CVE 번호를 확인할 수 있습니다.

# Ubuntu: 설치된 패키지의 변경 로그에서 CVE 번호를 찾습니다
zgrep -E 'CVE-2026-10961[89]' /usr/share/doc/php8.3-fpm/changelog.Debian.gz
# RHEL 계열: 보안 업데이트 목록에서 php를 찾습니다
dnf updateinfo list --security | grep php

컨테이너는 베이스 이미지 태그를 수정 버전으로 갱신해 다시 빌드한 뒤 실행 중인 컨테이너를 교체해야 합니다. PHP 재단은 Chainguard가 릴리스 예정 시각보다 6시간 이상 먼저 수정이 포함된 이미지를 공개해 엠바고를 깨뜨렸다고 밝혔습니다. Socket에 따르면 Chainguard CEO Dan Lorenc는 Hacker News 댓글에서 전날 밤부터 악용 시도를 보았고 그에 따라 판단했다고 설명했습니다. The Record는 Chainguard가 논평 요청에 답하지 않았다고 썼습니다.

패치 뒤 재시작 범위

실행 방식마다 재시작해야 하는 대상이 다릅니다. Mattias Geniar가 컨테이너에서 검증해 정리한 내용을 옮기면 다음과 같습니다.

실행 방식재시작 대상
PHP-FPMphp-fpm 서비스 재시작. reload로도 새 바이너리가 올라갑니다
Apache mod_phpapache2/httpd 전체 재시작. reload와 apachectl graceful로는 이전 libphp가 남아 있습니다
LiteSpeedlsphp 패키지 갱신 뒤 pkill lsphp. LiteSpeed 재시작만으로는 PHP 프로세스가 재시작되지 않습니다
FrankenPHP새 릴리스나 이미지로 교체. PHP가 내장되어 있어 apt와 dnf로 고쳐지지 않습니다
Docker php: 이미지새 이미지를 받아 docker build --pull로 다시 빌드하고 컨테이너를 재생성합니다
큐 워커, Laravel Octane, Supervisor 작업별도로 재시작합니다. octane:reload는 코드만 다시 읽고 PHP는 바꾸지 않습니다

이전 바이너리가 남아 있는 프로세스는 다음 명령으로 찾을 수 있습니다.

# 해제된 라이브러리 파일을 아직 매핑하고 있는 프로세스를 찾습니다. 출력이 없으면 전부 새 PHP입니다
sudo grep -lE ' /(usr|lib|opt).*\(deleted\)$' /proc/[0-9]*/maps

패치가 바꾸는 동작: 하드 메모리 한도

이번 수정은 취약점만 막는 것이 아니라 메모리 한도의 동작을 바꿉니다. 8.4.27 소스 기준으로 memory_hard_limit_percent라는 INI 설정이 새로 생겼고, 기본값(-1)에서는 memory_limit의 200%가 하드 한도입니다. memory_limit이 1G를 넘으면 200% 대신 1G를 더한 값을 사용하고, memory_limit=-1이면 하드 한도도 없습니다.

memory_limit에 닿으면 이전처럼 “Allowed memory size exhausted” 치명적 오류가 나지만, 이제는 다음 VM 안전 지점에서 요청을 정리하며 끝냅니다(소프트 OOM). 하드 한도에 닿으면 “Hard memory limit of N bytes exhausted” 메시지를 남기고 _exit()로 프로세스를 즉시 끝냅니다(하드 OOM). 이때는 register_shutdown_function()에 등록한 함수가 실행되지 않고, 테스트 파일 기준으로 프로세스는 시그널 9로 종료된 것처럼 보입니다. FPM 로그에 워커가 종료 코드 137로 죽는 기록이 새로 보이면 이 경로를 의심할 수 있습니다.

패치 직후 보고된 회귀도 있습니다. PHP 버그 #24492(10월 21일)는 sprintf(), pack(), bin2hex() 등 일부 함수의 문제를 보고합니다. 이 함수들은 할당할 메모리 크기를 함수에 넘긴 인자로 정하고, 할당이 실패해도 호출한 쪽에 실패를 알리지 않고 그 자리에서 처리를 중단하는 방식의 할당(non-fallible allocation)을 사용합니다. 이 할당이 새 하드 한도를 넘으면 할당 크기가 작아도 프로세스 전체가 종료됩니다. 8.5.11에서는 잡을 수 있던 OOM 오류가 8.5.12와 8.4.27에서는 하드 OOM으로 프로세스를 죽인다는 내용입니다. 글을 쓰는 시점까지 메인테이너 답변은 확인하지 못했습니다. 큰 문자열을 다루는 코드가 있다면 패치 뒤에 메모리 사용량과 워커 종료 로그를 지켜봐야 합니다.

이번 릴리스에는 보안 수정 외에 정기 릴리스에 들어갈 일반 버그 수정이 함께 들어 있습니다. 8.5.12의 변경 로그에는 Core, Opcache, PDO, Intl, DOM 등 수십 건의 수정이 있고, pg_service() 함수가 제거되었습니다. 보안 패치만 골라 적용할 수 없으므로, 변경 영향 검토는 정기 업데이트와 같은 범위로 잡아야 합니다.


패치를 바로 올릴 수 없을 때 할 수 있는 일은 부분 완화뿐입니다

PHP는 업데이트만이 완전한 해결책이라고 밝혔습니다. 임시 조치는 특정 공격 경로를 줄일 뿐 설치를 안전하게 만들지 못합니다. watchTowr는 .ini 변경을 포함한 완화책을 익스플로잇을 막는 수단으로 이해하면 안 된다고 평가했고, GMO Flatt Security도 완화 설정을 모두 적용해도 모든 시나리오를 막지 못한다고 봤습니다.

조치줄어드는 경로한계
session.upload_progress.enabled=0 설정 후 모든 워커 재시작세션 업로드 진행률 경로다른 수명주기 경로는 막지 못합니다. 권고문은 요청별 재정의보다 시작 시 적용되는 php.ini, FPM 풀, 서버 설정을 우선하라고 했으므로 ini_set()과 .user.ini에 의존하면 안 됩니다
fatal_error_backtraces=0(PHP 8.5만) 설정 후 모든 워커 재시작확인된 mod_php RCE의 생산자 경로요청이 소유한 다른 상태는 막지 못합니다
memory_limit=-1을 관리자 설정으로 강제(FPM이라면 php_admin_value)하고 풀, 디렉터리, 애플리케이션 설정이 유한한 한도를 되살리지 않는지 확인CVE-2026-109618에서 확인된 OOM 유발 경로하위 메모리 할당이 실패하면 같은 경로가 열리고, 메모리 고갈 위험이 커집니다
post_max_size를 보수적으로 유지하고 프런트엔드에서 요청 크기, 입력 변수, 파트, 파트별 헤더, 업로드 수, max_input_nesting_level 제한multipart 파싱 단계의 OOM 유발Content-Length가 없는 요청까지 제한해야 하고 패치를 대체하지 못합니다
multipart를 사용하지 않는 서비스에서 multipart/form-data 요청 차단multipart를 이용한 OOM 유발업로드 기능이 있는 경로에는 적용할 수 없습니다
pm.max_requests=1워커가 여러 요청을 처리하며 상태를 이어 가는 조건성능 비용이 커서 성능이 문제가 되지 않을 때만 고려합니다
WAF(Cloudflare 등)알려진 익스플로잇 패턴시그니처 기반이라 우회 시도가 관측되었고, Cloudflare도 패치를 함께 권고했습니다

RLIMIT_AS와 RLIMIT_DATA로 메모리를 제한하면 mmap() 실패로 취약한 경로가 오히려 열릴 수 있으므로 사용하지 않아야 합니다. cgroup 같은 컨테이너 메모리 제한은 가용성 경계로 사용할 수 있지만, 워커 하나만 종료하는 정책이 아니면 마스터와 워커가 함께 죽을 수 있습니다.

; 패치 전 임시 조치이며 패치를 대체하지 못합니다
session.upload_progress.enabled = 0
; PHP 8.5에서만 적용합니다
fatal_error_backtraces = 0

패치한 뒤에도 점검해야 할 것들이 있습니다

점검 항목은 다섯 가지입니다.

  • 워커 재시작과 버전 확인: 업데이트 뒤에 모든 워커를 재시작하고, 실제 요청을 처리하는 프로세스가 수정 버전인지 확인합니다.
  • 로그와 설정 보존: 변경하기 전에 PHP와 웹 서버 로그, 설정 스냅샷, 의심스러운 파일을 보존해야 합니다.
  • 이상 징후 탐색: Wiz는 이 결함이 애플리케이션 계층 아래에서 실행되어 애플리케이션 로그에 남는 맥락이 적을 수 있다고 설명합니다. 비정상적인 multipart 요청 뒤에 메모리 한도 오류나 HTTP 500이 반복되는지, 비정상적으로 크거나 깨진 PHPSESSID 응답 쿠키가 있는지, 이유 없이 워커가 다시 뜨는지를 확인해야 합니다. Huntress는 PHP 워커(php-fpm, php-cgi, php-cgi.exe, httpd, apache2)가 셸, 인터프리터, 다운로드 도구를 자식 프로세스로 실행하는지를 보라고 안내합니다.
  • 웹 루트와 계정 점검: 새 웹셸, 예상하지 못한 cron 작업, 새로 생긴 계정이 있는지 확인합니다.
  • 시크릿 교체 판단: 로그에 남는 맥락이 적을 수 있고 메모리 노출은 RCE 없이도 가능하므로, 패치 전에 인터넷에 열려 있던 서버가 다룬 비밀번호, 토큰, API 키는 교체를 검토해야 합니다.

패치한 뒤에도 노출 면을 줄여 두어야 합니다. PHP는 사용하지 않는 업로드 진행률 기능을 패치 뒤에도 꺼 두는 것이 신중한 선택이라고 안내했습니다. post_max_size를 낮게 유지하고 디코딩된 요청 크기를 프런트엔드에서 제한하는 것도 패치 뒤의 서비스 거부 위험을 줄입니다. PHP를 내장한 어플라이언스와 패키지 제품은 NCSC-NL이 운영하는 제품 목록에서 확인할 수 있습니다.


이번 사건에서 눈에 띄는 패턴

이번 사건에서 반복해서 볼 수 있는 패턴은 다섯 가지입니다.

  • 실패 경로의 정리 누락과 워커의 전역 상태: 정상 경로는 요청마다 실행되어 검증되지만, OOM이나 최대 실행 시간 초과로 끊기는 경로는 드물게 실행되어 검증도 덜 됩니다. 이번 결함은 그 경로에서 정리 코드가 빠진 곳에서 나왔고, 워커가 요청 사이에 전역 상태를 이어 가는 구조가 그 영향을 다음 요청까지 전달했습니다. 애플리케이션 코드에서도 상태를 먼저 공개하고 나중에 초기화하는 순서는 오류 경로에서 같은 문제를 만들 수 있으므로, 코드 리뷰에서 그 순서를 점검 항목으로 둘 수 있습니다.
  • 중심부 결함의 수정 비용: 메모리 관리와 오류 처리는 거의 모든 요청이 지나가는 부분이어서, 수정안마다 호환성과 성능, 서드파티 확장 모듈 수정이라는 비용이 따랐다고 PHP 재단은 설명합니다. 업스트림 수정이 늦어질 때 운영자가 할 수 있는 일은 노출 면을 줄이는 것이고, 업로드 진행률 비활성화와 요청 크기 제한은 평소에도 적용해 둘 수 있습니다.
  • 지원이 끝난 런타임: Mallory가 2026년 10월 20일에 Shodan 배너로 집계한 결과, 버전이 확인되는 PHP 호스트 약 150만 대 중 59%(883,717대)가 공식 수정이 없는 8.1 이하였습니다. 한국은 버전이 확인되는 30,102대 중 24,591대(82%)였습니다. 다만 이 집계는 호스트 수이고 사이트 수가 아니며, 약 74만 대는 버전을 숨기고 있고 배포판 백포트도 반영되지 않았으므로 추정치입니다. VirusTotal의 집계에서도 PHP로 식별된 약 510만 IP 중 버전을 공개하는 곳은 약 110만 곳이었습니다. Cisco Talos도 공개 정보 기준으로 약 50%가 패치를 받지 못할 것으로 추정했습니다. 인벤토리에서 지원이 끝난 버전을 먼저 찾고, 이전하기 전까지는 인터넷 노출을 줄여야 합니다.
  • 조정된 공개의 취약성: 이번 공개 과정에서는 CVE 번호 유출, PoC 영상 게시, 수정이 포함된 이미지의 조기 공개가 이어졌습니다. PHP 재단은 릴리스를 하루 앞당겼고, 이미지를 먼저 공개한 쪽은 악용 시도를 보고 판단했다고 설명했습니다. 공개 시점은 운영자가 정할 수 없으므로, 패치 전에 PHP가 어디서 어떤 실행 방식으로 돌고 있는지, 긴급 변경을 누가 승인하는지, 업데이트를 어떻게 확인하는지를 알고 있어야 합니다.
  • AI와 익스플로잇 비용: Wiz는 익스플로잇을 환경마다 조정하는 비용이 AI로 크게 줄었다고 평가하며, 복잡해서 악용하기 어렵다는 가정이 약해졌다고 봅니다. Calif는 대상의 취약점 역사를 알고 질문한 쪽이 더 나은 결과를 냈다고 평가합니다. 두 평가는 모두 자기 보고이고 사례가 하나뿐이어서, 공격 비용의 변화를 일반화하려면 더 많은 사례가 필요합니다.

마무리

해야 할 일은 네 가지입니다.

  1. 웹 요청을 처리하는 PHP의 실제 버전과 실행 방식을 확인하고, 8.5.12, 8.4.27, 8.3.36, 8.2.35 이상으로 올립니다. 배포판 패키지는 배포판 공지로 수정 여부를 확인합니다.
  2. 업데이트 뒤에 실행 방식에 맞게 워커를 재시작하고, 이전 바이너리가 남은 프로세스가 없는지 확인합니다. 새 하드 메모리 한도 때문에 워커가 종료 코드 137로 죽는지도 며칠 지켜봅니다.
  3. 8.1 이하는 공급자의 백포트를 확인하거나 이전 일정을 잡고, 그 전까지는 인터넷 노출을 줄입니다.
  4. 노출 기간의 로그와 프로세스 이력을 점검하고, 시크릿을 교체할지 정합니다.

이 글은 2026년 10월 22일 오전을 기준으로 작성했으며, 악용 현황과 배포판 패키지 상태는 계속 바뀌고 있습니다.


약자 모음

  • CVE (Common Vulnerabilities and Exposures): 공개된 취약점에 부여하는 식별자
  • CVSS (Common Vulnerability Scoring System): 취약점 심각도를 0에서 10까지의 점수로 나타내는 표준
  • DLA (Debian LTS Advisory): Debian 장기 지원 팀이 내는 보안 공지
  • DoS (Denial of Service): 요청을 대량으로 보내거나 자원을 고갈시켜 서비스를 멈추게 하는 공격
  • DSA (Debian Security Advisory): Debian 보안팀이 내는 보안 공지
  • EOL (End Of Life): 공식 지원이 끝나 보안 수정을 더 받지 못하는 상태
  • FPM (FastCGI Process Manager): PHP를 별도 프로세스로 띄워 워커를 관리하는 실행 방식
  • GHSA (GitHub Security Advisory): GitHub가 보안 권고에 부여하는 식별자
  • KEV (Known Exploited Vulnerabilities): CISA가 실제 악용을 확인한 취약점을 모은 목록
  • LTS (Long-Term Support): 보안 수정을 오래 제공하는 장기 지원
  • OOM (Out-Of-Memory): 메모리 부족
  • PoC (Proof of Concept): 취약점이 동작함을 보이는 개념 증명 코드
  • RCE (Remote Code Execution): 원격에서 서버에 임의의 코드를 실행하는 공격
  • SAPI (Server API): PHP가 웹 서버와 맞물려 요청을 받는 실행 방식
  • UAF (Use-After-Free): 해제된 메모리를 다시 사용하는 오류
  • WAF (Web Application Firewall): HTTP 요청을 검사해 알려진 공격 패턴을 차단하는 방화벽

참고 자료

Footnotes

  1. 공지와 릴리스는 10월 21일 13:30 UTC 무렵에 나왔지만, php.net 변경 로그와 소스의 NEWS 파일에는 릴리스 날짜가 22 Oct 2026으로 적혀 있습니다. 이 글은 공지 시각을 기준으로 10월 21일로 적습니다. ↩