2026년 10월 21일(UTC), PHP 개발팀이 치명적인 보안 취약점 두 건을 고친 긴급 업데이트를 발표했습니다. 번호는 CVE-2026-109618과 CVE-2026-109619이고, 보안 업계에서는 둘을 묶어 PHP2Shell이라고 부릅니다. PHP를 통해 서버의 셸(명령창)을 빼앗을 수 있다는 뜻입니다.
PHP는 워드프레스, 그누보드, 쇼핑몰 솔루션부터 회사 ERP와 관리자 페이지까지 수많은 웹사이트를 움직이는 언어입니다. 서버 쪽 언어를 알 수 있는 전 세계 웹사이트의 70% 이상이 PHP를 쓴다는 조사(W3Techs)가 있을 정도입니다. 이번 글에서는 개발자가 아닌 분도 이해할 수 있게 취약점을 풀어 설명하고, 버전별 패치 방법과 구버전을 쓸 때의 조치, 이번 일을 계기로 다시 살펴볼 만한 세션과 쿠키의 차이, 그리고 케네스랩이 관리하는 서버를 어떻게 패치했는지 정리합니다.
- 무엇이 — 로그인 없이 인터넷에서 요청을 보내는 것만으로 서버를 장악할 수 있는 PHP 엔진 자체의 결함입니다. 위험도는 10점 만점에 9.2점, '치명적' 등급입니다.
- 누가 — PHP 8.2~8.5 중 수정판 이전 버전 전부, 그리고 지원이 끝난 8.1 이하 전부가 해당됩니다.
- 어떻게 — 8.5.12, 8.4.27, 8.3.36, 8.2.35 이상으로 올리고 PHP를 다시 시작합니다.
- 언제 — 지금입니다. 공식 발표 전에 내용이 새어 나갔고, 실제 공격 시도가 이미 관측되고 있습니다.
1. PHP2Shell, 무엇이 문제인가
두 취약점은 워드프레스 플러그인 같은 특정 프로그램의 버그가 아니라 PHP 엔진 자체의 결함입니다. 그래서 PHP 위에서 무엇을 돌리든, 코드를 얼마나 잘 짰든 영향을 받습니다. 보안 위험도 점수(CVSS)는 둘 다 9.2점입니다.
공격 결과도 같습니다. 공격자가 로그인하지 않고, 조작한 요청을 인터넷으로 보내는 것만으로 서버에서 자기 명령을 실행할 수 있습니다. 이를 원격 코드 실행(RCE)이라고 부릅니다. 서버에 몰래 뒷문(웹셸)을 심거나, 설정 파일에 든 DB 비밀번호를 가져가거나, 서버 메모리에 남은 다른 사용자의 비밀번호·토큰·API 키를 엿볼 수 있습니다.
CVE-2026-109618 — 메모리가 꽉 찼을 때의 뒷정리 실수
PHP 서버를 식당 주방이라고 생각해 보겠습니다. 손님(방문자)의 주문(요청)이 들어오면 요리사(PHP)가 조리대(메모리)를 써서 요리를 만듭니다. 주문 하나가 쓸 수 있는 조리대 크기(memory_limit)는 정해져 있고, 이를 넘으면 요리사는 하던 일을 그 자리에서 내려놓고 곧장 뒷정리로 넘어갑니다.
문제는 요리가 반쯤만 준비된 순간에 조리대가 넘칠 때입니다. 뒷정리 담당은 반쯤 된 접시를 다 된 접시로 알고 치우는데, 그 접시에는 공격자가 미리 끼워 둔 쪽지가 섞여 있습니다. 뒷정리 담당이 그 쪽지를 지시로 읽어 버리면 주방은 공격자 손에 넘어갑니다.
공격자는 조리대를 일부러 넘치게 만드는 방법도 알고 있습니다. 파일 업로드에 쓰는 요청 형식(multipart)에 헤더를 끝없이 붙이거나, 빈 파일 칸과 겹겹이 중첩된 이름을 잔뜩 넣는 식입니다. 전송되는 크기는 작아도 PHP 안에서는 메모리를 크게 차지하기 때문에, 업로드 크기 제한(post_max_size) 안에서도 메모리 한도를 넘길 수 있습니다. 특히 Apache의 mod_php 방식에서는 웹사이트 코드가 한 줄도 실행되기 전에 공격이 성립한다는 것이 확인되었습니다.
CVE-2026-109619 — 치우지 않은 테이블에 다음 손님을 앉히는 실수
바쁜 식당은 테이블을 계속 돌려 씁니다. PHP도 속도를 위해 요청을 처리하는 작업자(worker)를 매번 새로 만들지 않고 재사용합니다. 정상이라면 손님이 떠날 때 테이블을 깨끗이 치우고 다음 손님을 받습니다.
그런데 요청이 치명적인 오류(시간 초과, 메모리 초과, 문법 오류 등)로 중간에 끝나면 일부 정보가 치워지지 않고 남습니다. 테이블은 이미 반납했는데 "3번 테이블에 내 물건이 있다"는 메모만 남은 셈입니다. 다음 요청이 그 메모를 믿고 3번 테이블을 건드리면, 이미 다른 손님이 앉은 자리를 뒤지게 됩니다. 공격자는 첫 요청으로 이런 메모를 남기고, 같은 작업자에게 닿는 다음 요청으로 그 자리를 건드려 메모리를 조작합니다.
확인된 공격 경로 중 하나가 세션 업로드 진행률(session.upload_progress) 기능입니다. 파일 업로드가 몇 퍼센트 진행됐는지 알려 주는 기능인데, PHP에 기본으로 켜져 있고, 웹사이트가 세션을 쓰지 않아도(session_start()를 부르지 않아도) 동작합니다. 대부분의 운영자가 있는 줄도 모르던 기능이 공격 통로가 된 것입니다. PHP 8.5에서는 치명적 오류의 호출 기록을 남기는 fatal_error_backtraces 설정도 확인된 경로입니다.
한눈에 보기
| CVE-2026-109618 | CVE-2026-109619 | |
|---|---|---|
| 한 줄 요약 | 메모리 초과를 처리하다 메모리가 손상됨 | 이미 반납한 메모리를 가리키는 정보가 다음 요청까지 남음 |
| 비유 | 반쯤 만든 요리를 다 된 것으로 알고 치움 | 치우지 않은 테이블에 다음 손님을 앉힘 |
| 주요 통로 | 파일 업로드 형식(multipart) 요청으로 메모리 초과 유도 | 치명적 오류 뒤의 세션 업로드 진행률, 요청별 PHP 설정 경고 |
| 위험도 | CVSS 9.2 (치명적) | CVSS 9.2 (치명적) |
| 결과 | 로그인 없이 원격 코드 실행 | 로그인 없이 원격 코드 실행, 메모리 내용 노출 |
누가 위험한가
- 버전 — PHP 8.2.35, 8.3.36, 8.4.27, 8.5.12 미만이 모두 해당됩니다. 지원이 끝난 8.1 이하(7.x, 5.x 포함)도 모두 취약하며, PHP 팀의 정식 수정판은 나오지 않습니다.
- 실행 방식 — 웹 요청을 처리하는 모든 방식이 해당됩니다. Apache mod_php가 가장 위험하고, nginx와 함께 많이 쓰는 PHP-FPM, php-cgi, FrankenPHP, LiteSpeed도 대상입니다. PHP-FPM은 구조상 일부 경로가 막혀 있지만, 세션 업로드 진행률 경로는 FPM에서도 공격이 검증되었습니다.
- PHP를 품은 제품 — 겉으로는 PHP가 보이지 않아도 안에 PHP를 넣어 둔 관리 솔루션, 장비의 관리 화면 등도 해당될 수 있습니다. 제조사의 업데이트 공지를 확인해야 합니다.
얼마나 급한가
공식 발표 전인 10월 20일에 내용이 먼저 새어 나갔고, PHP 팀은 서둘러 다음 날 수정판을 냈습니다. 공개 직후부터 watchTowr, Cloudflare, Amazon 등이 실제 공격 시도를 관측했다고 밝혔고, 유럽연합 사이버보안기구(CERT-EU)는 공격 코드(PoC)가 돌기 시작해 이미 악용되고 있을 가능성이 높다고 평가했습니다. 인터넷에 열린 PHP 서버라면 "언젠가"가 아니라 "오늘" 패치해야 합니다.
"패치된 버전으로 업데이트하는 것만이 유일하게 완전한 대책입니다." — PHP 보안 권고(GHSA-w8hw-w9fx-j79w)
2. 패치하는 방법
고친 버전
| 쓰고 있는 계열 | 이 버전 이상으로 |
|---|---|
| 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 이하 | 정식 수정판 없음 — 아래 '구버전을 쓰고 있다면' 참고 |
1단계 — 지금 버전 확인
php -v # 명령줄 PHP 버전
php-fpm -v # 웹에서 쓰는 PHP-FPM 버전 (배포판에 따라 php-fpm8.3 -v 처럼 버전이 붙습니다)
명령줄의 PHP와 웹 서버가 쓰는 PHP가 다를 수 있습니다. 한 서버에 여러 버전이 깔려 있거나 Docker 컨테이너 안에 따로 있는 경우도 많으니, 실제로 웹 요청을 처리하는 PHP의 버전을 확인해야 합니다.
주의할 점이 하나 더 있습니다. Red Hat, Debian, Ubuntu 같은 배포판은 버전 번호는 그대로 두고 보안 수정만 옮겨 넣기도(백포트) 합니다. 이 경우 버전이 낮게 보여도 이미 고쳐진 것일 수 있으니, 패키지 변경 기록에 CVE 번호가 있는지로 확인합니다.
# RHEL·Rocky·AlmaLinux·Amazon Linux
rpm -qa --changelog 'php*' | grep CVE-2026-10961
# Debian·Ubuntu
zgrep CVE-2026-10961 /usr/share/doc/php*/changelog.Debian.gz
2단계 — 패키지 업데이트
# Amazon Linux 2023 — 저장소 버전이 고정돼 있어 latest 를 줘야 새 보안 패키지가 보입니다
sudo dnf --releasever=latest upgrade 'php*'
# RHEL·Rocky·AlmaLinux
sudo dnf upgrade 'php*'
# Ubuntu·Debian (다른 패키지도 함께 올라갑니다)
sudo apt update && sudo apt upgrade
- 외부 저장소(ondrej PPA, remi 등)에서 PHP를 받았다면, 그 저장소에 수정판이 올라왔는지 확인합니다. remi 저장소는 공개 당일 수정 패키지가 늦었다는 공지(CERT-EU)가 있었습니다.
- Docker를 쓴다면 공식 이미지(
php:8.4-fpm등)를 다시 받아 이미지를 새로 빌드하고 컨테이너를 교체합니다. 기존 컨테이너를 재시작하는 것만으로는 바뀌지 않습니다. - Windows라면 windows.php.net에서 새 빌드를 받아 교체하고 IIS나 Apache를 다시 시작합니다.
- 웹호스팅을 쓴다면 서버의 PHP는 호스팅 업체가 올려야 합니다. 관리 화면에서 PHP 버전을 고를 수 있다면 지원 중인 최신 버전으로 바꾸고, 업체 공지를 확인합니다.
3단계 — 반드시 다시 시작
가장 많이 빠뜨리는 단계입니다. 패키지를 올려도 이미 떠 있는 PHP 프로세스는 메모리에 올라가 있는 옛 코드로 계속 돕니다. PHP-FPM과 웹 서버를 다시 시작해야 고친 코드가 적용됩니다. 재시작하는 순간 사이트가 1~2초 끊길 수 있습니다.
sudo systemctl restart php-fpm # 배포판에 따라 php8.3-fpm 처럼 버전이 붙습니다
sudo systemctl restart httpd # mod_php 를 쓰는 Apache (Debian·Ubuntu 는 apache2)
# 옛 코드로 도는 프로세스가 남았는지 확인 — 아무것도 나오지 않아야 정상입니다
sudo grep -l 'php.*(deleted)' /proc/[0-9]*/maps 2>/dev/null
4단계 — 흔적 점검
패치 전에 이미 공격을 받았을 수도 있습니다. 공격자는 보통 웹 서버 권한으로 PHP 파일(웹셸)을 몰래 만들어 두므로, 내용이 새어 나간 10월 20일 이후 웹 서버 계정이 만들거나 고친 PHP 파일이 있는지 살펴봅니다. 메모리 손상 공격은 실패하면 PHP 작업자가 비정상 종료되므로 그 기록도 단서가 됩니다.
# 10월 20일 이후 웹 서버 계정(nginx·apache·www-data 등)이 만든 PHP 파일 — 경로는 서버에 맞게
sudo find /var/www /tmp /var/tmp /dev/shm -type f -user nginx -newermt 2026-10-20 \
\( -name '*.php*' -o -name '.htaccess' -o -name '.user.ini' \)
# PHP 비정상 종료와 메모리 초과 기록
sudo journalctl --since 2026-10-20 | grep -E 'php.*(segfault|dumped core)'
sudo grep -E 'exited on signal (6|7|11)' /var/log/php-fpm/*.log
sudo grep 'Allowed memory size' /var/log/php-fpm/*.log /var/log/nginx/error.log
무언가 나왔다면 지우기 전에 로그와 파일을 먼저 보관하고 전문가와 상의하세요. 고치기 전에 증거부터 남기는 것이 원칙입니다.
구버전(8.1 이하)을 쓰고 있다면
PHP 8.1 이하는 이미 공식 지원이 끝나 PHP 팀이 수정판을 내지 않습니다. 하지만 국내에는 아직 PHP 7.x, 심지어 5.x로 도는 사이트도 적지 않습니다. 다음 순서로 검토합니다.
- 지원 버전으로 올리기 (가장 좋은 방법) — 8.4나 8.5로 올리면 이번 문제뿐 아니라 앞으로 나올 보안 수정도 받을 수 있습니다. 다만 7.x에서 8.x로 넘어갈 때는 바뀐 문법과 함수 때문에 코드 수정이 필요할 수 있으니, 검증 서버에서 먼저 돌려 보는 것이 안전합니다.
- 운영체제가 주는 수정 패키지 쓰기 — 운영체제가 아직 지원 기간이라면, 그 운영체제에 들어 있는 PHP는 구버전이라도 공급사가 고쳐 줍니다. Red Hat은 PHP 7.4 패키지를 고친 RHSA-2026:87080을, Ubuntu(USN-8948)와 Debian(DSA-6574-1)도 보안 공지를 냈습니다. 위 2단계의 업데이트 명령만으로 해결될 수 있으니, 1단계의 CVE 확인 명령으로 확인합니다.
- 직접 빌드하기 — 사안의 심각성 때문에 PHP 보안팀이 7.4, 8.0, 8.1용 최소 수정 소스를 따로 공개했습니다(php-src 저장소의
PHP-7.4-CVE-2026-109618-109619같은 브랜치). 소스에서 직접 빌드해 쓰는 곳이라면 이 패치를 적용해 다시 빌드합니다. - PHP 7.3 이하·5.x — 정식 수정판도 수정 소스도 없습니다. 업그레이드 말고는 근본적인 해결책이 없으니, 그때까지는 아래 임시 조치와 함께 외부 접근을 최대한 줄여야 합니다.
당장 올릴 수 없을 때의 임시 조치
아래 설정은 확인된 공격 경로 일부를 막아 시간을 버는 용도일 뿐이며, 서버를 안전하게 만들어 주지는 않습니다. 설정은 요청을 처리하기 전에 적용되는 곳(php.ini, FPM 풀 설정, 웹 서버 설정)에 넣고, ini_set()이나 .user.ini에 넣지 않습니다. 바꾼 뒤에는 모든 PHP 작업자를 다시 시작합니다.
; php.ini (FPM 풀 설정이라면 php_admin_value[...] 형태로)
session.upload_progress.enabled = 0 ; 세션 업로드 진행률을 끈다 (CVE-2026-109619)
fatal_error_backtraces = 0 ; PHP 8.5 만 해당 (CVE-2026-109619)
post_max_size = 8M ; 파일 업로드가 없다면 8M, 있다면 32M
upload_max_filesize = 2M ; 항상 post_max_size 보다 작게
- 세션 업로드 진행률 기능을 쓰지 않는다면, PHP 팀은 패치한 뒤에도 꺼 두기를 권합니다.
- CVE-2026-109618에는 메모리 한도를 없애는
memory_limit = -1도 권고되지만, 요청 하나가 서버 메모리를 다 써 버릴 수 있다는 부작용이 큽니다. 쓴다면 컨테이너나 cgroup 메모리 제한을 함께 걸고, ulimit(RLIMIT_AS·RLIMIT_DATA)로 대신하지는 마세요. 오히려 취약한 경로를 부릅니다. - 웹 서버나 프록시에서 요청 크기와 업로드 형식(multipart) 요청을 제한하고, 파일 업로드를 쓰지 않는 사이트라면
multipart/form-data요청 자체를 막습니다. - 오류 메시지를 숨기는 것(
display_errors = Off,error_reporting,@)은 효과가 없습니다. - 그래도 패치가 늦어진다면, 관리자 페이지처럼 외부에 열 필요가 없는 곳은 IP 제한으로 닫고, 가능하면 서버를 인터넷에서 떼어 둡니다.
3. 세션(SESSION)과 쿠키(COOKIE), 무엇이 다른가
이번 취약점의 확인된 경로 중 하나가 세션 기능(세션 업로드 진행률)이었습니다. 이 기회에 웹에서 로그인 상태를 기억하는 두 가지 방법, 세션과 쿠키의 차이를 정리해 보겠습니다.
웹(HTTP)은 기본적으로 기억력이 없습니다. 방금 로그인한 사람이 다음 페이지를 열어도 서버는 그 사람이 누군지 모릅니다. 그래서 "이 사람은 누구다"라는 정보를 어딘가에 두고 요청마다 확인하는데, 그 정보를 브라우저에 두면 쿠키 방식, 서버에 두면 세션 방식입니다.
- 쿠키 — 정보를 브라우저에 저장하고, 브라우저가 요청할 때마다 함께 보냅니다. 놀이공원 자유이용권처럼 손님이 직접 들고 다니고, 직원은 진짜인지 도장만 확인합니다.
- 세션 — 정보는 서버에 저장하고, 브라우저에는 번호표(세션 ID, PHP에서는
PHPSESSID쿠키)만 줍니다. 물품 보관소처럼 짐은 보관소에 있고 손님은 번호표만 가지고 다닙니다.
세션도 번호표는 쿠키로 주고받으니, 둘의 차이는 결국 정보를 어디에 두느냐입니다.
| 쿠키 방식 | 세션 방식 | |
|---|---|---|
| 정보 저장 위치 | 사용자 브라우저 | 서버(파일, Redis, DB 등) |
| 서버 부하 | 저장 없음, 서명 확인 계산만 | 요청마다 저장소를 읽고 쓰고 잠금 |
| 서버가 여러 대일 때 | 서명 키만 같으면 어느 서버든 확인 | 세션 저장소를 공유하거나 같은 서버로만 보내야 함 |
| 서버 재시작·배포 | 로그인 유지 | 저장 방식에 따라 로그아웃될 수 있음 |
| 용량 | 쿠키 하나당 약 4KB | 서버 자원이 허락하는 만큼 |
| 보안 | 사용자가 내용을 보고 고칠 수 있어 서명·암호화가 필수 | 내용은 서버에 있지만, 번호표를 훔치면 그대로 도용됨 |
| 강제 로그아웃 | 만료 전에 즉시 무효로 만들기 어려움(보완 장치 필요) | 서버에서 지우면 즉시 |
케네스랩이 쿠키 방식을 권하는 이유
케네스랩은 되도록 서버에 상태를 두지 않는 쿠키 방식을 씁니다. 여기서 말하는 쿠키 방식은 정보를 그대로 쿠키에 넣는 것이 아니라, 서버만 아는 비밀 키로 서명한(필요하면 암호화한) 쿠키를 뜻합니다.
- 서버 부하가 적습니다. 요청마다 세션 저장소를 열고, 잠그고, 다시 쓰는 대신 서명이 맞는지 한 번 계산하면 끝납니다.
- 서버를 늘리기 쉽습니다. 서버가 두 대, 열 대가 되어도 같은 키만 있으면 어느 서버든 사용자를 알아봅니다. 세션을 공유하기 위한 Redis 같은 저장소를 따로 운영할 필요가 없습니다.
- 배포와 재시작에 강합니다. 서버를 다시 시작하거나 새로 띄워도 사용자가 로그아웃되지 않습니다. 이번처럼 긴급 패치로 PHP를 다시 시작해야 할 때도 부담이 적습니다.
- 공격면이 줄어듭니다. 세션 기능을 쓰지 않으면 세션 관련 설정을 마음 놓고 끌 수 있습니다. 다만 쿠키 방식을 쓴다고 이번 취약점이 저절로 막히지는 않습니다. 세션 업로드 진행률은 앱이 세션을 쓰지 않아도 기본으로 켜져 있으므로 설정으로 직접 꺼야 하고, 무엇보다 패치가 먼저입니다.
쿠키 방식을 쓸 때 지킬 것
- 반드시 서명합니다. 서명 없는 쿠키에
user_id=3을 넣으면 사용자가user_id=1로 고쳐 관리자 행세를 할 수 있습니다. HMAC-SHA256으로 서명하고, 비교는hash_equals()로 합니다. - 민감한 정보는 넣지 않습니다. 서명은 위조를 막을 뿐 내용을 숨기지 않습니다. 비밀번호나 개인정보는 넣지 말고, 꼭 숨겨야 하는 값이라면 암호화(
sodium)까지 합니다. - 쿠키 속성을 챙깁니다.
HttpOnly(자바스크립트가 읽지 못하게),Secure(HTTPS에서만 전송),SameSite=Lax(다른 사이트에서 보내는 요청에 실리지 않게). - 만료는 짧게, 강제 로그아웃 장치는 따로. 쿠키는 한번 주면 만료 전까지 유효합니다. 사용자별 토큰 버전을 DB에 두고, 비밀번호를 바꾸거나 강제 로그아웃할 때 버전을 올려 옛 쿠키를 무효로 만듭니다.
- 작게 유지합니다. 쿠키는 모든 요청에 실려 가고 약 4KB 제한이 있으니, 사용자 번호·토큰 버전·만료 시각 정도만 담습니다.
- 서명 키는 코드 밖에 둡니다. 저장소에 올리지 않는 비밀 설정 파일에 두고, 유출이 의심되면 즉시 바꿉니다. 키를 바꾸면 모든 쿠키가 무효가 되어 전원이 다시 로그인합니다.
// 로그인 성공 시: 사용자 번호·토큰 버전·만료 시각만 담아 서명한다
$payload = base64_encode(json_encode(['uid' => $user['id'], 'ver' => $user['token_ver'], 'exp' => time() + 3600]));
$sig = hash_hmac('sha256', $payload, AUTH_SECRET);
setcookie('auth', $payload . '.' . $sig, [
'expires' => time() + 3600,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
// 요청마다: 서명 → 만료 → 토큰 버전 순으로 확인한다
[$payload, $sig] = explode('.', $_COOKIE['auth'] ?? '', 2) + ['', ''];
$auth = null;
if (hash_equals(hash_hmac('sha256', $payload, AUTH_SECRET), $sig)) {
$data = json_decode(base64_decode($payload), true);
if (is_array($data) && $data['exp'] > time() && $data['ver'] === tokenVersionOf($data['uid'])) {
$auth = $data; // 로그인 상태
}
}
세션을 써야 한다면 고려할 점
세션이 나쁜 것은 아닙니다. 크고 자주 바뀌는 임시 데이터를 다루거나, 즉시 강제 로그아웃이 꼭 필요한 금융·관리 시스템에서는 세션이 더 맞을 수 있습니다. 세션을 쓴다면 다음을 챙기세요.
- 세션 잠금 — PHP 기본 세션(파일 저장)은 요청이 끝날 때까지 세션 파일을 잠급니다. 한 화면에서 AJAX 요청을 여러 개 동시에 보내면 서로 줄을 서서 하나씩 처리되어 화면이 느려집니다. 값을 읽기만 한다면
session_start(['read_and_close' => true])를 쓰고, 값을 다 썼으면 바로session_write_close()를 부릅니다. - 쓸데없는 세션 만들지 않기 — 모든 페이지 맨 위에서
session_start()를 부르면, 쿠키를 들고 다니지 않는 검색 로봇이 페이지를 열 때마다 새 세션 파일이 생깁니다. 로그인이 필요한 곳에서만 시작합니다. - 세션 청소 부하 — PHP는 요청 중 일정 확률로 오래된 세션 파일을 지우는 청소(GC)를 돌립니다. 세션 파일이 수십만 개 쌓이면 마침 그 요청을 보낸 사용자가 느려지므로, 확률을 0으로 두고 cron으로 따로 지우는 편이 낫습니다. Debian·Ubuntu는 기본으로 이렇게 되어 있습니다.
- 서버를 늘릴 때 — 서버가 여러 대면 세션을 Redis나 DB 같은 공유 저장소에 두거나, 로드밸런서에서 같은 사용자를 늘 같은 서버로 보내야(sticky session) 합니다. 공유 저장소는 또 하나의 운영 대상이자 장애 지점이 됩니다.
- 세션 데이터는 작게 — 큰 배열이나 객체를 넣으면 요청마다 그만큼 읽고, 쓰고, 직렬화합니다. 사용자 번호처럼 꼭 필요한 값만 넣고 나머지는 DB에서 읽습니다.
- 보안 설정 — 아래 설정을 켜고, 로그인에 성공하면
session_regenerate_id(true)로 세션 ID를 새로 발급해 로그인 전에 번호표를 심어 두는 공격(세션 고정)을 막습니다.
session.use_strict_mode = 1 ; 서버가 만들지 않은 세션 ID 는 거부
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Lax
session.upload_progress.enabled = 0 ; 쓰지 않는다면 끈다
4. 케네스랩의 서버 패치 사례
케네스랩은 자체 서비스와 고객사의 PHP 서버를 운영·관리하고 있습니다. 공개 다음 날인 10월 22일 오전까지 케네스랩 자체 서비스 서버의 패치를 모두 마쳤고, 관리 중인 고객사 J사, F사, E사 등의 서버도 모두 패치를 완료했습니다. 진행한 순서는 다음과 같습니다.
빠짐없는 서버 목록부터
가장 먼저 한 일은 "PHP가 어디서 돌고 있는가"를 확인하는 것이었습니다. 기억에 의존하지 않고 클라우드 계정의 모든 리전에서 서버 목록을 조회해, PHP가 도는 서버를 추렸습니다. 운영 서버와 개발 서버가 대상이었고, 정적 사이트만 서비스하는 서버는 PHP가 없어 제외했습니다. 긴급 패치에서 가장 위험한 것은 잊고 있던 서버 한 대입니다.
스크립트 하나로 같은 절차를
서버마다 손으로 명령을 치다 보면 한 대쯤은 재시작을 빠뜨리기 쉽습니다. 그래서 서버 한 대에서 할 일을 스크립트 하나로 만들고, 모든 서버에 같은 스크립트를 돌렸습니다.
- PHP 버전을 확인하고, 고친 버전이 아니면 패키지를 올립니다.
- 고친 버전을 받지 못하면 임시 완화 설정(세션 업로드 진행률 끄기 등)을 넣고 '취약'으로 남깁니다.
- php-fpm 등 PHP를 싣는 서비스를 다시 시작하고, 옛 코드로 도는 프로세스가 남지 않았는지 확인합니다.
- 내용이 새어 나가기 시작한 날부터의 흔적(비정상 종료, 메모리 초과, 웹 서버 계정이 만든 PHP 파일)을 점검합니다.
- 서버별 결과(안전, 취약, 재시작 필요)를 한 줄씩 요약하고, 각 사이트의 응답을 확인합니다.
같은 스크립트에 '점검만' 모드를 두어, 아무것도 바꾸지 않고 상태만 다시 확인할 수 있게 했습니다. 다음 보안 공지가 나오면 고친 버전 표만 바꿔 다시 쓸 수 있습니다.
함정 — "업데이트할 것이 없습니다"
서버들은 Amazon Linux 2023 위에서 nginx와 PHP-FPM으로 돌고 있습니다. Amazon Linux 2023은 저장소 버전을 특정 시점으로 고정해 두기 때문에, 기본 업데이트 명령으로는 그 뒤에 나온 수정 패키지가 보이지 않았습니다. --releasever=latest를 붙이자 수정 패키지(ALAS2023-2026-3304, PHP 8.5.12)가 나타났습니다. "업데이트할 것이 없다"는 메시지를 "이미 안전하다"로 읽었다면 취약한 채로 남았을 것입니다.
결과
| 대상 | 결과 |
|---|---|
| 케네스랩 자체 서비스 (운영·개발 서버) | PHP 8.5 → 8.5.12, 재시작과 서비스 응답 확인 완료, 공격 흔적 없음 |
| 고객사 J사 | 패치 완료 |
| 고객사 F사 | 패치 완료 |
| 고객사 E사 | 패치 완료 |
오늘 '점검만' 모드로 자체 서버를 모두 다시 확인한 결과, 전부 8.5.12로 돌고 있었고 옛 코드로 도는 프로세스도, 비정상 종료나 메모리 초과 기록도, 웹 서버 계정이 새로 만든 PHP 파일도 없었습니다.
이번 일로 다시 확인한 것
- 서버 목록은 평소에 최신으로. 패치 속도는 "어디에 PHP가 있는지" 바로 답할 수 있느냐에서 갈립니다.
- 패치는 업데이트 + 재시작 + 확인입니다. 셋 중 하나라도 빠지면 끝난 것이 아닙니다.
- "업데이트 없음"은 "안전"이 아닙니다. 실제로 도는 버전과 CVE 기록으로 확인합니다.
- 개발 서버도 인터넷에 그대로 열지 않습니다. 케네스랩 개발 서버는 평소에도 인증을 통과해야만 PHP에 닿도록 해 두어, 이번 같은 '로그인 없는' 공격에 노출될 여지가 작았습니다.
맺으며
PHP2Shell은 코드를 아무리 잘 짜도 그 아래의 엔진이 뚫리면 소용없다는 것을 보여 준 사건입니다. 동시에, 서버 목록과 패치 절차가 준비된 곳에서는 하루 안에 끝낼 수 있는 일이기도 했습니다. 아직 패치하지 않았다면 오늘 버전을 확인하고, 올리고, 다시 시작하세요. PHP 서버 점검이나 패치에 도움이 필요하다면 케네스랩에 문의해 주세요.
참고 자료
- PHP 보안 권고 GHSA-w8hw-w9fx-j79w — RCE via OOM corruption (CVE-2026-109618)
- PHP 보안 권고 GHSA-6wmf-4rv5-p3rp — RCE via stale global state (CVE-2026-109619)
- PHP 8 ChangeLog
- CERT-EU Security Advisory 2026-018 — Critical Vulnerabilities in PHP
- Qualys — PHP2Shell: PHP Patches Two Critical RCE Vulnerabilities
- watchTowr — PHP Remote Code Execution (PHP2Shell)