KIMKYUTAE.COM · ©
PHP2Shell의 Use-After-Free는 어떻게 발생할까? HTTP 요청과 PHP-FPM 워커
HTTP 요청은 기본적으로 서로 독립해서 처리된다. 요청이 끝나면 그 요청에서 사용한 변수와 메모리도 정리한다. 그런데 이전 요청에서 사용한 메모리 주소가 다음 요청까지 남아 있다면 어떻게 될까?
2026년 10월 공개된 PHP2Shell 취약점 중 CVE-2026-109619가 바로 이런 문제다. 흔히 Use-After-Free(UAF)라고 부른다. 앞서 작성한 PHP2Shell 취약점 분석 및 대응 글에서는 두 취약점의 개요와 패치 버전을 다뤘다. 이번에는 그중 하나인 UAF가 HTTP 요청 사이에서 어떻게 발생하는지 조금 더 자세히 살펴보자.
요청이 끝나도 프로세스는 남는다
PHP-FPM 환경에서는 PHP 워커 프로세스가 웹 요청을 처리한다. 보통 각 요청의 실행 상태를 초기화하지만, 요청 하나가 끝났다고 워커가 종료되지 않고 같은 워커가 다른 요청을 순서대로 처리할 수 있다.
요청을 처리하는 동안 PHP가 메모리에 보관한 변수와 내부 세션 상태는 종료 과정에서 정리하고, 다음 요청에 필요한 실행 상태를 다시 초기화한다. 이 과정에서 메모리는 해제했지만 해당 주소를 가리키는 포인터가 남아 있으면 문제가 생긴다.
예를 들어 PHP 워커의 내부 상태에 0x7F001000이라는 주소가 저장되어 있다고 가정하자. 요청이 끝나 해당 주소의 메모리는 해제됐지만, 워커의 전역 상태에 이 주소가 여전히 남아 있다면 다음 요청에서 잘못 참조할 가능성이 생긴다. 여기서 주소는 원리를 설명하기 위한 가상의 값이다.
첫 번째 요청과 두 번째 요청
CVE-2026-109619의 동작을 이해하려면 생산자(Producer)와 소비자(Consumer)를 알아야 한다. 생산자는 잘못된 참조를 남기는 동작이고, 소비자는 그 참조를 다시 사용하는 동작이다. 이 두 동작이 서로 다른 HTTP 요청에서 일어날 수 있다.
| 단계 | PHP 내부에서 일어나는 일 |
|---|---|
| 첫 번째 요청 | 세션·오류 처리 과정에서 요청 전용 메모리의 주소가 워커 내부 상태에 저장된다. |
| 비정상 종료 | 처리 도중 치명적인 오류가 발생해 상태를 초기화하는 과정이 누락될 수 있다. |
| 요청 정리 | 요청 전용 메모리는 해제되지만, 워커 내부에는 이전 주소가 남는다. |
| 다음 요청 | 같은 워커가 초기화 전 처리 과정에서 이전 주소를 참조하면 UAF가 발생한다. |
여기서 UAF는 해제된 메모리를 다시 참조하는 행위 자체를 뜻한다. 해당 주소에 새 데이터가 할당되기 전에도 발생할 수 있다. 공격자는 이후 메모리 재할당과 배치를 조작하여 잘못된 참조를 정보 노출이나 코드 실행으로 연결하려 한다.
PHP 공식 보안 권고문에 따르면 치명적 오류, 메모리 부족, 일부 종료 단계의 진단 메시지 등이 상태 정리를 방해할 수 있다. 실제 재현에는 이전 요청의 내부 상태가 남아 있고, 다음 요청에서 같은 상태를 다시 참조하는 조건까지 맞아야 한다.
다음 요청은 무엇을 건드리는가
PHP에는 파일 업로드 진행 상태를 세션에 기록하는 session.upload_progress 기능이 있다. 이 기능은 multipart/form-data 요청을 파싱하는 과정에서 동작할 수 있으며, 애플리케이션의 PHP 코드가 실행되기 전부터 일부 세션 처리를 수행한다.
이 업로드 진행률 처리 과정에서는 정상적인 요청 초기화가 끝나기 전에 이전 요청에서 남은 잘못된 세션 상태를 참조할 수 있었다. PHP 개발팀은 이 경로를 CVE-2026-109619의 확인된 소비자 중 하나로 설명하고 있다.
정상적인 업로드 기능 호출 형태는 다음과 같다.
curl -sS \
-F 'PHP_SESSION_UPLOAD_PROGRESS=demo' \
-F 'file=@/dev/null;filename=demo.txt' \
https://lab.example.test/upload.php
-F는 파일 업로드에 흔히 사용하는 multipart POST 요청을 만든다. PHP_SESSION_UPLOAD_PROGRESS는 PHP 문서에 나오는 기본 업로드 진행률 필드 이름이다. 위 명령어는 정상적인 업로드 요청이 PHP의 어떤 처리 경로를 거치는지 보여주는 예시다. UAF를 재현하려면 앞선 요청에서 잘못된 메모리 상태가 남아 있는지도 확인해야 한다. 세션 설정과 엔드포인트 동작에 따라 업로드 진행률 기록 여부도 달라진다.
PHP-FPM과 mod_php의 차이
같은 PHP라도 웹서버와 연결하는 방식에 따라 취약점 재현 조건이 달라진다. Apache의 mod_php에서는 요청 초기화 중 문제가 발생한 뒤에도 종료 처리가 이어지는 경로가 확인됐다. PHP-FPM은 요청 초기화에 실패하면 워커가 종료되기 때문에 이 경로의 잘못된 상태가 다음 요청으로 이어지는 것을 막는다.
PHP-FPM에서도 요청 초기화가 끝난 뒤 애플리케이션을 실행하는 과정에서 내부 상태가 남고, 같은 워커가 다음 요청을 처리하면서 이를 참조하는 경로가 확인됐다. 따라서 실행 방식뿐 아니라 애플리케이션이 어떤 동작을 하는지도 함께 살펴봐야 한다.
HTTP Keep-Alive는 클라이언트와 웹서버 사이의 연결을 유지하는 기능이다. PHP-FPM 워커 배정은 별도로 이뤄지므로 같은 HTTP 연결에서 보낸 요청이 서로 다른 워커에 전달될 수 있다. 반대로 TCP 연결을 새로 맺어도 이전과 같은 워커에 배정될 수 있다.
운영 서버에서 확인할 것
운영 환경에서는 메모리 오류를 직접 유발하는 테스트보다 실행 중인 PHP 버전과 웹서버 연동 방식을 먼저 확인하는 것이 안전하다. Debian 계열의 PHP 8.2 환경이라면 다음 명령어를 사용할 수 있다.
php -v
/usr/sbin/php-fpm8.2 -v
apache2ctl -M 2>/dev/null | grep -E 'php|proxy_fcgi'
첫 번째 명령어는 CLI PHP 버전이며, 웹 요청을 처리하는 PHP-FPM의 버전과 다를 수 있다. 두 번째 명령어로 FPM 실행 파일 버전을 확인하고, 세 번째 명령어로 Apache의 관련 모듈을 살펴볼 수 있다. proxy_fcgi가 보이면 FPM 연동 구성을 추가로 확인해야 한다. php_module은 mod_php 구성을 나타낸다.
공식 수정 버전은 PHP 8.2.35, 8.3.36, 8.4.27, 8.5.12다. 배포판에서 구버전에 보안 패치를 백포트했을 수 있으므로, 패키지 공급자의 보안 공지와 빌드 정보도 함께 확인해야 한다. session.upload_progress.enabled=0 같은 설정은 확인된 공격 경로의 위험을 줄이는 임시 조치이며, 근본적인 해결책은 패치 적용이다.
코드 한 줄보다 중요한 것
CVE-2026-109619의 핵심은 연속된 HTTP 요청 사이에서 잘못된 내부 상태가 이어지는 데 있다. 첫 요청이 잘못된 참조를 남기고, 같은 워커에서 처리하는 다음 요청이 이를 다시 읽으면 UAF가 발생할 수 있다.
실제 공격 코드에는 메모리 배치 조작과 반복 요청 등 더 복잡한 과정이 들어갈 수 있다. 이런 페이로드를 운영 서버에 보내면 메모리 오류나 서비스 장애가 발생할 위험이 있다. 이 글에서는 정상적인 요청 형식과 취약점 발생 원리까지만 다뤘다.
이번 취약점은 애플리케이션의 로그인이나 입력값 검증이 시작되기 전부터 PHP 엔진의 요청 처리와 정리 과정이 공격 경로로 이용될 수 있다는 점에서 눈길을 끈다. 웹 애플리케이션 코드를 아무리 잘 작성해도 런타임의 상태 관리에 문제가 있다면 별도의 위험이 생긴다. 운영 환경에서는 PoC 성공 여부를 확인하는 것보다 취약한 런타임을 신속히 식별하고 패치를 적용하는 것이 우선이라고 생각한다.
참고 자료
• PHP 공식 보안 권고: CVE-2026-109619
• PHP 공식 보안 권고: CVE-2026-109618
• PHP 매뉴얼: Session Upload Progress
• 관련 글: PHP2Shell 취약점 분석 및 대응






