# [PHP 취약점 기술 설명] PHP2Shell (CVE-2026-109618 / CVE-2026-109619) 가 왜 원격 코드 실행 (RCE) 으로 이어지는지

> https://bookfactory.kr/board/news/23357
> 게시판: 뉴스
> 작성자: admin
> 작성일: 2026-10-23T06:52:07.407Z

---

## 서론

이 글에서는 2 일 전 문제가 된 PHP2Shell 취약점에 대해 상세히 설명합니다. (※엔지니어를 위한 기술 글입니다.)

먼저, 2026 년 10 월 21 일 PHP 는 인증 없이 원격으로 코드를 실행할 수 있게 하는 중대한 취약점 2 개를 수정했습니다. 대상은 다음 2 개입니다.

- CVE-2026-109618: 메모리 부족 (OOM) 처리로 인한 요청 상태 파손
- CVE-2026-109619: 요청 종료 후에도 남아있는 글로벌 상태가 해방된 메모리를 참조하는 취약점

두 버전 모두 CVSS v4.0 점수가 9.2(중요)입니다. PHP 공식 보안 패치는 PHP 8.5.12, 8.4.27, 8.3.36, 8.2.35 버전에서 배포되었습니다.

이 글에서는 단순히 '메모리 파괴가 발생한다'고만 설명하지 않고, 문제의 논리를 다음 순서대로 추적합니다.

- PHP 의 요청 처리에서 메모리 부족이 공격자로부터 유발되는 이유
- 처리 중간에 중단되었을 때, 왜 '불완전한 데이터'가 후처리 대상이 되는지
- 왜 한 번의 요청으로 끝나지 않고 후속 요청에도 영향을 미치는지
- 그 상태가 어떻게 정보 유출이나 원격 코드 실행 (RCE) 으로 발전할지

> 
> 
> 이것은 원리에 대한 설명입니다. 실제 환경에서 재현하기 위한 공격 페이로드나 RCE 를 위한 구체적인 단계는 다루지 않습니다.
> 

## 영향 범위

PHP 공식 설명에서는 웹 요청을 처리하는 PHP 환경을 대상으로 합니다. PHP-FPM, Apache 의 `mod_php`, php-cgi, FrankenPHP, LiteSpeed 등이 이에 포함됩니다. 특히 `mod_php` 의 경우 애플리케이션의 PHP 코드 실행 전 요청 시작 단계만으로도 공격 조건을 구성할 수 있다는 것이 확인되었습니다.

수정판은 다음과 같습니다.

- PHP 8.5 시리즈: 8.5.12 이후
- PHP 8.4 시리즈: 8.4.27 이후 버전
- PHP 8.3 시리즈: 8.3.36 이후 버전
- PHP 8.2 시리즈: 8.2.35 이후 버전

PHP 8.1 이전 버전은 PHP 프로젝트의 일반적인 지원 대상이 아닙니다. OS 디스트리뷰션이나 LTS 벤더가 수정 사항을 백포팅할 수 있으므로, 단순한 버전 번호뿐만 아니라 현재 사용 중인 패키지의 제공자가 대응하고 있는지 반드시 확인해야 합니다.

## 2. 공통된 근본 원인: 비국소적 중단과 사후 처리

이번 두 사례의 공통점은 PHP 가 요청 처리 중, 일반적인 함수의 반환 방식이 아닌 비국소적으로 처리를 중단한다는 점입니다.

PHP 는 메모리 부족과 같은 치명적인 오류 상태로부터 탈출하기 위해 `longjmp()` 를 사용했습니다. 이는 일반적인 `return` 과 달리 각 함수가 순차적으로 복귀하는 것이 아니라, 미리 설정된 탈출 지점까지 제어 흐름을 즉시 이동시키는 메커니즘입니다.

이 구조 자체는 즉시 취약점으로 간주되지 않지만, 다음과 같은 순서가 되면 위험합니다.

즉, 문제는 단순히 '메모 할당 실패'라는 점만이 아닙니다. 실패 발생 시 부분적으로 생성된 상태를 무효화하는 처리와 후처리가 안전하게 건너뛸 수 있는 메커니즘이 부족했다는 것이 본질입니다.

## 3. CVE-2026-109618: OOM 으로 인해 처리되지 않은 HashTable 이 후속 처리 단계로 전달됨

### 3.1 공격자가 OOM 을 유발할 수 있는 이유

PHP 의 `memory_limit` 은 PHP 내부 메모리 사용량을 제한하는 반면, `post_max_size` 는 수신된 데이터의 와이어상 크기를 제한합니다.

이 두 가지는 동일한 제한을 받지 않습니다. 예를 들어 multipart 형식의 POST 요청에서는 짧은 입력에서도 PHP 내부에서 많은 `HashTable`, `Bucket`, 문자열 키 등이 생성될 수 있습니다. 네스트된 필드명이나 빈 파일 컨트롤을 많이 포함하면 HTTP 프로토콜 상의 데이터 양에 비해 PHP 내부에서 필요한 메모리 양이 크게 증가할 수 있습니다.

결과적으로 요청 전체가 `post_max_size` 범위 내에 있다고 보아도 PHP 내부 처리에서는 `memory_limit`에 도달할 수 있습니다.

### 3.2 해시 테이블의 '공개가 너무 빠르다'라는 문제

연구자들의 분석에 따르면, 해시 테이블에 요소를 추가하는 과정에서 먼저 요소 개수를 증가시키고, 이후 키 문자열 등을 할당했습니다. 이를 개념화하면 다음과 같은 순서가 됩니다. 2

원래는 마지막까지 초기화가 완료된 후 '유효한 요소'로 처리해야 하지만, 요소 수가 먼저 증가해 있습니다. 이후 `allocate_key()` 가 OOM 으로 중단되면 다음과 같은 상태가 남게 됩니다.

요청 시작 처리가 정상적으로 실패를 반환하고 해당 객체를 후처리 대상에서 제외할 수 있다면 문제를 억제할 수 있습니다. 그러나 Apache 의 `mod_php`에서는 요청 시작이 실패하더라도 요청의 shutdown 처리가 호출됩니다. shutdown 단계에서는 증가된 요소 수를 신뢰하여 완료되지 않은 Bucket까지 유효한 요소로 스캔해버립니다.

### 3.3 그 결과, 메모리 파괴가 발생하는 이유는 무엇인가

해시 테이블의 폐기 처리는 개념적으로 다음과 같이 동작합니다.

`nNumUsed`만 보면 해당 범위의 버킷이 완료된 것처럼 보이지만, 실제로는 OOM으로 인해 `key`나 `val`의 초기화가 아직 완료되지 않았을 수 있습니다.

그런 다음 폐기 처리는 다음과 같은 위험한 작업을 수행합니다.

- 초기화되지 않은 포인터를 해제합니다.
- 우연히 존재하는 값을 포인터로 처리합니다.
- 해방 대상 주소와 크기를 잘못 인식함
- 힙의 다른 영역을 파괴한다

이것은 단순한 크래시가 아니라 메모리 파괴 원시 함수가 될 수 있습니다. 공격자는 힙 상의 배치나 재사용 방식을 조정하여 처리 시스템이 참조하는 데이터를 일정 부분 제어하고, 궁극적으로 제어 흐름이나 함수 포인터 등에 영향을 미칩니다.

### 3.4 SAPI 의 차이점

이 경로는 모든 SAPI 에서 동일하게 성립하지는 않습니다.

`mod_php` 의 경우 요청 시작이 실패하더라도 Apache 측의 요청 디스트럭터가 정상적인 요청 종료로 진행되므로, 완료되지 않은 데이터가 폐기 처리에 전달됩니다. 반면 PHP-FPM 은 요청 시작 실패 시 자식 프로세스를 종료하는 경로를 가지며, 동일한 완료되지 않은 해시 테이블을 그대로 소비하는 것을 방지합니다.

다만, 이는 'FPM 이라면 안전하다'는 의미는 아닙니다. 애플리케이션 처리나 다른 상태 관리 방식을 사용하는 다른 경로가 남아있을 수 있습니다. PHP 공식 문서 또한 웹 요청을 처리하는 전체 환경을 대상으로 안내하고 있습니다.

## 4. CVE-2026-109619: 요청 간 횡단을 일으키는 Use-After-Free 취약점

### 4.1 PHP 워커는 여러 요청을 처리합니다.

PHP-FPM 또는 `mod_php` 의 워커는 일반적으로 한 번의 HTTP 요청마다 프로세스를 새로 생성하지 않고, 동일한 워커 프로세스가 여러 요청을 처리합니다.

요청 고유 데이터는 요청 종료 시 해제됩니다. 반면, 워커에 유지되는 글로벌 구조체나 확장 기능의 상태는 다음 요청에서도 유지됩니다.

여기서는 글로벌 변수가 요청 A 의 힙 상의 데이터를 계속 참조하고 있다가, 요청 A 의 종료 처리에서 해당 힙이 해제되면 다음과 같은 상태가 됩니다.

이러한 경우, 해제된 메모리를 가리키는 포인터를 뮝글링 포인터(dangling pointer)라고 합니다. 해제 후 해당 포인터를 사용하는 경우를 Use-After-Free(UAF)라고 합니다. 4

### 4.2 프로듀서와 소비자

이 취약성은 단일 처리로 해결되는 것이 아니라, 다음 두 가지 역할이 결합되어 발생합니다.

- 프로듀서: 요청의 고유 상태를 참조 가능한 글로벌 영역에 저장하는 처리
- 사용자: 다음 요청은 초기화 이전의 해당 상태를 읽거나 해제하거나 분기 처리에 사용하는 것입니다.

설립 요건을 정리하면 다음과 같습니다.

### 4.3 세션 업로드 진행 상황 예시

PHP 의 Session Upload Progress 는 multipart upload 의 진행 상황을 기록하기 위한 기능입니다. 중요한 것은 애플리케이션이 일반적인 `session_start()` 를 호출하기 전에, 즉 PHP 가 요청 바디를 파싱하는 단계에서 Session 관련 상태를 접근할 수 있다는 점입니다.

개념적 흐름은 다음과 같습니다.

다음으로, 동일한 워커가 요청 B 를 받습니다. 요청 B 가 멀티파트 처리 초기 단계에서 세션 업로드 진행 상황 클리닝으로 진행되면, 일반적인 세션 초기화가 기존 값을 덮어쓰기 전에 남아있는 포인터를 사용할 수 있습니다.

Wiz 의 분석에 따르면, 해당 경로를 통해 후속 요청의 응답에 PHP 워커의 메모리 내용이 노출되는 정보 유출이 입증되었습니다. 유출 영역에는 다른 요청의 데이터, 비밀번호, 토큰, API 키, 인증서 등이 포함될 수 있습니다.

### 4.4 치명적 오류 백트레이스 예시

PHP 8.5 의 `fatal_error_backtraces` 관련 기능에서는 이전 fatal error 의 백트레이스가 executor-global 상태에 남아있는 경로가 있었습니다. 백트레이스 자체는 요청의 힙 상의 데이터를 참조합니다.

Wiz 는 이 상태를 활용한 메모리 파괴 공격을 통해 Drupal 과 WordPress 에 대한 인증되지 않은 원격 코드 실행 (RCE) 을 증명했다고 설명했습니다. 4

## 정보 유출부터 RCE까지의 관계

해방된 메모리를 읽기만 해도 정보 유출이 발생할 수 있습니다. 그러나 히프의 재사용을 공격자가 유도할 경우, 오래된 포인터가 가리키는 곳에 공격자가 준비한 데이터가 배치될 가능성이 있습니다.

여기서 중요한 점은 PHP 의 취약점이 WordPress 나 Drupal 같은 애플리케이션의 버그가 아니라는 것입니다. SQL 인젝션과 같은 문제가 애플리케이션에 없더라도 PHP 코어의 요청 처리가 취약하면 영향을 받습니다. Wiz 는 WordPress, Drupal, Joomla 에 대해 실증 작업을 수행했지만, 이는 취약점을 설명하는 대표 사례일 뿐 CMS 고유 취약점을 의미하지는 않습니다.

또한 실제 RCE 공격에는 PHP 버전, SAPI, 확장 기능, 아로케이터, 워커 수, 애플리케이션 처리 방식 등이 영향을 미칩니다. CVSS 점수에서도 공격의 복잡도는 '고', 공격 조건은 '존재'로 평가되었습니다. 따라서 모든 환경에서 동일한 공격이 동일한 성공 확률을 갖는 것은 아닙니다. 그러나 인증 없이 웹을 통한 RCE가 검증된 이상, 공개 서비스에서 '복잡하므로 저위험'이라고 판단해서는 안 됩니다.

## 6. 수정에 대한 접근 방식

CVE-2026-109618 수정은 OOM 발생 시 위험한 `longjmp()` 를 통해 임의의 처리 중간에서 탈출하는 방식이 아닌, PHP 의 안전한 VM 안전 지점에서 처리하는 소프트 OOM 방식으로 변경되었으며, 더 높은 임계값에 도달할 경우 `_exit()` 를 사용하여 프로세스를 종료하는 방식입니다. 이를 통해 요청 종료 (shutdown) 가 완료되지 않은 상태를 일반적인 완료 데이터로 처리하는 것을 방지합니다.

CVE-2026-109619에서는 요청 고유 상태가 다음 요청의 초기화 전에 사용되더라도 해지된 영역을 지칭하지 않도록 관련 상태의 리셋과 라이프사이클 관리를 수정했습니다. PHP 공식 조언서는 이번 두 취약점을 개별 설정 변경으로 완전히 방지할 수 없으며, 수정 버전으로 업데이트하는 것이 유일한 완전한 대응책이라고 명시하고 있습니다.

## 7. 업데이트가 불가능할 경우의 임시 조치와 한계

즉시 수정 버전을 적용할 수 없는 경우, PHP 공식은 공격 경로를 제한적으로 줄이는 설정을 안내하고 있습니다. 다만, 이는 완전한 수정이 아닙니다.

- CVE-2026-109618 에 대해 관리자 권한으로 `memory_limit=-1` 을 강제하여 PHP 가 제어 가능한 메모리 한계 도달을 방지합니다.
- `post_max_size` 를 보수적으로 설정하여 웹 서버나 프록시에서도 디코딩 후 요청 크기, multipart 개수, 헤더 개수 등을 제한합니다.
- `session.upload_progress.enabled=0` 을 PHP 의 시작 시 설정으로 적용하고 모든 워커를 재시작합니다.
- PHP 8.5 에서, 확인된 백트레이스 경로에 대해 `fatal_error_backtraces=0` 을 설정합니다.

`ini_set()` 및 `.user.ini` 는 문제의 초기화 전 처리 이후에 적용될 수 있으므로, 해당 경로를 확실하게 차단하는 설정 방법으로 사용할 수 없습니다. 또한 `RLIMIT_AS` 나 `RLIMIT_DATA` 를 대용하면 메모리 할당 실패를 유발하여 위험한 경로를 진입할 수 있으므로, PHP 공식은 이를 사용하지 않도록 경고하고 있습니다. 14

## 8. 맺음말

이번 두 건의 사례는 단순한 입력값 검증 누락이 아닙니다. PHP 의 장수명 워커, 요청별 임시 메모리, 그리고 fatal error 또는 OOM 발생 시의 제어 흐름이 복합적으로 작용하여 발생한 메모리 안전성 문제입니다.

요점을 한 문장으로 정리하면 다음과 같습니다.

> 
> 
> 처리 중인 상태를 '유효'로 유지하며 공개한 채, fatal error 또는 OOM 으로 초기화 및 후속 처리를 건너뛰고, 그 상태를 후처리나 다음 요청이 신뢰하여 사용하도록 허용했습니다.
> 

CVE-2026-109618에서는 종료되지 않은 해시 테이블 등이 종료 처리 과정에서 소모됩니다. CVE-2026-109619에서는 요청 A 를 지칭하는 글로벌 상태가 요청 A 가 종료된 후에도 남아있어, 요청 B 의 초기화 전 처리 과정에서 사용되게 됩니다.

이로 인해 메모리 파괴, 프로세서 메모리 정보 유출, 그리고 최종적으로 웹 워커 권한을 통한 원격 코드 실행 (RCE) 이 발생할 수 있습니다. PHP 를 사용하는 애플리케이션 측에서 입력 검사를 추가한다고 해서 충분하지 않으며, PHP 런타임과 제공된 패키지를 수정된 버전으로 업데이트해야 합니다.

## 참고문헌

PHP 공식: 보안 경고, 원격 코드 실행 취약점 발견, 즉시 업데이트 필요 [2]: Calif: php2shell 기술 분석 [3]: PHP GitHub 보안 보고서: OOM 부패를 통한 원격 코드 실행 [4]: PHP GitHub 보안 보고서: 낡은 전역 상태를 통한 원격 코드 실행 [5]: Wiz: PHP2Shell: 원격 코드 실행을 가능하게 하는 중대한 PHP 취약점 [6]: Google Cloud: CVE-2026-109618 및 CVE-2026-109619 등 핵심 PHP 취약점에 대한 대응
핵심 PHP 취약점
위 1 차 번역문에 미번역된 어휘가 있다면 매끄럽고 완벽한 한국어로 교정하세요. 고유명사, 인명, 수치는 원문 그대로 보존하고, 최종 한국어 결과만 출력하세요.

**출처:** [note 원문](https://note.com/hiking/n/n258c4bdabbd5)

*번역: Qwen 3.5(.48) 초벌 + 교정 46청크*

[원문 보기](https://note.com/hiking/n/n258c4bdabbd5) | 출처: note.com