2026년 10월 21일, PHP에서 심각한 보안 문제를 해결한 업데이트가 공개됐다.
특정 조건에서 공격자가 웹사이트에 로그인하지 않고도 서버에서 코드를 실행할 수 있는 PHP 취약점이었다.
PHP2Shell이라는 이름으로 알려진 문제다.
이번에 공개된 두 취약점은 모두 CVSS 4.0 기준 9.2점(Critical) 으로 평가됐다.
CVSS는 보안 취약점의 심각도를 0점부터 10점까지 나타내는 평가 체계다. 9.2점은 가장 높은 심각도 등급인 Critical에 해당한다.
웹사이트에 로그인하지 않은 공격자가 서버에서 코드를 실행할 수 있다니, 어떤 과정에서 문제가 생긴 걸까?
내용을 살펴보니 웹사이트 개발자가 작성한 코드보다 더 아래에 있는, PHP 자체의 요청 처리 과정에 문제가 있었다.
1. 웹사이트 코드가 안전해도 영향을 받을 수 있었다
브라우저가 JavaScript 코드를 실행하듯, 서버에서는 PHP 런타임이 PHP 코드를 실행한다.
프론트엔드 개발자에게 익숙한 환경으로 비교하면, Next.js 애플리케이션과 이를 실행하는 Node.js 런타임의 관계를 생각해 볼 수 있다.
사용자가 PHP로 만든 웹사이트에 접속하면 서버는 요청을 받아 필요한 작업을 수행하고 응답을 돌려준다.
그 과정에서 PHP는 입력 데이터를 읽고, 작업에 필요한 메모리를 준비하고, 요청이 끝나면 사용한 데이터를 정리한다.
PHP2Shell은 이 내부 처리 과정에서 발생한 문제였다.
따라서 웹사이트의 로그인 기능이나 입력 검사를 잘 만들었더라도, 그 아래에서 실행되는 PHP가 취약하다면 영향을 받을 수 있었다.
연구진은 WordPress, Drupal, Joomla 환경에서 인증 없는 원격 코드 실행을 시연했다.
이 프로그램들만의 버그를 이용한 것은 아니었다. 취약점은 PHP 엔진 자체에 있었고, 이 프로그램들은 실제 영향을 보여주기 위한 사례였다.
물론 PHP를 사용하는 모든 웹사이트에서 똑같은 방식으로 공격이 가능하다는 뜻은 아니다.
실제 공격 가능성과 조건은 PHP 실행 방식, 서버 설정, 애플리케이션의 동작에 따라 달라진다.
그렇다면 PHP 내부에서는 무엇이 잘못됐을까?
2. 문제는 요청이 실패하거나 끝난 뒤에 있었다
이해하기 쉽게 식당의 주문 처리 과정을 생각해 보자.
직원이 손님의 주문을 받아 주문표를 작성한다.
주방은 주문표를 보고 음식을 준비하고, 손님에게 음식을 전달하면 해당 주문을 마무리한다.
그런데 음식을 준비하던 중 재료가 떨어졌다고 가정하자.
이때는 조리를 멈추는 것뿐 아니라 주문을 취소하거나 변경하고, 주방과 계산대의 기록도 맞춰야 한다.
그렇지 않으면 누군가는 이미 취소된 주문을 계속 준비하거나, 다음 손님의 주문을 처리하면서 이전 기록을 사용할 수 있다.
작업을 정상적으로 끝내는 방법뿐 아니라, 중간에 실패했을 때 안전하게 정리하는 방법도 필요한 것이다.
PHP도 요청을 처리하다가 오류를 만나면 작업을 중단하고 내부 데이터를 정리해야 한다.
또한 하나의 요청이 끝났다면 그 요청에서 사용한 메모리와 내부 상태도 적절하게 정리되어야 한다.
PHP2Shell로 묶인 두 취약점은 이 과정에서 서로 다른 문제가 발생한 사례다.
첫 번째: 준비가 끝나지 않은 데이터를 사용한 문제
첫 번째는 CVE-2026-109618이다.
식당에서 주문표를 아직 다 작성하지 않았는데, 다른 직원이 그 주문표를 처리할 수 있는 상태가 됐다고 생각해 보자.
그사이 재료 부족으로 주문 처리가 중단됐다.
그런데 마무리를 맡은 직원은 주문표가 완성됐다고 생각하고 적혀 있는 내용을 그대로 처리한다.
작성하지 않은 칸까지 정상적인 정보라고 믿으면 잘못된 작업이 이어질 수 있다.
PHP에서도 비슷한 일이 발생했다.
PHP는 요청을 처리하는 중 메모리 확보에 실패하면 현재 작업을 중단하는 경로가 있다.
문제는 일부 내부 데이터가 완전히 준비되기도 전에 작업이 중단될 수 있었다는 점이다.
이후 요청을 정리하는 코드가 그 불완전한 데이터를 정상적인 데이터처럼 사용하면서 메모리 손상으로 이어질 수 있었다.
여기서 한 가지 알아둘 점이 있다.
메모리 부족(OOM, Out of Memory)이 반드시 서버 전체의 메모리가 모두 소진됐다는 의미는 아니다.
PHP에는 요청이 사용할 수 있는 메모리의 한도를 설정하는 memory_limit이라는 옵션이 있다.
실제 서버에 사용 가능한 메모리가 남아 있더라도, 요청이 이 한도에 도달하면 메모리 부족 오류가 발생할 수 있다.
이번 취약점은 이러한 오류 처리 과정에서도 발생할 수 있었다.
두 번째: 이전 요청의 정보를 다음 요청에서 사용한 문제
두 번째는 CVE-2026-109619다.
이번에는 손님 A의 주문을 끝내고 주문표를 치웠다고 가정하자.
그런데 직원의 메모에는 여전히 "처리할 주문표가 저기에 있다"는 정보가 남아 있다.
다음 손님의 주문을 처리하던 직원이 그 메모를 보고 이미 치워진 주문표를 다시 찾으려 한다면 문제가 생긴다.
실제 PHP에서도 비슷한 메모리 관리 문제가 있었다.
실행 환경에 따라 같은 PHP 작업 프로세스(Worker)가 여러 요청을 차례로 처리할 수 있다.
예를 들어 하나의 Worker가 A 사용자의 요청을 처리한 다음, B 사용자의 요청을 처리할 수 있다는 뜻이다.
요청 하나가 끝났다고 해서 해당 Worker 자체가 반드시 종료되는 것은 아니다.
이때 이전 요청에서 사용한 메모리는 해제됐는데, 그 메모리를 가리키는 정보가 PHP 내부에 남을 수 있었다.
같은 Worker가 다음 요청을 처리하면서 해당 정보가 정상적으로 초기화되기 전에 사용하면, 이미 해제된 메모리에 접근하게 된다.
이것이 Use-After-Free(UAF), 해제 후 사용 문제다.
1편에서 살펴봤던 바로 그 개념이다.
이번에는 특히 하나의 요청에서 남은 내부 상태가 다음 요청의 동작에 영향을 줄 수 있었다는 점이 중요하다.
이 문제는 메모리 부족과 같은 치명적인 오류뿐 아니라, 일부 종료 처리 과정에서 발생하는 경고 등으로도 관련 상태가 잘못 남을 수 있었다.
두 취약점의 차이
| 취약점 | 잘못된 상태 |
|---|---|
| CVE-2026-109618 | 준비가 끝나지 않은 내부 데이터를 정상 데이터처럼 사용 |
| CVE-2026-109619 | 이전 요청의 해제된 메모리를 가리키는 정보가 남아 다시 사용 |
두 취약점 모두 PHP 내부의 메모리와 상태를 안전하게 관리하지 못했다는 공통점이 있다.
하지만 실제 문제가 발생하는 시점과 공격 조건에는 차이가 있었다.
3. 그런데 이런 오류가 어떻게 서버 코드 실행으로 이어질까?
메모리가 부족해서 요청 하나가 실패하는 상황만 생각하면, 보안 취약점이라는 말이 조금 멀게 느껴질 수 있다.
하지만 프로그램은 메모리에 저장된 데이터를 읽고, 그 값을 바탕으로 다음에 무엇을 할지 결정한다.
그 데이터나 데이터를 가리키는 정보가 잘못되면, 예상하지 않은 메모리를 읽거나 쓰는 동작으로 이어질 수 있다.
공격자는 자신이 보낸 요청이 이런 잘못된 상태를 만들도록 시도할 수 있다.
여기에 메모리의 배치와 재사용 상황까지 이용하는 추가 공격 과정을 연결하면, 정보 노출이나 코드 실행으로 발전할 수 있다.
메모리 부족 자체가 곧바로 공격자의 코드 실행을 뜻하는 것은 아니다. 오류 처리의 결함을 실제 공격으로 연결하는 과정이 필요하다.
PHP2Shell 연구진은 이 과정을 구성해 원격 코드 실행과 PHP 프로세스 메모리의 정보 노출을 시연했다.
이를 원격 코드 실행(Remote Code Execution, RCE) 이라고 한다.
쉽게 말해 외부에서 접근한 공격자가 서버에서 자신이 의도한 코드를 실행할 수 있다는 뜻이다.
서버 프로세스가 가진 권한에 따라 민감한 데이터를 읽거나, 파일을 변경하거나, 추가적인 공격에 이용할 수 있다.
로그인 기능이 있는데 어떻게 인증 없이 공격할 수 있을까?
이번 취약점에서 특히 눈여겨볼 부분이 있었다.
PHP는 Apache 웹 서버와 함께 사용하는 경우가 많다.
그중에는 PHP를 Apache의 모듈로 실행하는 mod_php 방식이 있다.
PHP 공식 발표에 따르면 이 환경에서는 일부 취약한 처리 과정이 웹사이트의 애플리케이션 코드가 실행되기도 전에 발생할 수 있었다.
쉽게 말하면 다음과 같은 순서다.
일반적으로 예상하는 요청 흐름
- 사용자가 서버에 요청을 보낸다.
- 애플리케이션에서 로그인 상태나 입력값을 검사한다.
- 필요한 작업을 수행한다.
- 응답을 반환한다.
이번 취약점이 악용될 수 있었던 흐름
- 공격자가 특정 조건을 만족하는 요청을 보낸다.
- PHP가 애플리케이션 실행을 준비하며 요청 데이터를 처리한다.
- 이 과정에서 PHP 내부의 메모리 처리 취약점이 발생한다.
- 애플리케이션의 로그인 검사에 도달하기 전 취약점이 악용될 수 있다.
연구진은 Apache mod_php 환경에서 기본 설정을 이용한 인증 없는 원격 코드 실행을 실제로 시연했다.
PHP-FPM 환경에서도 애플리케이션의 특정 실행 경로를 통한 원격 코드 실행이 확인됐다.
다만 실행 방식별로 문제가 발생하는 조건은 서로 달랐다.
따라서 웹사이트 코드에서 로그인 검사와 입력 검증을 안전하게 구현했더라도, 그 아래에서 실행되는 PHP 런타임의 보안 문제까지 막을 수 있는 것은 아니었다.
이 부분을 통해 애플리케이션의 보안과 런타임의 보안을 함께 살펴봐야 하는 이유를 이해할 수 있었다.
4. 그런데 이렇게 오래된 문제였다고?
취약점의 동작을 살펴보다가 또 다른 사실이 눈에 들어왔다.
Wiz는 공개한 취약점 중 적어도 하나의 영향 범위가 PHP 5.4까지 거슬러 올라간다고 설명했다.
PHP 5.4는 2012년에 출시된 버전이다.
수정 직전의 PHP 8.x 계열도 영향을 받았다.
여기서 이런 의문이 들었다.
"그렇게 오래전 버전부터 있던 문제인데, 왜 이제야 고쳐진 걸까?"
오랫동안 사용된 소프트웨어라면 그만큼 많은 사람이 코드를 살펴봤을 것 같다.
그동안 수많은 버전이 출시되고 기능도 개선됐을 텐데, 이런 문제는 왜 계속 남아 있었을까?
먼저 오래된 코드가 최신 버전에도 남을 수 있다는 점부터 생각해 볼 필요가 있었다.
앞에서 이야기한 식당이 매년 새로운 메뉴를 추가한다고 가정해 보자.
새 메뉴를 팔기 시작해도 주문을 받고, 주방에 전달하고, 취소된 주문을 정리하는 방식까지 매번 새로 만드는 것은 아니다.
오래 사용하던 업무 방식이 그대로 이어질 수 있다.
소프트웨어도 비슷하다.
새 버전에는 새로운 기능과 수정 사항이 들어가지만, 기존 코드도 많이 재사용한다.
프론트엔드에서 사용하는 라이브러리나 프레임워크 역시 버전을 올릴 때마다 모든 내부 코드를 다시 작성하지는 않는다.
따라서 최신 버전 안에 오래전에 작성한 코드와 그 코드의 문제가 함께 남아 있을 수 있다.
그렇다면 이번 문제는 아무도 모르고 있었던 걸까?
사실 2024년에 이미 이상 징후를 발견했다
PHP 재단의 설명에 따르면, 2024년 외부 보안 감사에서 보고된 다른 문제를 수정하던 과정에서 메모리 부족 처리와 관련된 이상 징후가 발견됐다.
이것이 나중에 CVE-2026-109618로 분류된 문제의 근본 원인이었다.
당시에도 공격 가능성이 논의됐지만 구체적인 공격 경로는 확인되지 않았다.
이후에도 수정 방법을 검토했지만 해결이 지연됐다.
그러다가 2026년 8월 11일, 보안업체 Calif의 연구원이 같은 근본 원인으로 원격 코드 실행이 가능하다고 보고했다.
PHP 재단은 이때부터 해당 문제의 해결을 보안 업무의 최우선 과제로 삼았다고 설명한다.
이후 Wiz와 OpenAI 등에서도 관련 취약점과 추가적인 공격 경로가 보고됐다.
주요 흐름을 정리하면 다음과 같다.
| 시점 | 주요 내용 |
|---|---|
| 2024년 | 외부 보안 감사 후속 작업에서 메모리 부족 처리 문제의 징후 발견 |
| 2026년 8월 11일 | Calif가 같은 근본 원인을 이용한 원격 코드 실행 가능성을 보고 |
| 2026년 8월 24일 | Wiz가 관련 취약점을 PHP 보안팀에 보고 |
| 2026년 10월 21일 | 두 주요 취약점의 수정 버전 공개 |
여기서 주의할 점은 2024년에 PHP2Shell의 모든 공격 경로를 파악한 것은 아니라는 사실이다.
당시에는 메모리 부족 처리 문제의 징후를 발견했고, 잠재적인 위험을 논의한 상태였다.
이후 실제 공격으로 이어지는 구체적인 방법이 확인되면서 문제의 심각성이 더욱 분명해진 것이다.
즉, 오래된 코드에 문제가 숨어 있었던 시간과, 관련 징후를 알고도 수정이 지연된 시간이 함께 있었다.
그렇다면 또 하나의 의문이 생긴다.
"문제가 있다는 사실을 알고 있었다면, 왜 더 일찍 수정하지 못했을까?"
5. 알고도 바로 고치기 어려웠던 이유
식당의 주문 처리 방식에 문제가 있어 주문서 양식을 바꾸기로 했다고 생각해 보자.
계산대만 새 양식을 사용하고 주방과 배달 담당자는 예전 양식을 계속 사용한다면, 주문이 제대로 전달되지 않을 수 있다.
여러 사람이 함께 사용하는 방식을 바꾸려면 서로 맞춰야 하는 부분도 많아진다.
PHP의 메모리 관리와 오류 처리도 많은 내부 코드와 외부 확장 기능이 함께 사용하는 부분이었다.
PHP 재단은 검토했던 수정 방법마다 다음과 같은 문제가 있었다고 설명했다.
- 기존 코드와의 하위 호환성이 깨질 수 있었다.
- 새로운 서비스 거부(DoS) 문제를 만들 수 있었다.
- 많은 외부 확장 기능을 함께 수정해야 할 수 있었다.
여기서 서비스 거부란 서버나 프로그램이 정상적인 요청을 처리하지 못하도록 만드는 문제를 의미한다.
즉, 하나의 보안 취약점을 해결하려다가 새로운 장애나 호환성 문제를 만들 위험이 있었던 것이다.
그렇다면 기술적으로 어려웠기 때문에 패치가 늦어진 것일까?
PHP 재단은 그 이유만 있었던 것은 아니라고 설명했다.
PHP 엔진 내부 구조를 깊이 이해하는 개발자가 많지 않았고, 이들 역시 여러 업무를 함께 맡고 있었다.
또한 PHP 보안팀은 구성원들의 합의를 통해 수정 방향을 결정하는 방식으로 운영되고 있었다.
각 수정 방법에 서로 다른 위험이 있다 보니 어떤 방법을 선택할지 결정하기 어려웠다.
그 결과 수정 작업이 중단됐다가 다시 시작되는 과정이 반복됐다.
PHP 재단은 이를 의사결정 방식과 자원 부족에서 비롯된 구조적 실패라고 인정했다.
이 부분을 읽고 나니 취약점을 발견하는 일과 안전한 수정안을 배포하는 일이 서로 다른 과제라는 점이 보였다.
문제를 알아내는 사람뿐 아니라, 수정의 영향을 판단하고 어려운 선택을 결정할 사람과 시간도 필요했다.
취약점의 발견만큼이나 발견된 문제를 끝까지 해결할 수 있는 구조가 중요했던 것이다.
6. 지금은 어떻게 대응해야 할까?
2026년 10월 21일, PHP 프로젝트는 두 취약점에 대한 수정 버전을 공개했다.
최초 수정 버전은 다음과 같다.
| PHP 계열 | 최초 수정 버전 |
|---|---|
| 8.2 | 8.2.35 |
| 8.3 | 8.3.36 |
| 8.4 | 8.4.27 |
| 8.5 | 8.5.12 |
이 표는 CVE-2026-109618, CVE-2026-109619에 대한 최초 수정 버전이다.
이후의 다른 보안 문제까지 모두 해결된 최신 버전이라는 의미는 아니다.
오래된 PHP 버전은 어떻게 될까?
여기서 한 가지 더 궁금해졌다.
"PHP 5.x처럼 오래된 버전에도 영향을 미쳤다면, 그런 서버는 어떻게 해야 할까?"
PHP 프로젝트의 공식 지원이 종료된 버전은 더 이상 정규 보안 업데이트를 제공받지 못할 수 있다.
하지만 운영체제 배포판이나 별도 유지보수 업체가 수정 사항을 이전 버전에 적용하는 경우도 있다.
이를 백포트(Backport) 라고 한다.
쉽게 말해 새로운 버전에서 수정한 내용을 이전 버전에서도 사용할 수 있도록 가져오는 작업이다.
예를 들어 PHP 자체의 버전 번호는 예전 버전으로 남아 있더라도, 배포판에서 해당 취약점의 수정 사항을 적용했을 수 있다.
이번 사건에서도 PHP 보안팀은 PHP 7.4, 8.0, 8.1을 위한 최소한의 소스 백포트를 별도로 제공했다.
다만 이것이 해당 버전의 공식 지원이 다시 시작됐거나, 모든 환경에 업데이트 패키지가 제공됐다는 뜻은 아니다.
따라서 오래된 버전 번호만 보고 무조건 취약하다고 판단하기보다는, 실제 사용하는 제품과 패키지에 보안 수정 사항이 적용됐는지 확인해야 한다.
물론 공식 지원이 종료된 버전을 계속 사용하는 것에는 별도의 보안 위험이 있으므로, 지원되는 버전으로의 전환도 고려해야 한다.
패치가 공개됐다고 내 서버까지 수정된 것은 아니다
웹사이트 프로그램을 업데이트하는 것과 PHP 자체를 업데이트하는 것도 구분해야 한다.
예를 들어 WordPress의 버전을 업데이트했다고 해서 서버에서 실행 중인 PHP까지 자동으로 변경되는 것은 아니다.
실제로 웹 요청을 처리하는 PHP 런타임에 수정 버전이나 보안 패키지가 반영되어야 한다.
또한 실행 중인 PHP 프로세스에도 업데이트가 반영되도록 재시작 등의 과정이 필요할 수 있다.
환경마다 적용 방법이 다르기 때문에 구체적인 명령어보다는 사용하는 PHP 배포판이나 호스팅 업체의 공식 보안 공지를 참고하는 것이 정확하다.
PHP 프로젝트 역시 이번 두 취약점에 대해서는 수정된 버전으로 업데이트하는 것이 완전한 해결 방법이라고 안내하고 있다.
관련 공식 문서
- PHP 공식 보안 발표 — 2026년 10월 21일
- PHP 재단 — 취약점 발견 및 수정 경위
- CVE-2026-109618 — 메모리 부족 처리 취약점
- CVE-2026-109619 — 이전 요청 상태 관리 취약점
- PHP 공식 지원 버전
마무리
처음에는 PHP2Shell이 어떤 취약점인지 궁금했다.
살펴보니 요청을 처리하다 오류가 발생했을 때 준비가 끝나지 않은 데이터를 사용하거나, 이전 요청의 메모리를 다시 사용하는 문제가 있었다.
그리고 이런 메모리 관리 문제가 단순한 프로그램 충돌을 넘어 실제 원격 코드 실행까지 이어질 수 있다는 사실도 알게 됐다.
그 과정에서 또 하나의 의문이 생겼다.
"오래된 버전부터 있던 문제라는데, 왜 이제야 고쳐진 걸까?"
자료를 살펴보니 그 안에는 여러 시간이 겹쳐 있었다.
오래된 코드가 최신 버전에도 이어진 시간, 문제의 징후를 발견한 뒤 실제 위험을 확인하기까지의 시간, 수정 방법을 결정하고 배포하기까지의 시간, 그리고 사용자가 실제로 업데이트하는 시간이다.
그중에는 기술적으로 해결하기 어려웠던 부분도 있었고, 이미 알고 있었지만 의사결정과 자원 부족으로 해결이 지연된 부분도 있었다.
이번 일을 통해 기억하게 된 점은 이것이다.
소프트웨어가 오래 사용됐다는 사실만으로 모든 문제가 검증된 것은 아니며, 새로운 버전이 나왔다는 사실만으로 오래된 내부 코드까지 모두 바뀐 것도 아니다.
평소 프론트엔드를 개발하다 보면 라이브러리나 프레임워크의 버전 업데이트를 자주 접하게 된다.
하지만 그 버전이 올라가기까지 어떤 코드가 유지됐고, 어떤 문제가 발견됐으며, 수정 과정에서 어떤 어려움이 있었는지까지 살펴보는 경우는 많지 않았다.
앞으로 비슷한 보안 뉴스를 읽으면 취약점의 이름이나 위험도뿐 아니라, 어떤 처리 과정에서 문제가 발생했고 그 수정이 실제 환경에 어떻게 반영되는지도 함께 살펴보게 될 것 같다.