Programming/기본 (Baisc)

[PHP/그누보드] PHP2Shell 긴급 패치 대응기: EOL 정리부터 무중단 롤링 교체까지

YH.Dream 2026. 10. 22. 12:21

 PHP2Shell 긴급 패치 대응기 대표 이미지

PHP RCE 긴급 릴리스에 대비해 인벤토리부터 롤백까지 준비한 과정을 정리했습니다.

2026년 10월 20일, 연구자들이 “PHP2Shell”이라 부른 PHP 원격 코드 실행 취약점 2건의 공개 예고가 먼저 유출됐습니다.
같은 날 PHP는 다음 날 긴급 릴리스를 내겠다고 공지했고, 10월 21일 13:36경에는 예고 시각보다 이르게 패치 버전을 확인할 수 있었습니다.
디알소프트는 공개를 기다리는 동안 EOL 버전을 먼저 정리하고, 패치 공개 직후 소스 검증과 앱 단위 롤링 교체를 진행했습니다.

 


유출 글 확인부터 공식 공지, 패치 릴리스 확인까지의 UTC 기준 흐름입니다.

🚨 취약점 이해하기

이번 이슈는 CVE-2026-109618과 CVE-2026-109619 두 건입니다. “PHP2Shell”은 PHP 프로젝트의 공식 명칭이 아니라 연구자들이 붙인 이름입니다. 두 취약점 모두 use-after-free, 즉 이미 해제된 메모리를 다시 사용하는 문제이며 CWE-416으로 분류됩니다.

CVSS 4.0 기준 점수는 9.2로 치명적입니다. 인증 없이 네트워크에서 공격할 수 있지만 공격 복잡도는 높아, 실제 코드 실행까지 가려면 여러 조건이 맞아야 합니다. mod_php와 PHP-FPM 양쪽에서 웹 워커 권한의 원격 코드 실행이 검증됐습니다.

🧱 왜 다음 요청까지 문제가 이어졌나

PHP는 메모리가 부족해지면 longjmp()로 현재 요청 처리를 급히 빠져나갑니다. 그런데 요청 메모리가 완전히 초기화되기 전이거나 메모리 고갈 같은 치명 오류로 작업이 중간에 끊기면, 요청이 끝난 뒤에도 워커의 전역 상태가 이미 해제된 메모리를 가리킬 수 있습니다.

다음 요청이 그 값을 사용하면 use-after-free가 발생합니다. 공격자가 힙 배치를 정교하게 만들 수 있는 조건에서는 이 문제가 코드 실행으로 이어집니다. 공개 직후 실제 악용 보고는 없었지만, 2024년 PHP-CGI 취약점은 공개 후 24시간 안에 공격 시도가 관측된 전례가 있어 준비를 미룰 이유는 없었습니다.

📌 영향 버전과 패치 버전

브랜치 영향 범위 수정 버전
8.2 8.2.35 미만 8.2.35
8.3 8.3.36 미만 8.3.36
8.4 8.4.27 미만 8.4.27
8.5 8.5.12 미만 8.5.12
8.1 이하 지원 종료, 패치 없음 지원 브랜치로 전환




8.1 이하는 안전한 예외가 아니라 패치가 제공되지 않는 지원 종료 브랜치입니다.

공식 우회책은 없습니다. 어드바이저리는 패치가 유일한 완전한 해법이라고 명시합니다. memory_limit=-1, post_max_size 축소, 멀티파트 요청 차단 같은 보조책도 언급됐지만 완전하지 않으며, 메모리가 작은 서버에서 제한을 없애면 서버 전체 메모리 고갈 위험이 있습니다.

🧭 우리가 먼저 한 일: 범위를 알고 EOL부터 줄이기

첫 단계는 읽기 전용 인벤토리였습니다. 서버마다 php -v로 버전을, SAPI 정보를, php -m으로 모듈 목록을 확인했습니다. 공식 어드바이저리와 변경 이력도 함께 읽어 영향 범위와 수정 버전을 교차 확인했습니다.

환경은 크게 세 종류였습니다. 자체 웹 서버 3대는 소스로 빌드한 PHP 8.5.2를 사용했고, 웹 엔진이 앱마다 FastCGI 프로세스를 직접 띄우는 구조였습니다. 노드마다 앱은 52개였습니다.

VPS 3대에는 배포판 패키지 기반 PHP 8.0 또는 8.1이 있었고, 모두 EOL이었습니다. 한 대는 서드파티 저장소, 두 대는 배포판 기본 저장소를 사용했습니다. PHP 버전을 업체가 관리하는 공유 호스팅은 이번 직접 작업 범위에서 제외했습니다.

 


직접 관리 범위와 버전 공급 주체에 따라 대응 방법을 나눴습니다.

패치가 나오기 전에도 할 수 있는 일은 분명했습니다. EOL인 8.0과 8.1을 지원되는 8.3으로 먼저 올리고, 기존과 같은 모듈을 맞춘 뒤 서버별 확인 대상 40개, 10개, 12개 주소의 응답을 전환 전후로 비교했습니다. 상태 코드, 응답 크기, 오류 문자열이 성공 기준이었습니다.

🔧 패치 공개 후: 검증된 절차를 실행하기

10월 21일 13:36경 공식 릴리스를 확인한 뒤 8.5.12 소스를 받았습니다. 소스의 SHA256을 공식 값과 대조하고, 서버마다 낮은 우선순위로 빌드했습니다. 빌드는 약 7분 걸렸으며 기존 바이너리와 모듈 목록, configure 옵션, 링크 라이브러리를 비교했습니다.

php -v
php -m

웹 서버 3대에서는 앱 단위로 교체했습니다. 새 바이너리 디렉터리를 준비한 뒤 앱 프로세스 마스터를 하나씩 종료하면 웹 엔진의 관리 데몬이 2초 안에 새 바이너리로 다시 띄웠습니다. 서비스 전체를 재시작하지 않고 노드당 약 3분이 걸렸고, 노드는 한 대씩 순서대로 진행했습니다.


준비, 한 앱 교체, 자동 재기동, 응답 검증, 다음 앱 진행을 반복했습니다.

VPS 2대는 소스 빌드한 8.5.12를 별도 경로에 병렬 설치했습니다. 풀 설정과 소켓 경로는 그대로 두고 systemd drop-in에서 FPM 실행 파일만 바꿨으며, 검증에 실패하면 자동으로 이전 실행 파일로 돌아가도록 했습니다. 나머지 한 대는 서드파티 저장소에 패치판 8.3.36이 올라온 뒤 패키지를 업데이트했습니다.

[Service]
ExecStart=
ExecStart=/opt/php-8.5/sbin/php-fpm --nodaemonize --fpm-config /opt/php-8.5/etc/php-fpm.conf

전환의 완료 조건은 프로세스가 뜨는 것만이 아니었습니다. 모든 서버에서 전환 전후의 상태 코드, 응답 크기, 오류 문자열을 비교했습니다. 이전 디렉터리는 즉시 지우지 않아 롤백 경로도 유지했습니다.

⚠️ 실제로 밟은 함정 5가지



명령의 동작 방식, 운영 부하, 컴파일 기본값, 바이너리 차이, 숨은 의존성이 문제를 만들었습니다.

📌 1. php -l은 여러 파일을 모두 검사하지 않았습니다

증상: xargs -n40으로 파일을 묶어 실행한 뒤 “오류 0건”으로 보였습니다. 원인: php -l에 파일을 여러 개 넘겨도 첫 번째 파일만 검사했습니다. 해결: 검사 파일 수가 기대와 맞지 않음을 확인한 뒤 한 번에 한 파일씩 다시 검사했습니다.

find . -name '*.php' -print0 | xargs -0 -n1 php -l

교훈은 간단합니다. 린트 결과를 믿기 전에 성공 건수가 실제 검사 대상 파일 수와 같은지 확인해야 합니다.

📌 2. 운영 서버에서 린트를 과하게 병렬화했습니다

증상: 메모리 1.7GB인 소형 VPS에서 전체 파일 린트를 실행하자 부하 평균이 9.4까지 올랐습니다. 원인: 운영 서버에서 린트를 8병렬로 실행했습니다. 사이트는 정상 동작했지만 감수할 이유가 없는 부하였습니다.

해결: nice와 ionice로 우선순위를 낮추고 병렬 수를 2 이하로 제한했습니다. 가능하면 운영 서버와 분리된 환경에서 검사하는 편이 안전합니다.

📌 3. 소스 빌드의 기본값이 배포판 패키지와 달랐습니다

증상: localhost 소켓으로 데이터베이스에 연결하던 사이트가 모두 HTTP 500을 반환했습니다. 원인: 새 빌드에서는 mysqli.default_socket이 비어 있고 pdo_mysql.default_socket은 /tmp/mysql.sock이었습니다.

해결: 응답 비교가 실패를 감지해 자동 롤백했고, 배포판 PHP의 값을 읽어 ini 설정에 반영한 뒤 재전환했습니다. 소스 빌드에서는 소켓 경로와 include_path 같은 배포판 기본값을 반드시 비교하고 서버마다 스모크 테스트해야 합니다.

📌 4. 한 노드의 바이너리를 다른 노드에 복사할 수 없었습니다

증상: 한 노드에서 빌드한 바이너리를 다른 노드에 공통으로 배포할 수 없었습니다. 원인: 웹 서버 3대는 OS 마이너 버전과 링크 라이브러리 구성이 달랐고, 의존 라이브러리도 52개와 68개로 차이가 났습니다. 해결: 노드마다 따로 빌드했습니다.

교체 후에는 별도 FPM 서비스와 6월부터 남아 있던 고아 프로세스가 옛 바이너리를 계속 사용하고 있었습니다. /proc/<pid>/exe를 기준으로 전수 검색해 정리했고, “새 서비스가 떴다”와 “옛 바이너리가 사라졌다”를 별도 검증 항목으로 나눴습니다.

📌 5. EOL CLI가 배치 데몬의 의존성이었습니다

증상: 구버전 디렉터리를 바로 지우면 모든 배치가 멈출 수 있었습니다. 원인: 크론 배치 데몬이 EOL인 PHP 7.4 CLI로 실행되고, 배치도 데몬과 같은 PHP 바이너리인 PHP_BINARY를 사용했습니다.

해결: 삭제보다 의존성 확인을 먼저 했습니다. 데몬을 8.5로 옮기고 정시 배치가 실제로 성공하는 것을 확인한 뒤에야 구버전 디렉터리를 복구용 압축본과 함께 정리했습니다.

여기서 한 가지 더 만났습니다. 데몬을 옮기자 한 노드에서만 정상 종료한 배치가 “실패(코드 -1)”로 기록됐습니다. 같은 소스인데도 그 노드의 PHP만 빌드 옵션(--enable-sigchild)이 달라, 자식 프로세스 종료 코드가 사라진 것이었습니다. 다른 노드의 바이너리를 임시 경로에서 돌려 보고 시스템 호출을 추적해 범위를 좁힌 뒤, 정상 노드의 옵션으로 다시 빌드해 해결했습니다. 그래서 새 빌드에는 종료 코드를 확인하는 짧은 검사를 항상 넣습니다.

✅ 다음 PHP 보안 릴리스를 위한 체크리스트


다음 긴급 릴리스에서도 그대로 사용할 수 있도록 확인 순서를 남겼습니다.

  • 공식 변경 이력과 어드바이저리에서 영향 버전과 수정 버전을 확인하고, EOL에는 패치가 없음을 정리합니다.
  • 서버별 PHP 버전, SAPI, 모듈 인벤토리를 갱신합니다.
  • 서비스별 재시작 허용 범위를 사전에 합의합니다.
  • 공식 해시를 대조하고 노드별로 빌드한 뒤 기존 바이너리와 모듈, configure 옵션, 링크 라이브러리를 비교합니다.
  • 한 대씩 교체하며 전후 응답을 비교하고, 옛 바이너리 프로세스를 전수 검색합니다.
  • 이전 디렉터리를 보관하고 자동 롤백 스크립트를 준비합니다.
  • 사용한 문서와 스크립트를 저장소에 남깁니다.

🔒 마무리: 이번 대응에서 배운 세 가지

패치 공개 전에도 인벤토리, EOL 선제 업그레이드, 빌드 절차 검증까지 할 수 있었습니다. 덕분에 공개 후에는 이미 검증한 절차를 실행하는 데 집중할 수 있었습니다.

자동화 스크립트에는 실행 단계만이 아니라 성공 기준과 자동 롤백을 함께 넣어야 합니다. 이번에도 응답 비교가 컴파일 기본값 차이를 찾아 실제 장애로 이어지기 전에 되돌렸습니다.

마지막으로, “지워도 되겠지”라고 생각한 EOL 버전이 다른 작업의 의존성일 수 있습니다. 구버전 정리는 삭제보다 참조 확인과 대체 경로 검증이 먼저입니다.

📬 서버·PHP 보안 점검이 필요하시다면

서버 PHP 점

검과 긴급 패치 적용이 필요하시면 디알소프트로 문의주세요.

운영 중인 서버에서 PHP 버전이 어디까지 올라와 있는지, 지원이 끝난 버전이 남아 있지는 않은지 확인하고 싶으시다면 디알소프트가 함께 점검해 드립니다.

  • 🧭 운영 중인 서버의 PHP 버전과 EOL 여부 점검
  • 🔧 서비스 영향을 줄인 패치 적용과 롤백 준비
  • 🧪 전후 응답 비교로 검증

디알소프트로 문의주세요. ✉️ dr@drsoft.co.kr

사업자 정보 표시
디알소프트 | 유용훈 | 서울시 양천구 오목로 30길 | 사업자 등록번호 : 184-47-00657 | TEL : 02-2692-1002 | Mail : gkioo@daum.net | 통신판매신고번호 : 호 | 사이버몰의 이용약관 바로가기