PHP2Shell 원격 코드 실행, 패치 확인법과 8.1 이하 서버 대응
PHP2Shell(CVE-2026-109618·CVE-2026-109619)은 앱 코드가 아니라 PHP 엔진 안의 메모리 처리 결함이다. 그래서 웹 요청을 받는 PHP 라면 그 위에 무엇이 올라가 있든 대상이 된다. 10월 21일 나온 지원 브랜치(8.2~8.5) 보안 릴리스로 올리고 워커를 모두 다시 띄워야 막힌다. 8.1 이하는 배포판 백포트를 받거나, 보안팀이 공개한 소스 백포트를 직접 빌드하거나, 지원 버전으로 옮겨야 한다. session.upload_progress.enabled=0 같은 설정은 확인된 경로 몇 개만 줄이는 임시책이다.
01 2024년 감사에서 보인 결함이 두 달 공조 끝에 고쳐졌다
뿌리는 메모리 부족(OOM, out of memory) 처리다. PHP Foundation 글에 따르면 이 문제는 2024년 PHP 가 몇 년 만에 처음 받은 외부 보안 감사 때 이미 징후가 잡혔다. Sovereign Tech Agency 가 비용을 대고 OSTIF 가 꾸리고 Quarkslab 이 수행한 감사였는데, Quarkslab 이 보고한 문제를 고치던 코어 개발자가 OOM 문제의 첫 징후를 발견했다. 다만 그때는 실제 공격 경로를 찾지 못했다. 고치는 방식을 두고는 후보마다 호환성 깨짐이나 새 서비스 거부 위험 같은 대가가 따랐고, 결정이 늦어지면서 작업이 몇 달에 걸쳐 멈췄다가 다시 시작되기를 되풀이했다. Foundation 은 이 지연을 "구조적 실패"라고 스스로 적었다.
올여름 신고가 몰리면서 사정이 바뀌었다. Calif 는 7월 30일 PHP 감사를 시작해 8월 11일 같은 근본 원인으로 조건 없는 원격 실행을 신고했고, OpenAI 도 같은 날 같은 원인을 알렸다. Wiz 는 8월 19일 첫 버그를 찾아 24일 보안팀에 넘겼다. 찾는 데는 자체 도구와 함께 Gemini 4 Argon, GPT-5.6 Sol 을 썼다. OpenAI 는 이 대응을 유지관리자·연구자·보안업체를 잇는 Patch the Planet 공조의 사례로 소개했지만, 패치를 만들어 내보낸 쪽은 PHP 유지관리자였다.
AI 도구로 찾은 신고가 늘면서 PHP 보안팀이 감당할 양도 커졌다. 보안팀은 7월 이후에만 CVE 15건을 냈고, 무효·중복·재현 불가로 걸러 낸 신고도 100건을 넘었다고 Foundation 은 밝혔다.
공개는 예정보다 하루 당겨졌다. 10월 20일 오후(UTC) CVE 번호가 먼저 퍼졌는데, 실제 CVSS 9.2 가 아니라 10.0 이라는 틀린 점수가 붙어 있었다. 보안팀은 릴리스를 하루 당겨 21일 14:00 UTC 에 내놓았다. 그 사이 PoC(개념 증명) 시연 영상이 실수로 공개됐고, 한 이미지 배포 업체는 약속한 시각보다 6시간 넘게 앞서 수정 빌드를 올렸다. 네덜란드 NCSC 는 패치가 나오기 전 자기 웹사이트를 예방 차원에서 내렸다. AI타임스는 네덜란드·덴마크·핀란드·독일·프랑스의 공공기관과 병원, 학교 사이트가 줄줄이 문을 닫았다고 전했다.
02 두 결함 모두 중간에 끊긴 요청이 남긴 엔진 상태에서 시작한다
| CVE | 공식 이름 | 남는 것 | 심각도 |
|---|---|---|---|
| CVE-2026-109618 | RCE via OOM corruption | 메모리 한도에 걸려 중간에 빠져나온 요청 데이터가 반쯤 초기화된 채 정리 단계로 넘어간다 | CVSS v4.0 9.2 |
| CVE-2026-109619 | RCE via stale global state | 이미 해제된 요청 메모리를 가리키는 전역 상태가 같은 워커의 다음 요청까지 살아남는다 | CVSS v4.0 9.2 |
CVE-2026-109618 부터 보면, PHP 는 OOM 이 나면 longjmp() 로 지금 맥락을 한 번에 빠져나온다. 요청 메모리가 다 채워지기 전에 이 탈출이 일어나면, 요청 종료 처리가 반쪽짜리 값을 정상으로 믿고 읽는다. 공격자는 이 OOM 을 일부러 만든다. GHSA 공지를 보면 쓸 만한 Content-Length 가 없는 multipart 요청은 첫 post_max_size 검사를 지나 본문을 계속 보낼 수 있다. 파트마다 헤더를 무제한 붙이면 회선에 실린 양보다 PHP 안에서 쓰는 메모리가 훨씬 크게 불어난다.
CVE-2026-109619 는 두 요청이 짝을 이뤄야 성립한다. 앞 요청에서 치명적 오류(파싱 실패, 잡히지 않은 오류, 실행 시간 초과)나 OOM 이 상태를 남긴다. 같은 워커가 받은 다음 요청에서는 정상 초기화보다 먼저 도는 코드가 그 상태를 읽는다. 읽는 쪽으로 확인된 것은 INI 설정을 적용할 때 나오는 경고와 세션 업로드 진행률 처리다. 업로드 진행률 처리는 기본으로 켜져 있고, 앱이 session_start() 를 부르지 않아도 돈다.
Wiz 는 이 결함들로 wordpress:latest·drupal:latest·joomla:latest 컨테이너 이미지에서 인증 없는 원격 실행을 시연했다. 해제된 메모리 내용이 PHPSESSID 응답 쿠키에 실려 나오는 유출 경로도 보였는데, Wiz 는 이것만으로 비밀번호·인증 토큰·API 키가 샐 수 있다고 썼다. 영향 범위는 Wiz 와 Cloudflare 모두 5.4 이후 거의 모든 버전으로 잡았다. php.net 공지는 범위를 따로 적지 않고 고친 릴리스만 밝혔다.
위험도는 실행 방식(SAPI, PHP 를 웹 서버에 붙이는 방식)에 따라 다르다. GHSA 두 건을 기준으로 정리하면 아래와 같다.
| 실행 방식 | 공지가 밝힌 상태 |
|---|---|
| Apache mod_php | 시작이 실패해도 종료 처리를 부른다. 앱 코드가 돌기 전에 OOM 을 일으키는 경로로, 기본 설정에서 원격 실행이 검증됐다 |
| PHP-FPM | 시작에 실패하면 자식 프로세스를 끝내 mod_php 경로는 막힌다. 정상 시작 뒤 OOM 은 남아 있지만 이 경로로 FPM 을 끝까지 공략한 사례는 아직 없다. 업로드 진행률 경로(109619)는 FPM 에서도 검증됐다 |
| FrankenPHP | 소스 분석상 CVE-2026-109618 에는 mod_php 처럼 취약할 가능성이 있다 |
| LiteSpeed(LSAPI) | FPM 을 본뜬 구조라 앱에 따라 가능성이 있으나 검증되지 않았다 |
| php-cgi | 웹 요청을 받는 설치로 php.net 공지의 대상 목록에 들어 있다 |
수정판은 OOM 에서 longjmp() 를 걷어냈다. 한도를 넘으면 다음 VM 안전 지점에서 정상적으로 멈추는 soft OOM 이 걸리고, 그보다 높은 두 번째 문턱에 닿으면 _exit() 로 프로세스를 바로 끝낸다. 메모리 한도는 그대로 지키면서, 종료 처리가 반쪽짜리 상태를 볼 일이 사라진 셈이다.
03 고쳐졌는지는 php -v 숫자보다 PHP 를 받은 경로로 가린다
PHP 프로젝트가 낸 수정 릴리스는 브랜치마다 버전 셋째 자리가 다르다(2026년 10월 php.net 공지 기준).
| 브랜치 | 고쳐진 릴리스(셋째 자리) | 덧붙일 것 |
|---|---|---|
| 8.5 | 12 이상 | CVE-2026-109619 의 가장 확실한 경로(fatal_error_backtraces)가 8.5 에만 있다 |
| 8.4 | 27 이상 | — |
| 8.3 | 36 이상 | — |
| 8.2 | 35 이상 | 2026년 12월 31일 보안 지원이 끝난다 |
| 8.1 이하 | 공식 릴리스 없음 | 다음 절 |
소스로 빌드했거나 공식 Docker php 이미지처럼 upstream 번호를 그대로 쓰면 php -v 의 셋째 자리만 보면 된다. 배포판 패키지는 사정이 다르다. Ubuntu 는 USN-8948-1 로 24.04 의 php8.3, 22.04 의 php8.1 에 수정을 백포트했다(26.04·26.10 의 php8.5 도 같은 공지에 있다). 이런 패키지는 고친 뒤에도 php -v 에 예전 upstream 번호가 찍히므로, 패키지 버전 문자열을 USN 같은 배포판 공지와 견줘야 한다.
반대쪽 함정도 있다. Laravel Forge 문서는 Ubuntu 의 무인 업데이트(unattended-upgrades)가 Ubuntu 보안 저장소에서 온 패키지만 올린다고 짚는다. PHP 를 외부 PPA 에서 받아 깔았다면, 자동 보안 업데이트를 켜 두었어도 PHP 는 옛 버전에 머문다. Forge 는 사이트가 쓰지 않는 버전까지 서버에 깔린 PHP 버전마다 따로 올리라고 안내한다. apt-cache policy 의 Candidate 가 고친 버전과 다르면 저장소에 아직 패키지가 올라오지 않은 것이니, 기다렸다가 다시 하면 된다. RHEL·Rocky·Alma 계열에서 흔히 쓰는 Remi 저장소(rpms.remirepo.net)는 자원봉사로 운영되는데, php.net 공지는 릴리스 당일 이 저장소에 업데이트가 없다며 배포판 패키지로 바꾸거나 소스로 빌드하라고 권했다.
# 설치된 PHP 브랜치 (Debian·Ubuntu)
ls /etc/php
# CLI 와 FPM 을 따로 본다. 8.3 은 실제 브랜치로 바꾼다
php8.3 -v
php-fpm8.3 -v
# 저장소에 고친 패키지가 올라왔는지
sudo apt-get update
apt-cache policy php8.3-fpm패키지를 올려도 이미 떠 있는 프로세스는 옛 코드를 계속 쓴다. FPM 이나 Apache 를 재시작하고, 라라벨이라면 큐 워커·Horizon·Octane 처럼 오래 도는 CLI 프로세스도 다시 띄운다.
php artisan queue:restart
php artisan horizon:terminate
php artisan octane:reloadDocker 라면 베이스 이미지가 실제로 바뀌었는지부터 본다. 공식 php 이미지는 docker-library 쪽 PR 이 병합된 뒤에도 official-images 반영과 빌드를 더 거쳐야 태그가 바뀐다. 21일 당시 wordpress:fpm 이 아직 고치기 전의 8.3 을 싣고 있었다는 지적도 나왔다. 이미지를 다시 받아 빌드한 뒤 컨테이너 안에서 php -v 로 확인하는 편이 확실하다.
04 8.1 이하는 배포판 백포트가 먼저, 직접 빌드는 최소 완화다
PHP 프로젝트는 8.1 이하에 더는 릴리스를 내지 않는다. 그렇다고 손쓸 방법이 없지는 않고, PHP 를 어디서 받았는지에 따라 길이 갈린다.
첫째는 배포판·LTS 업체의 백포트다. Ubuntu 22.04 기본 저장소의 php8.1 은 위의 USN-8948-1 로 고쳐졌다. 다른 배포판이나 유료 LTS 지원을 받는 중이라면 그 업체 공지를 확인한다.
둘째는 PHP 보안팀이 공개한 7.4·8.0·8.1 백포트 브랜치를 직접 빌드하는 길이다. 8.1 브랜치의 해당 커밋은 설명에서 스스로를 지원 끝난 버전용 최소 완화로 규정한다. 바뀐 내용을 보면 메모리 한도 초과나 크기 넘침이 생겨도 일반 오류 처리를 거치지 않고, 표준 오류에 치명적 오류를 찍은 뒤 _exit(1) 로 프로세스를 바로 끝낸다. 지원 브랜치처럼 다음 안전 지점까지 기다렸다 정리하는 soft OOM 이 없어서, 메모리를 많이 쓰는 요청은 정상 종료 절차 없이 워커째 끝난다. 큰 업로드나 가져오기 기능이 있다면 이 동작을 먼저 시험해 봐야 한다.
셋째는 지원 브랜치로 옮기는 것이고, 가장 확실하다. 8.2 도 올해 말에 보안 지원이 끝나니 옮긴다면 8.3 이상이 낫다. CISA 는 8.1 이하를 쓰거나 당장 업데이트하지 못하는 조직이라면 PHP 시스템을 오프라인으로 두라고 권했다. 남의 일이 아니다. AI타임스가 전한 Censys 집계로는 관측한 약 260만 개 호스트 가운데 약 150만 개가 수정본을 받기 어려운 PHP 버전이었다.
05 임시 완화책은 확인된 경로만 막고, WAF 는 한 겹일 뿐이다
php.net 릴리스 공지는 "완전한 완화책은 없고 업데이트만이 안전한 선택"이라고 못 박았다. 아래 설정은 바로 올리지 못하는 서버에서 시간을 버는 용도다.
| 설정 | 겨냥하는 결함 | 한계와 주의 |
|---|---|---|
memory_limit=-1 을 FPM pool·Apache 의 php_admin_value 로 고정 | 109618 | mmap() 실패 같은 다른 할당 실패는 같은 경로로 간다 |
post_max_size 를 업로드가 없으면 8M, 있으면 32M 로 | 109618 | 프록시에서 디코딩 크기·필드 수·파트 수·헤더 수·중첩 깊이를 따로 묶고, Content-Length 없는 요청도 덮는다 |
| 앱이 쓰지 않으면 multipart 요청 차단 | 109618 | 알려진 PoC 경로 하나를 막을 뿐이다 |
session.upload_progress.enabled=0 뒤 워커 전체 재시작 | 109619 | 세션 경로만 막는다. ini_set()·FPM .user.ini 로 끄는 데 기대면 안 된다 |
fatal_error_backtraces=0 (8.5 만) | 109619 | 백트레이스 경로만 막는다 |
pm.max_requests=1 | 109619 | 워커 재사용을 없애는 대신 성능을 치른다 |
; php.ini — 요청을 파싱하기 전에 읽히는 곳에 둔다
session.upload_progress.enabled = 0
post_max_size = 8M
; PHP 8.5 만
fatal_error_backtraces = 0; FPM pool 설정 — admin 값은 앱이 ini_set() 으로 되돌리지 못한다
php_admin_value[memory_limit] = -1memory_limit=-1 은 요청 하나가 메모리를 끝없이 먹을 수 있다는 뜻이기도 하다. 공지는 컨테이너나 cgroup 메모리 한도를 쓰되, 영향받은 워커만 죽도록 묶으라고 한다. GHSA 는 컨테이너 하나에 한도를 걸면 그 컨테이너가 죽을 때 FPM 마스터와 워커가 함께 내려갈 수 있으니 워커마다 cgroup 을 나누라고 짚는다. 거꾸로 RLIMIT_AS·RLIMIT_DATA 로 프로세스 메모리를 묶으면 그 한도에서 할당이 실패해 오히려 취약 경로로 들어간다. error_reporting 을 낮추거나 display_errors 를 끄거나 @ 로 경고를 숨기는 것도 소용없다. INI 경고가 만들어지는 순간 남은 상태를 읽기 때문이다. GHSA 는 패치를 마친 뒤에도 업로드 진행률 기능을 쓰지 않는다면 꺼 둔 채 두라고 권한다.
Cloudflare 를 앞에 두었다면 WAF 가 한 겹 더 있다. Cloudflare 는 공개 약 9시간 전에 규칙을 올렸다. Free 플랜은 Managed Free Ruleset 으로 따로 손댈 것이 없고, 유료 플랜은 Managed Ruleset 이 차단 모드로 배포돼 있는지 확인해야 한다. 이번 규칙은 요청뿐 아니라 같은 트랜잭션의 오리진 응답까지 보고 공격 성공 여부를 가리는 방식을 처음 쓴 관리형 탐지다. 다만 Cloudflare 스스로 공격자가 규칙을 피하려고 페이로드를 바꾸는 모습을 봤다고 밝혔으니, 패치를 미룰 근거는 못 된다. 무료 플랜에서 되는 것과 안 되는 것은 Cloudflare 무료 플랜으로 어디까지 쓸 수 있나에 따로 정리했다.
06 한국 PHP 서버 운영자는 외부에 열린 서버부터 올린다
대상은 넓다. 그누보드·워드프레스·라라벨처럼 PHP 위에서 도는 사이트라면 앱 버전과 상관없이 서버의 PHP 를 봐야 한다. 워드프레스 자동 업데이트를 켜 두었어도 PHP 자체는 바뀌지 않는다. PHP 를 직접 바꿀 수 없는 웹호스팅이라면 호스팅사가 고친 패키지를 깔고 재시작까지 마쳤는지 묻는 것이 첫 단계다. Node·Go·Java 처럼 PHP 를 쓰지 않는 스택이라면 이번 건으로 바꿀 것은 없다. 다만 CISA 지적처럼 다른 제품에 묶여 들어온 PHP 는 눈에 잘 안 띄니 목록을 한 번 훑는다.
시간은 공격자 쪽으로 기운다. 21일 공개 시점에 CISA 는 실제 악용을 확인하지 못했지만, Cloudflare 는 초기 공격 시도와 규칙 회피 변형을 관찰했다. 수정본이 공개된 뒤에는 공격자도 수정 전후 차이를 읽어 아직 올리지 않은 서버를 찾아 나설 수 있다. 이번 주 안에 아래 순서로 정리하면 된다.
- 웹 요청을 받는 PHP 를 모두 찾는다. FPM·mod_php·php-cgi·FrankenPHP·LiteSpeed 와 다른 제품 안의 PHP 까지 넣는다.
- 외부에 열린 서버부터 고친 패키지로 올리고, 배포판 패키지는 USN 같은 공지의 패키지 버전과 견준다.
- FPM·Apache 와 오래 도는 CLI 프로세스를 모두 재시작한 뒤 FPM 바이너리 버전을 다시 찍어 본다.
- 바로 못 올리는 서버는 완화 설정과 WAF 를 걸고, 8.1 이하라면 외부 접속 차단까지 검토한다.
- multipart 요청 직후의 메모리 한도 오류, 반복되는 500, 지나치게 크거나 깨진
PHPSESSID쿠키, 이유 없는 워커 재시작을 웹 서버·PHP 워커 로그에서 찾는다. - PHP 워커가 셸을 띄우거나 도구를 내려받은 흔적이 있으면 침해 점검과 함께 비밀번호·API 키 교체를 검토한다.
- 8.2 서버는 12월 31일 지원 종료 전에 8.3 이상으로 옮길 일정을 잡는다.
앱 로그에는 흔적이 거의 남지 않을 수 있다. 공격이 앱보다 아래층에서 일어나기 때문에, Wiz 는 웹 서버와 PHP 워커 수준의 기록을 보라고 했다. 메모리 유출로 키가 샜을 가능성을 배제하지 못한다면, 교체 비용이 침해 비용보다 싸다.
07 자주 묻는 질문
Ubuntu 22.04 에서 php -v 가 8.1.2 로 나오면 아직 취약한가?
숫자만으로는 알 수 없다. 기본 저장소의 php8.1 이라면 USN-8948-1 의 고친 패키지 버전은 8.1.2-1ubuntu2.28 이고, 24.04 의 php8.3 은 8.3.6-0ubuntu0.24.04.13 이다. apt-cache policy php8.1-fpm 의 설치 버전이 이 값 이상인지 본다. 외부 PPA 에서 받은 PHP 라면 그 저장소의 버전 번호를 따로 확인한다.
Cloudflare Free 플랜 규칙이면 두 CVE 를 다 막아 주나?
Cloudflare 글은 규칙을 CVE 별로 짝짓지 않았다. Free 플랜에 들어간 것은 "비정상 실행"을 잡는 일반 규칙 묶음이라, 특정 CVE 의 모든 변형을 막는다고 기대할 근거는 없다. 패치 전까지 버티는 보조 수단으로만 본다.
패치한 뒤 session.upload_progress 를 다시 켜도 되나?
진행률 표시가 꼭 필요한 앱만 켠다. GHSA 는 이 기능을 거의 쓰이지 않고 시험도 덜 된 영역으로 보고, 패치가 상태 초기화를 넓혔어도 같은 종류의 문제가 되풀이될 수 있는 곳이라고 적었다.