PHP2Shell(CVE-2026-109618·109619) 분석 및 원리와 해결방안

이번 PHP 취약점은 애플리케이션이 아니라 PHP 엔진 자체에서 비인증 원격코드실행(RCE)이 가능한 버그 두 건으로, 2026년 10월 21일 긴급 패치됐습니다. 묶어서 PHP2Shell이라고 부르고, 둘 다 CVSS v4.0 9.2입니다.

  • CVE-2026-109618 (OOM 중 메모리 손상) — 요청 파싱 도중 메모리 한도(memory_limit)를 일부러 터뜨리면, 반쯤 만들어진 내부 데이터를 정리 루틴이 정상 데이터로 착각해 해제 → use-after-free → RCE
  • CVE-2026-109619 (오염된 전역 상태) — 앞 요청이 남긴 낡은 상태를 같은 워커의 다음 요청이 초기화 전에 읽어버림 → use-after-free → RCE
  • 해결: 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 로 업데이트가 유일한 완전 해결책. 8.1 이하는 공식 패치가 없습니다
  • 업그레이드가 당장 안 되는 레거시라면: memory_limit·요청 크기 제한·WAF 시그니처·워커 재활용으로 공격면을 줄이고, 10월 20일 이후 로그부터 침해 흔적을 사냥해야 합니다

애플리케이션 코드가 아무리 깨끗해도 PHP가 요청 본문을 파싱하는 단계, 즉 내 코드가 한 줄도 실행되기 전에 터지는 버그라는 점이 이번 건의 무서운 부분입니다.

배경 — 유출에서 긴급 패치까지 이틀

조율된 공개(coordinated disclosure)가 진행되던 중 세부 내용이 먼저 새어 나왔고, PHP 팀은 예정을 당겨 긴급 릴리스를 냈습니다.

일시 (UTC)내용
2026-10-21 14:00PHP 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 릴리스, 연구진·PHP 동시 공개
2026-10-21 오전Cloudflare가 초기 공격 시도 관측 보고
2026-10-20세부 내용·PoC 영상 소셜미디어 유출, PHP가 긴급 릴리스 예고, watchTowr가 RCE 재현 확인
2026-08-24Wiz Research가 PHP에 보고
2026-08-11Calif가 첫 버그를 PHP에 보고, 조율된 공개 시작

영향 SAPI: PHP 공식 입장은 웹 요청을 처리하는 모든 설치가 위험하다는 것입니다. PHP-FPM, Apache mod_php, php-cgi, FrankenPHP, LiteSpeed가 모두 포함됩니다. 그중 위험이 가장 높은 건 mod_php로, 기본 설정에서 인증 없이 RCE가 검증됐습니다. FPM은 초기화 실패 시 워커가 바로 종료되는 구조라 애플리케이션 실행 단계를 거치는 조건부 경로로 영향을 받습니다.

지원 범위: 패치는 8.2 이상에만 나왔습니다. 8.1 이하(7.x, 5.x 포함)는 EOL이라 공식 수정이 없고, 8.2도 2026년 12월 31일 EOL입니다. Unit 42는 잠재적으로 취약한 인터넷 노출 인스턴스를 2,260만 개 이상으로 집계했고, watchTowr는 실제 악용(in the wild)을 확인했다고 밝혔습니다.

참고: 공개 초기 일부 매체는 두 CVE 번호의 설명을 서로 바꿔 적었습니다. 이 글은 PHP 공식 GHSA 기준으로 109618 = OOM 손상(GHSA-w8hw-w9fx-j79w), 109619 = 전역 상태 오염(GHSA-6wmf-4rv5-p3rp) 으로 정리합니다.

CVE-2026-109618 — OOM 중 메모리 손상

핵심은 “급정거하면 반쯤 지은 건물이 남는다”입니다. PHP는 요청이 memory_limit을 넘으면 longjmp()로 현재 실행 흐름을 통째로 건너뛰어 요청을 중단(bailout)합니다. 문제는 이 점프가 내부 데이터 구조를 만드는 도중에도 일어난다는 점입니다.

  1. PHP가 요청 본문($_POST, 업로드 등)을 파싱하며 해시테이블 같은 내부 구조를 만든다
  2. 일부 구조는 필요한 메모리 할당이 다 끝나기 전에 “사용 준비 완료”로 먼저 표시된다
  3. 그 직후 할당이 OOM으로 실패하면 longjmp()로 급히 빠져나가고, 반쯤 초기화된 상태는 롤백되지 않는다
  4. 요청 정리(cleanup) 루틴이 그 구조를 정상 데이터로 믿고 해제·참조한다
  5. 초기화되지 않은 메모리에는 공격자가 앞서 채워 둔 데이터가 남아 있을 수 있다 → use-after-free / 메모리 손상 → RCE

공격자는 OOM을 어떻게 “원하는 타이밍에” 일으키나

공개된 PoC(일명 rawkey)는 수 MB짜리 필드명 하나를 보냅니다. PHP가 그 키를 복사하는 순간 memory_limit이 고갈되도록 크기를 맞추는 방식이고, 최소 약 1MiB 수준의 필드명이 필요한 것으로 분석됩니다. application/x-www-form-urlencoded와 multipart/form-data 둘 다 통합니다.

PHP 공식 권고는 이보다 넓게 봅니다. OOM을 일으키는 “여러 전략”이 있다고 명시하고, 필드 개수·multipart part 개수·헤더 개수·중첩 깊이까지 제한하라고 권합니다. 즉 거대한 필드명은 대표 벡터일 뿐이고, 할당을 몰아서 한도를 넘기는 방법이면 무엇이든 방아쇠가 될 수 있다는 얘기입니다.

여기서 중요한 포인트 하나. 애플리케이션이 파일 업로드 기능을 쓰는지는 상관없습니다. mod_php는 스크립트가 실행되기 전 요청 본문을 파싱하는 단계에서 이미 영향을 받습니다.

패치는 무엇을 바꿨나

OOM 처리에서 longjmp()를 아예 뺐습니다. 한도를 넘으면 VM의 다음 안전 지점(safe point)에서 지연된 “소프트 OOM”을 일으키고, 더 높은 2차 임계치를 넘으면 _exit()로 즉시 프로세스를 끝내는 “하드 OOM”으로 바뀌었습니다. 정리 루틴이 반쯤 초기화된 메모리를 보는 상황 자체가 사라진 겁니다.

CVE-2026-109619 — 오염된 전역 상태(워커 재사용)

핵심은 “앞 손님이 남긴 쪽지를 다음 손님이 읽는다”입니다. PHP 워커 프로세스는 세션·에러 처리·객체·확장 모듈 상태 같은 전역 상태를 프로세스 단위로 들고 있습니다. 그런데 그중 일부가 사실은 특정 요청 하나가 소유한 메모리를 가리킵니다.

  1. 요청 A 처리 중 치명적 오류(fatal error)가 작업이나 정리 단계를 중간에 끊는다
  2. 전역 상태가 리셋되기 전에 요청 A의 메모리는 해제된다 → 전역 포인터는 이미 해제된 메모리를 가리킨 채 남음(stale)
  3. 같은 워커가 요청 B를 받고, 정상 초기화보다 먼저 실행되는 초기 단계 동작이 그 낡은 상태를 읽는다
  4. 해제된 메모리를 유효한 것처럼 사용 → use-after-free → 메모리 노출·RCE

그래서 공격은 요청 2개의 조합입니다. 나쁜 상태를 남기는 요청(생산자)과, 다음에 그 상태를 밟는 요청(소비자). Apache prefork + mod_php처럼 한 프로세스가 요청을 연달아 처리하는 구조가 정확히 이 조건에 맞습니다. 다만 같은 요청 안에서 PHP가 실패를 잡고 정리를 계속하는 경우에도 발생할 수 있어서, “워커 재사용만 막으면 끝”은 아닙니다.

생산자 경로는 여러 개다

  • session.upload_progress — 공개 PoC(php2shell)가 쓴 경로입니다. 이 기능은 기본값이 켜져 있고, 업로드 진행 상태를 세션에 기록하는 콜백이 애플리케이션 코드보다 먼저, 모든 POST 엔드포인트에서 동작합니다. 바디가 약 450바이트에 불과해 크기 기반 차단으로는 못 막습니다
  • fatal_error_backtraces — PHP 8.5에 들어온 기능으로, PHP 팀이 확인한 RCE의 생산자 경로입니다
  • 요청별로 적용되는 잘못된·폐기된 ini 설정 — 초기화 전에 진단 메시지(diagnostics)를 내는 설정도 생산자가 될 수 있습니다. error_reporting, display_errors, @로 메시지를 숨겨도 소용없습니다

PHP 공식 문서도 이것들은 “확인된 경로”일 뿐이고 다른 변형이 있을 수 있다고 못박았습니다. 특정 기능 하나 끈다고 안전해지는 구조가 아니라는 뜻입니다.

111

두 취약점 메커니즘 비교 · 각 4단계

109618은 한 요청 안에서, 109619는 요청과 요청 사이에서 같은 일이 벌어집니다. 해제·미초기화 메모리를 유효한 것으로 믿는 순간이 공통 지점입니다.

내 서버는 얼마나 위험한가 — 영향도 판단

판단의 기준은 CMS나 프레임워크 버전이 아니라 PHP 버전과 SAPI입니다. 아래 순서로 확인합니다.

확인 항목확인 방법판단
PHP 버전php -v, phpinfo()8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 미만이면 취약. 8.1 이하는 패치 없음
SAPIphpinfo()의 Server APIApache 2.0 Handler(mod_php)면 최고 위험, FPM은 조건부
인터넷 노출외부 스캔, LB·방화벽 정책외부에서 POST가 닿으면 비인증 공격 가능
memory_limitphp -i | grep memory_limit, 디렉터리·풀별 override유한값이면 109618 PoC의 전제 조건 충족
session.upload_progressphp -i | grep upload_progress켜져 있으면 109619 공개 PoC 경로 존재
워커 모델Apache MPM, FPM pm 설정한 프로세스가 요청을 연달아 처리하면 109619 조건 충족

레거시 PHP 5.3 사례로 보면

Apache prefork + mod_php 5.3.x로 남아 있는 레거시 서버를 예로 들어 보겠습니다. 실무에서 아직 꽤 흔한 조합입니다.

  • 109619의 확인된 생산자 경로 두 개는 없다 — session.upload_progress는 5.4부터, fatal_error_backtraces는 8.5부터 들어왔습니다. 그래서 “업로드 진행률 끄기”는 이 서버엔 해당 사항이 없습니다
  • 그렇다고 안전하다는 뜻은 아니다 — PHP가 공식적으로 다른 변형을 열어 두었고, prefork의 워커 재사용 구조는 그대로입니다
  • 109618이 핵심 위협이다 — OOM 시 longjmp로 빠져나가는 bailout 구조는 PHP 5 시절부터 이어져 온 설계입니다. 공식 권고는 EOL 버전의 영향 여부를 따로 밝히지 않았고, 연구진 보고도 “5.4 이상”과 “5 전체”로 엇갈립니다. 패치를 받을 수 없는 버전이니 취약하다고 가정하고 대응하는 게 맞습니다
  • 5.3.8 이하라면 max_input_vars조차 없다 — 필드 개수 제한은 해시 충돌 DoS 대응으로 5.3.9에 생긴 설정입니다. PHP 자체로 필드 수를 못 막으니 웹서버·WAF 단에서 막아야 합니다

덤으로, 5.3은 이번 건 말고도 수년간 누적된 미패치 취약점을 안고 있습니다. 이번 대응은 어디까지나 교체 전까지 버티는 임시 조치라는 걸 분명히 해 두는 게 좋습니다.

해결방안

정답은 업데이트 하나입니다. PHP는 두 권고 모두에서 패치 적용만이 완전한 해결책이고, 설정으로 완전히 막는 일반적인 방법은 없다고 명시했습니다. 아래 임시 완화는 업그레이드까지 시간을 버는 수단입니다.

1. 근본 해결 — 패치 버전으로

  • 8.2~8.5: 각각 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 이상으로 올린다. 배포판 패키지를 쓰면 백포트된 보안 업데이트 여부를 배포판 공지로 확인한다
  • 컨테이너는 베이스 이미지를 다시 받아 재빌드·재배포한다. 이미지 태그만 보고 끝내지 말 것
  • 적용 후 모든 PHP 워커·웹서버를 재시작한다. 이미 떠 있는 워커는 옛 바이너리로 계속 돈다
  • 8.1 이하: 8.2 이상 메이저 업그레이드가 원칙. 당장 불가하면 상용 연장지원(NES 등) 백포트 검토와 함께 아래 완화를 겹겹이 건다

2. 업데이트가 안 될 때 — 겹겹이 완화

조치막는 것한계·주의
memory_limit=-1 관리자 고정109618 PoC가 노리는 예측 가능한 OOMPHP 메모리 상한이 사라짐 → 서비스 단위 메모리 상한 필수. 하부 할당(mmap) 실패 시엔 여전히 취약 경로 진입 가능
post_max_size 축소 (업로드 없음 8M, 있음 32M)거대 바디 기반 OOM 유도업로드 크기 요구사항 확인 필요
웹서버·프록시 요청 제한 (바디 크기, 필드·part·헤더 수, 중첩 깊이)109618 계열 OOM 전략 전반Content-Length 없는 chunked·HTTP/2 요청까지 실제 바이트로 제한해야 함
multipart 미사용 앱은 프런트에서 multipart 차단multipart 기반 벡터업로드 기능 있는 앱엔 불가
session.upload_progress.enabled=0 (5.4+)109619 공개 PoC 경로다른 생산자 경로는 남음
fatal_error_backtraces=0 (8.5)109619 확인된 RCE 생산자8.5 전용
요청별 잘못된·폐기된 ini 설정 제거초기화 전 진단 메시지 생산자에러 숨김(display_errors 등)은 효과 없음
WAF 시그니처 + 대용량 POST 레이트리밋두 PoC의 요청 형태변형 우회 가능, 탐지 모드로 먼저 검증

3. mod_php 설정 예시

php_admin_*로 걸어야 .htaccess나 ini_set()으로 애플리케이션이 되돌리지 못합니다. 적용 후 디렉터리·가상호스트 단위로 유한한 memory_limit을 다시 넣는 설정이 없는지 꼭 확인하세요.

# httpd.conf 또는 해당 VirtualHost
php_admin_value memory_limit -1
php_admin_value post_max_size 8M
php_admin_value max_input_nesting_level 32
# max_input_vars는 5.3.9+에서만 존재
php_admin_value max_input_vars 500
# 5.4+에서만 존재
php_admin_flag session.upload_progress.enabled off

# Apache 코어: 바디·헤더 제한 (chunked 바디에도 적용)
LimitRequestBody 8388608
LimitRequestFields 50
LimitRequestFieldSize 8190

memory_limit을 풀었다면 서비스 단위 상한이 짝으로 따라와야 합니다. systemd의 메모리 상한(MemoryMax, 구버전 MemoryLimit)이나 컨테이너 메모리 제한을 쓰되, 가능하면 문제 워커 하나만 죽고 서비스 전체가 내려가지 않는 정책으로 맞춥니다. 반대로 RLIMIT_AS·RLIMIT_DATA(ulimit)로 대신하면 안 됩니다. 이것들은 오히려 취약한 할당 실패를 직접 일으킬 수 있습니다.

4. 워커 재활용 — 필자 의견

아래는 공식 권고가 아니라 원리에서 끌어낸 판단입니다. 109619의 핵심 조건이 “같은 워커가 다음 요청을 처리하는 것”이므로, prefork에서 MaxRequestsPerChild 1(2.4에서는 MaxConnectionsPerChild 1 + KeepAlive Off)로 프로세스를 요청마다 새로 띄우면 요청 간 상태 오염 경로가 크게 줄어듭니다. 대신 프로세스 생성 비용으로 성능이 크게 떨어지고, 한 요청 안에서 일어나는 경로는 막지 못합니다. 트래픽이 적은 레거시 내부 시스템이라면 검토해 볼 만한 수준입니다.

5. WAF·IPS 룰 방향

  • 필드명 1,000바이트 이상 차단 — 정상 클라이언트가 이런 필드명을 보낼 일은 없습니다. urlencoded 키와 multipart part 이름 둘 다, 값이 아닌 이름 길이를 봐야 합니다
  • Content-Length 헤더를 믿지 말 것 — chunked·HTTP/2는 헤더 없이 바디를 보내므로 실제 바디를 측정해야 합니다
  • multipart 안의 PHPSESSID 폼 필드 차단 — 정상 세션 ID는 쿠키 헤더로 옵니다
  • 파일명에 PHP 여는 태그(<?php, <?=)나 직렬화 문법(O:숫자:, a:숫자:{ 등)이 있으면 차단 — 파일 내용이 아니라 part 헤더만 검사해 오탐을 줄입니다
  • IP당 대용량 POST 레이트리밋 — 109618 익스플로잇은 ASLR 무력화를 위해 15분 남짓 동안 약 590건의 대형 요청을 보내는 것으로 분석됐습니다

상용 WAF라면 벤더에 PHP2Shell 시그니처 배포 여부를 확인하고, 없으면 위 조건으로 커스텀 룰을 만듭니다. ModSecurity·Suricata용 전체 룰셋은 참고자료의 Abstract Security 글에 정리돼 있습니다. 어느 쪽이든 탐지(로그) 모드로 오탐을 먼저 본 뒤 차단으로 전환하세요.

6. 노출 자체를 줄이기

패치가 불가능한 서버는 결국 “닿지 않게” 하는 게 가장 확실합니다. 내부 사용자용이면 IP 허용목록·VPN 뒤로 숨기고, 외부 서비스라면 리버스 프록시 뒤에 두어 위 제한을 프록시에서 강제합니다.

탐지와 침해 여부 점검

패치는 앞으로의 공격을 막을 뿐, 이미 들어온 침입을 되돌리지 않습니다. 인터넷에 노출된 서버라면 유출일인 2026년 10월 20일부터의 로그를 기준으로 침해 흔적을 찾아야 합니다. 익스플로잇은 워커를 죽이지 않고 웹서버 계정 권한으로 명령을 실행하기 때문에 겉으로는 아무 이상이 없어 보일 수 있습니다.

로그에서 볼 것

  • PHP Fatal error: Allowed memory size … (tried to allocate …) in Unknown on line 0 — 보통 메모리 초과는 파일명과 줄 번호가 찍힙니다. Unknown on line 0은 스크립트 실행 바깥, 즉 요청 파싱 단계에서 터졌다는 뜻이라 109618의 대표 신호입니다
  • 같은 IP의 대용량 POST 버스트 — 1MB 이상 POST가 10분 안에 10건 이상이면 들여다볼 가치가 있습니다
  • Apache 에러로그의 AH00052(자식 프로세스 시그널 종료) 다발 뒤 응답이 몇 초씩 걸린 요청 하나 — ASLR 브루트포스 후 성공한 패턴입니다
  • multipart 바디에 PHPSESSID 폼 필드, 파일명에 <?php나 직렬화 문법 — 109619 PoC 형태

호스트에서 볼 것

  • apache2·httpd·php-fpm·php-cgi의 자식으로 뜬 sh -c 프로세스, 특히 id, whoami, curl, wget, base64, nc, python 실행 흔적
  • 웹서버 계정이 /tmp, /dev/shm, /var/tmp, 웹 루트에 새로 만든 파일 — 웹셸, 리버스셸 스크립트
  • 웹서버 계정의 새 cron, authorized_keys 추가, 외부 비표준 포트로의 아웃바운드 연결
  • 웹서버 계정에 DB 권한이 있다면 신규 DB 계정·데이터 대량 조회 흔적

session.upload_progress 경로를 썼던 서버라면

109619 PoC 중 일부는 세션 파일에 페이로드를 심어 두고, 로컬 파일 인클루전(LFI) 같은 다른 버그로 나중에 불러오는 방식입니다. WAF 룰을 넣기 전에 이미 오염된 세션 파일은 만료 전까지 남아 있으므로, 룰 적용과 함께 session.save_path의 세션 파일을 비우는 게 안전합니다.

SIEM이 있다면 위 조건을 상관분석 룰로 만들어 두고, 웹 접근로그·에러로그·PHP 에러로그·프로세스 실행 로그가 모두 수집되는지부터 확인하세요. 로그가 없으면 사냥도 없습니다.

마무리 — 실무자 관점에서

두 취약점은 결국 같은 뿌리입니다. 요청이 비정상적으로 끝날 때 PHP가 메모리를 깨끗하게 치우지 못한다는 것. 109618은 그 순간 같은 요청의 정리 루틴이, 109619는 다음 요청이 그 쓰레기를 밟습니다.

우선순위를 정리하면 이렇습니다.

  1. 인터넷에 노출된 mod_php 서버부터 찾아서 패치 또는 노출 차단
  2. 8.2 이상은 바로 패치하고 워커 재시작
  3. 패치 불가한 레거시는 memory_limit 고정 + 요청 크기·필드 수 제한 + WAF 룰 + 서비스 메모리 상한을 겹쳐 건다
  4. 10월 20일 이후 로그로 침해 흔적 사냥
  5. 레거시 PHP 교체 일정을 이번 기회에 확정

특히 5.x·7.x 레거시를 “리뉴얼하면 걷어낼 거니까” 하고 버텨 온 곳이 많을 텐데, 이번처럼 엔진 레벨 RCE가 터지면 남은 기간 내내 패치 없는 서버를 운영하는 셈입니다. 임시 완화는 시간을 버는 것이지 위험을 없애는 게 아니라는 점, 보고서에도 그렇게 적어 두는 게 맞습니다.

— Root Hyeon

자주 묻는 질문 (FAQ)

PHP2Shell은 정확히 무엇인가요?

2026년 10월 21일 PHP가 긴급 패치한 두 취약점, CVE-2026-109618과 CVE-2026-109619를 함께 부르는 이름입니다. 둘 다 PHP 엔진 내부의 use-after-free 버그로, 인증 없이 HTTP 요청만으로 원격 코드 실행까지 이어질 수 있어 CVSS v4.0 9.2를 받았습니다.

내 서버의 PHP 버전이 취약한지 어떻게 확인하나요?

php -v나 phpinfo()로 버전을 보고, 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 미만이면 취약하다고 보면 됩니다. phpinfo()의 Server API가 Apache 2.0 Handler라면 mod_php로, 가장 위험도가 높은 구성입니다.

PHP 7.x나 5.x는 보안 패치가 나오나요?

나오지 않습니다. PHP 공식 패치는 8.2 이상만 대상이고, 8.1 이하는 EOL입니다. 메이저 업그레이드가 원칙이며, 당장 어렵다면 상용 연장지원 검토와 함께 본문의 임시 완화 조치를 겹쳐서 적용해야 합니다.

session.upload_progress만 끄면 안전한가요?

아닙니다. 그건 CVE-2026-109619 공개 PoC의 경로 하나를 막을 뿐입니다. PHP는 다른 생산자 경로가 있다고 밝혔고, CVE-2026-109618(OOM 손상)은 이 설정과 무관하게 동작합니다.

memory_limit=-1로 바꾸면 다른 문제는 없나요?

PHP 자체의 요청당 메모리 상한이 사라지므로, 요청 하나가 서버 메모리를 크게 잡아먹는 서비스 거부 위험이 커집니다. 그래서 systemd나 컨테이너 수준의 메모리 상한을 반드시 함께 걸어야 하고, ulimit(RLIMIT_AS·RLIMIT_DATA)으로 대신하면 오히려 취약한 경로를 유발할 수 있습니다.

WAF가 있으면 패치를 미뤄도 되나요?

권하지 않습니다. WAF 시그니처는 공개된 PoC의 요청 형태를 막는 것이라 변형에는 우회될 수 있습니다. WAF는 패치 전까지의 방어막이자 탐지 수단으로 쓰고, 패치 일정은 따로 확정해야 합니다.

이미 공격당했는지 어떻게 알 수 있나요?

에러로그의 in Unknown on line 0 메모리 초과 오류, 같은 IP의 대용량 POST 반복, 웹서버 프로세스가 띄운 셸 명령, 웹 루트의 낯선 PHP 파일을 2026년 10월 20일 이후 기준으로 확인하세요. 하나라도 걸리면 침해사고 대응 절차로 넘어가야 합니다.

참고자료

댓글 남기기

이 사이트는 Akismet을 사용하여 스팸을 줄입니다. 댓글 데이터가 어떻게 처리되는지 알아보세요.