


1. PHP2Shell (CVE-2026-109618 & CVE-2026-109619) 개요
PHP는 소규모 웹사이트부터 대규모 상용 애플리케이션까지 폭넓게 사용되는 대표적인 Server-Side 스크립트 언어입니다. 일반적으로 웹 서버와 밀접하게 통합되어 동작하므로, PHP 런타임의 취약점은 특정 애플리케이션 프레임워크나 비즈니스 로직과 관계없이 다양한 서비스에 영향을 미칠 수 있습니다.
2026년 10월 21일, PHP 프로젝트는 CVE-2026-109618과 CVE-2026-109619를 공개했습니다.


PHP2Shell이라고도 불리는 이 두 취약점은 인증되지 않은 원격 공격자가 조작된 HTTP 요청을 통해 PHP 웹 워커의 권한으로 임의 코드를 실행할 수 있는 취약점입니다. 공격에 성공하면 웹 애플리케이션의 데이터가 노출되거나 변조될 수 있으며, 서버 내부의 다른 시스템으로 공격 범위가 확대될 가능성도 있습니다.
PHP2Shell은 특정 프레임워크나 플러그인이 아닌 PHP Core 자체에 존재하는 취약점입니다. 따라서 PHP를 사용하는 모든 서비스가 애플리케이션 구성과 관계없이 잠재적인 공격 대상이 될 수 있습니다. 이러한 광범위한 영향과 원격 코드 실행 가능성으로 인해 CVE-2026-109618과 CVE-2026-109619에는 각각 CVSS v4.0 기준 9.2점이 부여됐습니다.
PoC Video
신속하게 취약 여부를 점검해야 하는 경우에는 당사에서 무료로 제공하는 PHP2Shell 스캐너와 사용 방법을 다룬 6장부터 확인하시기 바랍니다.
이 글에서는 PHP2Shell의 Root Cause과 영향받는 환경을 분석하고, 필요한 대응 방안을 설명합니다.
2. 기술적 배경
PHP2Shell의 Root Cause을 이해하려면 PHP 내부의 메모리 관리 방식과 Zend Engine에서 데이터를 관리하는 방식을 먼저 이해해야 합니다.
2.1 요청 메모리와 프로세스 범위 상태
PHP는 HTTP 요청을 처리하는 동안 생성되는 문자열, 배열, 객체 등을 요청별 메모리에 저장합니다. 이 메모리는 하나의 요청이 끝날 때 해제됩니다.
반면 Apache mod_php나 PHP-FPM에서는 하나의 프로세스가 여러 HTTP 요청을 처리하므로, 요청이 끝난 뒤에도 프로세스와 일부 내부 상태가 유지됩니다. 여기서 내부 상태란 PHP 코드의 $GLOBALS가 아니라 PHP Core와 확장 모듈이 C 레벨에서 관리하는 값을 의미합니다.
따라서 PHP 내부의 데이터는 하나의 요청에서만 유효한 request-scoped data와 요청이 끝난 이후에도 유지되는 process-scoped state로 구분할 수 있습니다.
2.2 Zend 엔진과 핵심 자료구조
Zend Engine은 PHP 코드를 해석하고 실행하는 PHP의 핵심 구성 요소입니다. PHP 코드를 opcode로 변환하고 실행하며, 함수 호출, 변수와 객체의 관리, 메모리 할당 및 오류 처리 등 PHP 런타임의 주요 기능을 담당합니다.
또한 PHP에서 사용되는 값을 zval 자료구조로 표현합니다. zval에는 실제 값 또는 다른 자료구조를 가리키는 포인터와 해당 값의 종류를 나타내는 타입 정보가 저장됩니다.
PHP의 배열과 여러 내부 심볼 테이블은 HashTable로 구현됩니다. HashTable의 각 요소는 Bucket에 저장되며, Bucket은 값에 해당하는 zval, 키의 해시값, 문자열 키를 가리키는 포인터로 구성됩니다.
HashTable은 nNumUsed를 통해 순회할 Bucket 영역의 범위를 관리하고, nNumOfElements를 통해 현재 유효한 요소의 수를 관리합니다. HashTable이 해제될 때 Zend Engine은 nNumUsed를 기준으로 Bucket 영역을 순회합니다. 이 과정에서 유효한 zval을 타입 정보와 HashTable에 등록된 pDestructor에 따라 정리합니다.
2.3 메모리 부족(OOM)과 Bailout
PHP는 memory_limit을 통해 하나의 요청이 사용할 수 있는 메모리의 최대 크기를 제한합니다.
시스템에 사용 가능한 물리 메모리가 남아 있더라도 특정 요청이 설정된 memory_limit을 초과하면 PHP 내부에서 OOM(out-of-memory) 오류가 발생할 수 있습니다.
Zend Engine은 치명적 오류가 발생했을 때 실행 흐름을 중단하기 위해 bailout 메커니즘을 사용합니다. Bailout은 C 함수들이 오류를 상위 호출자에게 차례로 반환하는 대신, 미리 지정된 복구 지점으로 실행 위치를 직접 이동시키는 방식입니다.
Zend Engine의 zend_try는 setjmp()를 사용하여 현재 실행 위치를 복구 지점으로 등록합니다. 이후 하위 함수에서 zend_bailout()이 호출되면 longjmp()를 통해 가장 최근에 등록된 복구 지점으로 이동하며, 실행은 해당 zend_catch 영역에서 계속됩니다.
이 과정에서 현재 함수부터 복구 지점 사이에 있는 함수들은 일반적인 return 과정을 거치지 않습니다. 따라서 각 함수에 남은 코드는 실행되지 않으며, 심지어 함수 반환 전에 수행되어야 할 정리 과정까지도 건너뛰게 됩니다.
다음 장에서는 이러한 메모리 관리 방식과 자료구조가 각 취약점의 근본 원인과 어떻게 연결되는지 분석합니다.
3. 근본 원인 분석 Part 1: CVE-2026-109618
이 장에서는 PHP2Shell의 두 취약점 중 CVE-2026-109618의 root cause를 먼저 분석합니다.
PHP가 공개한 보안 권고에서는 이 취약점을 다음과 같이 설명합니다.

PHP는 OOM(out-of-memory) 오류가 발생하면
longjmp()를 사용해 현재 실행 문맥을 벗어나고 요청 처리를 중단합니다. 일부 코드 경로에서는 요청 메모리가 완전히 초기화되기 전에 일시적으로 접근 가능한 상태가 됩니다. 이 시점에 메모리 할당이 실패하면 PHP가 조기 종료 상황을 적절히 처리하지 못해 요청 메모리가 유효하지 않은 상태로 남을 수 있습니다. 이때 실패한 메모리 할당은 유효하지 않게 된 메모리와 직접 관련되어 있을 필요가 없습니다. 이후 요청 종료 과정에서 PHP가 해당 메모리를 읽거나 그 값을 기준으로 분기할 수 있습니다. 이러한 상태는 여러 OOM 유발 방식으로 안정적으로 재현할 수 있으며, 원격 코드 실행으로 이어질 수 있습니다.
이를 바탕으로 CVE-2026-109618의 성립 조건을 정리해보면 다음과 같습니다.
메모리가 완전히 초기화되기 전에 일시적으로 접근 가능한 상태가 됩니다.
1의 상태가 유지되는 동안 PHP에서 메모리 할당이 수행됩니다.
2의 할당 과정에서 OOM이 발생하고,
longjmp()로 인해 현재 실행 흐름이 중단됩니다.3으로 인한 조기 종료 과정을 PHP가 적절히 처리하지 못해 요청 메모리가 불완전한 상태로 남습니다.
이후의 요청 종료 과정에서 불완전한 상태로 남은 메모리를 읽거나 이를 기준으로 분기하면서 메모리 손상이 발생할 수 있습니다.
다음으로 패치 전의 취약 코드를 살펴보며, 위 조건들이 실제 코드에서 어떻게 형성되고 메모리 손상으로 이어지는지 분석하겠습니다.
3.1 멀티파트 입력 등록
앞서 정리한 성립 조건을 코드 흐름에서 확인하려면 먼저 Multipart 요청이 PHP 내부 자료구조에 등록되는 과정을 살펴봐야 합니다.
PHP는 POST 방식의 multipart/form-data 요청을 애플리케이션 코드가 실행되기 전에 자동으로 해석합니다. 요청 초기화 과정에서 Content-Type에 대응하는 POST 핸들러를 선택하며, multipart/form-data에는 rfc1867_post_handler()가 등록되어 있습니다.
이 핸들러는 Multipart 요청에 포함된 일반 필드와 업로드 파일 정보를 해석하여 $_POST와 $_FILES를 구성합니다. 특히 파일 입력 하나에 대해서도 tmp_name, error, size 등의 여러 항목을 생성하며, 중첩된 필드 이름은 여러 단계의 PHP 배열로 변환됩니다. 생성된 값은 최종적으로 php_register_variable_ex()를 통해 Zend Engine의 HashTable에 등록됩니다.
Multipart 요청의 필드 이름과 중첩된 배열 구조는 HashTable의 문자열 키로 등록됩니다. 또한 다수의 헤더, 빈 파일 입력 및 중첩된 필드 이름은 요청 처리 과정의 메모리 사용량을 크게 증가시킬 수 있습니다.
공격자는 이러한 특성을 이용해 PHP가 입력값을 등록하는 도중 OOM을 발생시키고 원격에서 취약점을 트리거할 수 있습니다.
PHP 코드의 main/php_variables.c의 php_register_variable_ex()는 file[metadata][name]과 같은 입력 이름을 해석하면서 각 인덱스에 대응하는 중첩 배열을 생성합니다. 처리 중 현재 HashTable에 해당 인덱스가 존재하지 않으면 새로운 배열을 생성하고 zend_symtable_str_update_ind()를 통해 이를 HashTable에 삽입합니다.
따라서 공격자가 Multipart 필드 이름에 지정한 중첩 구조는 새로운 배열과 문자열 키를 반복적으로 생성하는 HashTable 삽입 연산으로 변환됩니다. 문자열 키를 삽입하는 경우 내부적으로 _zend_hash_str_add_or_update_i()가 호출됩니다.
3.2 불완전한 버킷 초기화
실제 문제가 발생하는 지점은 _zend_hash_str_add_or_update_i()가 새로운 Bucket을 추가하는 과정입니다.
이 코드는 새로운 Bucket의 인덱스를 결정하면서 nNumUsed와 nNumOfElements를 먼저 증가시킵니다. 이 시점부터 새로운 Bucket은 HashTable이 관리하고 순회해야 하는 범위에 포함됩니다.
그러나 Bucket의 key, h, val은 아직 초기화되지 않았습니다. 즉, 새로운 Bucket이 HashTable의 관리 대상에는 포함됐지만 실제 내용은 완성되지 않은 상태이며, 앞서 정리한 첫 번째 성립 조건이 여기서 만들어집니다.
이후 zend_string_init()은 Bucket에 저장할 문자열 키를 생성하기 위해 메모리를 할당합니다. 따라서 이 코드가 앞서 정리한 두 번째 성립 조건에 해당합니다.
정상적인 경우에는 zend_string_init() 이후의 코드가 실행되면서 Bucket 초기화가 완료됩니다. 하지만 문자열 키를 할당하는 도중 OOM이 발생하면 bailout으로 인해 현재 실행 흐름이 중단됩니다. 함수는 일반적인 반환 과정을 거치지 않으므로 증가한 nNumUsed가 복구되지 않고, p->val을 포함한 나머지 Bucket 필드도 초기화되지 않습니다.
3.3 요청 종료 시 유효하지 않은 버킷 처리
OOM으로 bailout이 발생하면 현재 함수의 나머지 코드는 실행되지 않습니다. 그러나 Apache mod_php는 요청 초기화가 실패한 경우에도 php_request_shutdown()을 호출하여 요청 종료 절차를 수행합니다.
요청 종료 과정에서 PHP는 $_POST, $_FILES를 포함한 HTTP 전역 변수를 순서대로 제거합니다.
$_FILES는 배열 타입의 zval이므로, 이를 제거하는 과정은 최종적으로 zend_array_destroy()로 이어집니다. 이 함수는 nNumUsed를 기준으로 HashTable에서 순회할 Bucket의 끝을 계산합니다.
앞선 HashTable 삽입 과정에서는 새로운 Bucket을 초기화하기 전에 nNumUsed가 증가했습니다.
따라서 OOM으로 Bucket 초기화가 중단되었더라도 zend_array_destroy()가 계산한 순회 범위에는 해당 Bucket이 포함됩니다.
HashTable의 상태에 따라 zend_array_destroy()는 불완전한 Bucket의 val을 정리하거나, val의 타입이 IS_UNDEF인지 확인합니다. 문자열 키를 사용하는 경우에는 key도 검사하고 해제합니다.
하지만 해당 Bucket의 val과 key는 OOM 발생 시점에 아직 초기화되지 않았습니다. PHP는 초기화되지 않은 데이터를 zval의 타입 정보나 포인터로 오인하여 읽거나, 그 값을 기준으로 정리 함수를 호출할지 결정하게 됩니다.
불완전한 val이 문자열, 배열, 객체와 같은 참조형 타입으로 해석되면 i_zval_ptr_dtor()는 그 안의 포인터를 유효한 주소로 간주하고 참조 횟수를 변경하거나 타입별 정리 함수를 호출할 수 있습니다. 마찬가지로 초기화되지 않은 key가 유효한 포인터처럼 보이면 zend_string_release_ex()가 해당 주소를 문자열 객체로 처리하게 됩니다.
결과적으로 CVE-2026-109618은 다음과 같이 원인을 정리할 수 있습니다.
4. 근본 원인 분석 Part 2: CVE-2026-109619
이제 PHP2Shell의 두 번째 취약점인 CVE-2026-109619의 root cause를 분석합니다.
PHP가 공개한 보안 권고에서는 이 취약점을 다음과 같이 설명합니다.

PHP에는 오류, 세션, 객체 및 확장 모듈과 관련된 여러 global state가 존재합니다. 이러한 상태는 하나의 프로세스에서 여러 요청에 걸쳐 유지될 수 있지만, 그중 일부는 개별 요청에 속한 메모리를 가리킵니다. 따라서 요청 메모리가 해제되기 전에 해당 상태를 반드시 초기화해야 합니다.
그러나 일부 입력 처리, 설정 적용, 세션 및 오류 처리 작업은 다음 요청의 일반적인 초기화 절차보다 먼저 실행됩니다. 이 과정에서 이전 요청이 남긴 stale state가 초기화되기 전에 사용되면 use-after-free가 발생할 수 있습니다.
이러한 상태는 여러 원인으로 남을 수 있습니다. 메모리 고갈을 포함한 치명적 오류가 작업을 중단시켜 상태 초기화를 건너뛸 수 있으며, 경고와 같은 치명적이지 않은 진단 메시지가 종료 과정에서 이미 초기화된 오류 상태를 다시 채울 수도 있습니다. 이후 요청 메모리는 해제되지만 프로세스에 남은 global state는 계속 해당 메모리를 가리키고, 다음 요청의 초기화 이전에 실행되는 작업이 이를 다시 사용할 수 있습니다.
이를 바탕으로 CVE-2026-109619의 성립 조건을 정리하면 다음과 같습니다.
하나의 요청에서 생성된 상태가 이후에도 접근 가능한 global state에 저장됩니다.
요청이 종료되기 전에 해당 상태가 초기화되지 않거나, 초기화 이후 다른 작업이 다시 값을 저장합니다.
요청 메모리는 해제되지만 프로세스에 남은 global state는 계속 해당 메모리를 가리킵니다.
동일한 프로세스가 다음 HTTP 요청을 처리합니다.
일반적인 요청 초기화가 stale state를 초기화하기 전에 다른 작업이 해당 상태를 읽거나 해제합니다.
결과적으로 이전 요청에서 유효했던 포인터가 다음 요청에서 해제된 메모리를 가리키는 dangling pointer가 되며, 이를 다시 사용하는 과정에서 UAF(use-after-free)가 발생합니다.
CVE-2026-109619는 오류, 세션, 객체 및 확장 모듈의 여러 내부 상태에서 발생할 수 있습니다.
다음으로 PHP 보안 권고에서 확인된 경로 중 session upload progress를 통해 session state가 요청 경계를 넘어 남는 과정을 중심으로 살펴보겠습니다.
4.1 멀티파트 파싱 중 세션 상태 초기화
CVE-2026-109619의 session 경로 역시 Multipart 요청 처리 과정에서 시작됩니다.
PHP의 session upload progress 기능은 파일 업로드 진행률을 세션에 저장하기 위해 Multipart 파서의 이벤트를 전달받습니다.
이 기능은 기본적으로 활성화되어 있으며, Multipart 요청에 PHP_SESSION_UPLOAD_PROGRESS 필드와 유효한 세션 ID가 포함되면 php_session_rfc1867_callback()이 업로드 진행률 추적을 시작합니다.
이 콜백은 애플리케이션 코드뿐만 아니라 Session 모듈의 일반적인 요청 초기화보다 먼저 실행될 수 있습니다. 따라서 필요한 Session 상태를 콜백 내부에서 직접 초기화합니다.
php_rinit_session(0)은 Session 모듈의 요청별 상태를 초기화하고, 이어지는 코드는 요청에서 확인한 세션 ID를 PS(id)에 저장합니다. PS()는 Session 모듈이 C 레벨에서 관리하는 global state이며 프로세스의 수명 동안 유지되지만, PS(id)가 가리키는 zend_string은 현재 요청의 메모리에 할당됩니다.
업로드 진행률을 세션에 기록하는 과정은 다음 호출 경로를 통해 Session state를 초기화합니다.
php_session_track_init()은 $_SESSION에 해당하는 배열을 생성하고, 이를 PS(http_session_vars)에 저장합니다.
이 과정에서 새로 생성된 세션 배열과 zend_reference는 현재 요청의 메모리에 할당되지만, 이를 가리키는 값은 Session global인 PS(http_session_vars)에 저장됩니다.
따라서 session upload progress가 실행되면 일반적인 Session 모듈 초기화 이전에 PS(id)와 PS(http_session_vars)가 현재 요청의 메모리를 가리키게 됩니다.
정상적인 요청에서는 이 상태가 Multipart 처리 또는 Session 모듈 종료 과정에서 정리되지만, 문제는 정리 전에 요청 처리가 중단되는 경우에 발생합니다.
4.2 요청 중단과 잔존 전역 상태
정상적인 경우 MULTIPART_EVENT_END에서 php_rshutdown_session_globals()를 호출하여 Session state를 정리하고, 업로드 진행률 데이터를 해제합니다.
php_rshutdown_session_globals()는 PS(http_session_vars)와 PS(id)가 가리키는 요청 메모리를 해제한 뒤 각 값을 초기화합니다.
문제는 Session state가 생성된 이후 MULTIPART_EVENT_END에 도달하기 전에 OOM이나 다른 치명적 오류가 발생하는 경우입니다. Bailout으로 Multipart 처리가 즉시 중단되면 위 정리 코드는 실행되지 않습니다.
Apache mod_php에서 Multipart 요청은 php_hash_environment()에서 처리됩니다. 확장 모듈의 일반적인 요청 초기화는 그다음의 zend_activate_modules()에서 시작되며, 초기화가 완료된 이후에야 PG(modules_activated)가 설정됩니다.
Multipart 처리 중 bailout이 발생하면 zend_activate_modules()에 도달하지 못하므로 PG(modules_activated)는 설정되지 않습니다.
요청 종료 과정은 이 값을 확인하여 확장 모듈의 RSHUTDOWN 호출 여부를 결정합니다.
따라서 Session 모듈의 RSHUTDOWN도 실행되지 않습니다.
PHP는 이후 요청 종료 절차를 계속 진행하고 shutdown_memory_manager()를 통해 요청 메모리를 해제합니다.
그 결과 PS(id)와 PS(http_session_vars)가 가리키던 메모리는 해제되지만, Session global에 저장된 포인터 값은 초기화되지 않습니다. 요청이 종료된 이후 두 값은 해제된 요청 메모리를 가리키는 dangling pointer로 남습니다.
4.3 다음 요청에서의 잔존 상태 재사용
요청 메모리가 해제된 이후에도 Apache worker는 종료되지 않고 다음 HTTP 요청을 처리할 수 있습니다. 이때 다음 요청이 동일한 worker에 전달되면 이전 요청에서 남은 Session global도 그대로 유지됩니다.
일반적으로 Session 모듈의 RINIT이 실행되면 php_rinit_session_globals()가 PS(id)와 PS(http_session_vars)를 초기화합니다. 그러나 session upload progress는 일반적인 모듈 초기화보다 먼저 실행되므로, 남아있는 state가 초기화되기 전에 접근할 수 있습니다.
두 번째 Multipart 요청에 세션 ID와 PHP_SESSION_UPLOAD_PROGRESS 필드는 포함되어 있지만 파일 입력이 없다면 MULTIPART_EVENT_FILE_START가 발생하지 않습니다.
따라서 FILE_START의 php_rinit_session(0)을 건너뛴 채 MULTIPART_EVENT_END에 도달하고, 기존 Session global을 초기화하지 않은 상태에서 php_session_rfc1867_cleanup()을 호출합니다.
php_session_rfc1867_cleanup()은 Session global을 먼저 초기화하지 않고 바로 php_session_initialize()를 호출합니다.
php_session_initialize()은 PS(id)가 NULL이 아니면 이를 현재 요청의 유효한 세션 ID로 간주하고, ZSTR_VAL(PS(id))[0]을 통해 문자열 데이터를 읽습니다. 그러나 PS(id)는 이전 요청에서 해제된 zend_string을 가리키는 dangling pointer이므로, 이 과정에서 해제된 메모리에 접근하며 UAF가 발생합니다.
이 시점에서 이미 UAF는 성립하지만, Session global에는 또 다른 stale state가 남아 있습니다. 이후 호출되는 php_session_track_init()은 PS(http_session_vars)가 IS_UNDEF가 아니면 기존 값에 zval_ptr_dtor()를 호출해 정리를 시도합니다.
하지만 PS(http_session_vars) 역시 이전 요청에서 해제된 zend_reference를 가리키고 있습니다. zval_ptr_dtor()는 이 값을 유효한 zval로 간주하여 참조 횟수를 변경하거나 내부 값을 제거하려고 하므로 UAF가 발생합니다.
결과적으로 CVE-2026-109619은 다음과 같이 원인을 정리할 수 있습니다.
5. 대응 방안
PHP 프로젝트는 PHP로 웹 요청을 처리하는 모든 환경이 이번 취약점의 영향 대상이 될 수 있다고 안내하고 있습니다. PHP-FPM, Apache mod_php, php-cgi, FrankenPHP 및 LiteSpeed를 포함하여 외부에 공개된 PHP 기반 서비스를 우선적으로 식별하고 보안 업데이트를 적용해야 합니다.
5.1 영향받는 시스템 및 버전
운영 중인 서비스를 비롯해 도입한 제품과 솔루션 등 인터넷에 공개된 모든 자산을 대상으로 PHP 사용 여부를 확인해야 합니다. 자산 관리 목록에만 의존하지 않고 실제로 동작 중인 서비스를 기준으로 점검하는 것이 중요합니다.
뿐만 아니라 PHP를 호스팅 업체나 제품 공급업체가 관리하는 경우에는 해당 업체에 보안 업데이트 적용 여부를 확인해야 합니다.
PHP 프로젝트가 발표한 수정 버전은 다음과 같습니다.
PHP Branch | Affected Version | Patched Version |
|---|---|---|
PHP 8.5 | 8.5.11 이하 | 8.5.12 |
PHP 8.4 | 8.4.26 이하 | 8.4.27 |
PHP 8.3 | 8.3.35 이하 | 8.3.36 |
PHP 8.2 | 8.2.34 이하 | 8.2.35 |
PHP 8.1 이하는 정규 보안 지원이 종료됐으므로 지원되는 버전으로 전환해야 합니다. PHP 8.2도 2026년 12월 31일 보안 지원이 종료될 예정이므로 가능한 경우 PHP 8.3 이상으로 전환하는 것이 권장됩니다.
Linux 배포판과 LTS 공급업체는 기존 PHP 버전 번호를 유지한 상태로 보안 패치를 백포트할 수 있습니다. 이 경우 버전 문자열만 비교하지 말고 해당 배포판이나 제품 공급업체의 보안 공지와 패키지 변경 이력을 함께 확인해야 합니다.
5.2 보안 업데이트 적용
인터넷에 공개된 서버를 우선하여 다음 버전 이상으로 업데이트해야 합니다.
PHP 8.5.12
PHP 8.4.27
PHP 8.3.36
PHP 8.2.35
업데이트 이후에는 PHP-FPM, Apache mod_php 및 php-cgi 등 PHP를 실행하는 모든 서비스를 재시작해야 합니다. 서비스를 재시작하지 않으면 메모리에 로드된 이전 PHP 코드가 계속 사용될 수 있습니다.
컨테이너 환경에서는 수정된 PHP 기반 이미지로 애플리케이션 이미지를 다시 빌드하고 기존 컨테이너를 재배포해야 합니다. 업데이트가 끝난 후에는 실제 웹 요청을 처리하는 PHP 런타임의 버전이 변경됐는지 다시 확인해야 합니다.
서버에서 실행한 php -v는 CLI 환경의 PHP를 가리킬 수 있으므로, Apache나 PHP-FPM이 사용하는 버전과 일치하는지 확인해야 합니다.
5.3 임시 완화 조치
즉시 업데이트하기 어려운 경우에는 다음 임시 조치를 적용하여 확인된 공격 경로의 노출을 줄일 수 있습니다.
PHP 8.5 환경에서는 다음 설정을 추가할 수 있습니다.
memory_limit=-1은 애플리케이션이 변경할 수 없도록 php.ini, PHP-FPM pool의 php_admin_value 또는 웹 서버 관리자 설정을 통해 적용해야 합니다. 이 설정은 요청별 메모리 제한을 제거하므로, 컨테이너나 cgroup을 통해 전체 메모리 사용량을 별도로 제한해야 합니다.
post_max_size는 서비스에 필요한 최소 크기로 설정해야 합니다. PHP 프로젝트의 보안 권고문에서는 파일 업로드가 필요하지 않은 경우 8M, 파일 업로드가 필요한 경우 32M 수준을 예시로 제시하고 있으며, upload_max_filesize는 post_max_size보다 작게 유지할 것을 권고하고 있습니다.
웹 서버, 리버스 프록시 또는 WAF에서도 요청 크기와 multipart part, 입력 필드, header 및 중첩 깊이를 제한해야 합니다. 파일 업로드 기능이 필요하지 않은 서비스라면 multipart 요청을 PHP에 전달하기 전에 차단할 수 있습니다.
이러한 설정은 특정 공격 경로만 제한하며 취약점을 완전히 해결하지는 않습니다. 설정 변경 후에는 모든 PHP 요청 처리 프로세스를 재시작하고 가능한 한 빠르게 보안 업데이트를 적용해야 합니다.
6. PHP2Shell 스캐너 공개
해당 취약점의 세부사항이 공개되기 전까지 PoC라고 주장하는 코드들이 많이 배포되었지만, 대다수의 코드들은 동작하지 않는 것으로 확인되었습니다. 또한, 인터넷에 공개된 출처 불명의 PoC에는 악성 행위가 포함될 수 있으며, PHP2Shell의 동적 검증 과정 자체도 OOM과 PHP 프로세스의 비정상 종료를 유발할 수 있습니다.
실운영 환경에서는 출처 불명의 PoC를 직접 실행하지 않고 실제 PHP 런타임의 버전과 공급업체의 패치 적용 여부를 기준으로 취약 여부를 판단해야 합니다. 기술적 검증이 필요한 경우에는 업무망과 분리된 테스트 환경에서만 수행하는 것이 권장됩니다.
엔키화이트햇은 OFFen ASM(공격표면관리) 솔루션과 PHP2Shell 긴급 스캐너를 제공하고 있으며, 이를 통해 해당 취약점을 안전하고 쉽게 검증할 수 있습니다.
OFFen ASM PHP2Shell 취약점 스캐너

7. 참고 자료

Popular Articles
More Articles







