PHP2shell 비상에 내 PHP 서버부터 점검한다

오늘(10월 22일) 아침 알림창이 PHP 이야기로 꽉 찼다. 과기정통부와 KISA가 PHP 보안 업데이트를 권고했고, 민간 분야 사이버위기 경보도 '관심'에서 '주의'로 한 단계 올렸다. 이해민 대통령비서실 AI미래기획 수석은 페이스북에 "이 상황을 매우 심각하게 보고 있다"고 썼다. 네덜란드와 핀란드에서는 일부 정부기관이 예방 차원에서 웹사이트를 아예 내렸다.
대통령실까지 PHP를 걱정하는 날이 올 줄은 몰랐다. 남 일도 아니다. 내 서버에서도 PHP가 돈다. 그래서 소식을 정리하는 김에 내가 직접 점검할 순서까지 같이 적어 둔다.
무엇이 터졌나
이번에 문제가 된 취약점은 2건이고, 둘을 묶어 'PHP2shell'이라고 부른다.
| CVE | 유형 | 한 줄 설명 |
|---|---|---|
| CVE-2026-109618 | 부적절한 예외 처리 | 메모리가 바닥나는 순간을 PHP가 제대로 처리하지 못한다 |
| CVE-2026-109619 | 해제 후 사용(UAF) | 이미 풀어 준 메모리를 다음 요청에서 다시 참조한다 |
제일 섬뜩한 대목은 이거다. 로그인이나 인증 없이 외부에서 코드를 실행해 서버를 장악할 수 있다. 위험도는 CVSS v4.0 기준 9.2점(Critical)이다. 미리 돌던 CVE 정보에는 10.0점이 붙어 있었는데, PHP 재단은 그 점수가 틀렸다고 밝혔다. 10점이 아니라 9.2점이라고 안심할 사람은 없을 테지만.
더 곤란한 건 구멍이 내 코드에 있지 않다는 점이다. 문제는 PHP 엔진 자체에 있다. Datadog이 재현해 보니 악성 요청은 애플리케이션 코드가 돌기도 전에 처리됐다. 앱에 넣어 둔 인증이나 입력 필터가 일을 시작하기도 전에 상황이 끝난다는 얘기다. 코드를 아무리 꼼꼼히 짰어도 PHP 버전이 낡았으면 똑같이 당한다.
대상은 웹 요청을 처리하는 모든 PHP 설치다. PHP-FPM, Apache mod_php, php-cgi, FrankenPHP, LiteSpeed가 전부 들어가고, PHP 쪽은 그중 mod_php의 위험을 가장 크게 봤다(Tenable 정리). 연구팀 Calif는 WordPress, Drupal, Joomla, Magento 일부 버전에서 인증 없이 원격 코드가 실행되는 것까지 확인했다.
시간표: 패치보다 공격이 먼저 왔다
- 10/17: Amazon 위협 인텔리전스가 미끼 서버망(MadPot)에서 첫 공격 시도를 관측했다. 공개 4일 전이다.
- 10/20: CVE 번호가 Hacker News와 SNS로 새어 나갔다. PHP 재단은 10/22로 잡았던 배포를 하루 앞당겼다.
- 10/21: 패치와 권고문이 나왔다(예고된 시각은 14:00 UTC).
- 10/22: 국내 사이버위기 경보가 '주의'로 올라갔다.
Amazon은 지금 "광범위하게 악용 중"이라며 로그를 최소 10/17부터 뒤져 보라고 권했다. Cloudflare CTO도 초기 공격이 나타나고 있다고 했다. PHP 재단의 말은 조금 다르다. 공개 시점에 작동하는 익스플로잇은 확인하지 못했다고 했다. 티오리에 따르면 공식 권고 전에 개념증명(PoC) 시연 영상과 패치된 바이너리가 유출됐고, 이를 바탕으로 한 PoC가 빠르게 퍼지는 중이다. Tenable은 출처가 불분명한 공격 코드에 악성코드가 섞여 있을 수 있다고 경고했다. 궁금해서 아무 PoC나 받아 돌렸다가는 내 노트북이 먼저 털릴 수도 있다.
원리를 한 그림으로
Datadog의 분석 가운데 109618 쪽을 단순하게 그리면 이렇다.
POST 폼 도착
│
▼
$_POST 해시 테이블에 넣기
├─ ① 항목 수를 먼저 +1 ← 이미 "있다"고 적어 둠
└─ ② 키 메모리 할당 시도
│ memory_limit 초과로 실패
▼
'빈 칸' 하나가 남음
│
▼
요청 정리 단계에서 빈 칸을 진짜 값으로 읽음
│
▼
공격자가 심어 둔 바이트 → 셸 명령 실행
창고로 치면 장부에 "물건 하나 들어옴"부터 적어 놓고, 선반에 올리다 물건을 떨어뜨린 셈이다. 나중에 정리하는 사람은 장부만 믿고 빈 선반에 손을 넣는다. 그 빈자리에 공격자가 미리 뭔가를 놓아 뒀다는 게 문제다. 109619는 한 워커가 이전 요청에서 쓰던 낡은 포인터를 다음 요청에서 다시 집어 드는 문제다. 기본으로 켜져 있는 세션 업로드 진행률 기능이 그 통로 가운데 하나가 된다.
내 서버 점검하기
고친 버전은 아래와 같다. KISA는 공식 보안 지원이 끝난 8.1 이하를 지원 중인 버전으로 올리라고 권고한다. 7.48.1에는 정식 릴리스 없이 최소한의 소스 백포트만 공개됐고, 5.47.3은 고침 자체가 없다.
| 버전 | 고친 버전 |
|---|---|
| 8.2 | 8.2.35 |
| 8.3 | 8.3.36 |
| 8.4 | 8.4.27 |
| 8.5 | 8.5.12 |
1단계: CLI만 믿지 않기. php -v가 보여 주는 버전과 실제로 웹 요청을 처리하는 런타임의 버전이 다른 경우가 있다. 터미널에서 최신 버전을 보고 안심했는데 FPM은 작년 버전이더라, 하는 일이 개발자에게는 낯설지 않다. 그러니 셋 다 확인한다.
php -v # CLI
php-fpm -v # FPM (배포판에 따라 php-fpm8.3 같은 이름)
웹 런타임 버전은 웹 루트에 임시 파일을 하나 올려서 본다. 확인이 끝나면 바로 지운다. 점검용 파일을 깜빡하고 남겨 두면 그게 또 하나의 구멍이다.
<?php echo PHP_VERSION, ' / ', PHP_SAPI;
2단계: 버전 비교. 아래는 고친 버전 표와 비교하는 셸 스크립트 예시다. 직접 돌려 보지는 않았다.
#!/usr/bin/env bash
# 예시: 사용법 ./check.sh 8.3.30 (인자 없으면 CLI 버전)
ver="${1:-$(php -r 'echo PHP_VERSION;')}"
minor="${ver%.*}"
case "$minor" in
8.2) fixed=8.2.35 ;;
8.3) fixed=8.3.36 ;;
8.4) fixed=8.4.27 ;;
8.5) fixed=8.5.12 ;;
*) echo "$ver: 공식 지원 밖, 지원 버전으로 올려야 함"; exit 1 ;;
esac
if [ "$(printf '%s\n' "$fixed" "$ver" | sort -V | head -n1)" = "$fixed" ]; then
echo "$ver: OK ($fixed 이상)"
else
echo "$ver: 취약 가능, $fixed 이상으로 올릴 것"
fi
다만 Datadog에 따르면 배포판 패키지는 버전 숫자를 그대로 두고 고침만 넣기도 한다. '취약 가능'이 나오면 쓰는 배포판의 보안 권고문도 같이 확인한다.
3단계: 컨테이너는 다시 빌드. 베이스 이미지 태그만 바꿔 놓고 손을 털면 안 된다. 이미지를 새로 빌드해서 다시 배포해야 실제 서버에 반영된다.
4단계: 당장 못 올린다면 임시 완화. PHP 권고에 나온 설정이다. 이건 고침이 아니고 노출을 줄일 뿐이다. 완전한 해결은 패치뿐이다.
; CVE-2026-109619 완화
session.upload_progress.enabled = 0
; PHP 8.5라면 추가
fatal_error_backtraces = 0
109618 쪽은 memory_limit=-1을 관리자 수준에서 강제하라고 권고한다. 메모리 제한을 아예 푸는 셈이라, 권고도 앞단의 요청 크기 제한과 컨테이너 메모리 제한을 함께 걸라고 한다. 서버 자원에 얼마나 부담이 가는지 따져 봐야 한다. 설정을 바꿨으면 모든 워커를 재시작해야 적용된다. 티오리도 임시 조치에는 세션 기능 오류 같은 부작용이 따르고 완전한 방어가 못 된다고 했다.
개발자가 아니라면 서버를 직접 만질 일이 거의 없다. 워드프레스나 호스팅 서비스로 사이트를 운영한다면 호스팅사 공지부터 확인하자. 스틸리언이 공개한 무료 점검 페이지(stealien.com/php2shell)에 URL을 넣어 보는 것도 방법이다.
국내외 대응
- 스틸리언: URL만 넣으면 취약 여부를 알려 주는 무료 점검 서비스를 열었다.
- 에버스핀: Eversafe for Web SaaS 고객에게 서버 앞단에서 공격을 막는 차단 규칙을 공식 패치 전에 적용했다. 다만 취약점 자체를 없애려면 공식 패치를 함께 적용해야 한다고 했다.
- 티오리: 직접 재현하고 검증한 뒤 대응 가이드, 임시 완화책, 점검 스크립트를 진트(Xint) 기술 블로그에 공개했다.
- Cloudflare: 알려진 공격 경로용 WAF 규칙을 무료 요금제까지 모든 고객에게 적용했다. 스스로 "임시방편"이라고 불렀다.
숫자는 출처마다 다르다
피해 규모 추정은 출처마다 차이가 크다. 팔로알토는 외부에 노출된 PHP 인스턴스를 약 2200만 개로 봤다. Silas Cutler는 취약할 수 있는 호스트를 130만 개 이상으로, Shodan 검색(Henk Poley)은 PHP를 돌리는 노출 호스트를 190만 개 이상으로 셌다. 세는 기준이 저마다 달라서 어느 하나를 정답으로 꼽기 어렵다. 점유율도 ZDNet이 인용한 외신은 웹사이트의 약 75%로, W3Techs(Tenable 인용)는 서버 언어가 알려진 사이트의 69.3%로 잡는다. 숫자를 어떻게 세든 "웹의 큰 부분"이라는 데까지는 모두 동의한다.
내 생각: AI가 찾은 구멍
PHP 재단은 Calif, OpenAI, Wiz 등에 공을 돌렸고, Calif와 Wiz는 연구에 AI 모델을 썼다고 밝혔다. Calif의 분석으로는 영향을 받는 코드 경로가 PHP 5.4.0까지 거슬러 올라간다. 10년 넘게 수많은 사람이 들여다본 엔진 코드에서, AI의 도움을 받아 이런 구멍이 나왔다. 나는 이 대목에서 한참 멈춰 있었다.
개발자로서 챙길 교훈은 두 가지다. 첫째, "오래된 코드라 검증이 끝났다"는 말은 이제 믿을 근거가 못 된다. 둘째, 패치보다 공격이 먼저 오는 일이 실제로 벌어졌다. 이번에는 공개 4일 전에 첫 공격 시도가 관측됐다. 버전 확인, 이미지 재빌드, 워커 재시작을 몇 시간 안에 끝내는 배포 체계가 있으면 그게 곧 보안이 된다. 나도 오늘 저녁에는 퇴근하자마자 FPM 버전부터 볼 생각이다.
정리
- PHP2shell은 인증 없이 서버를 장악할 수 있는 PHP 엔진 결함 2건이고, CVSS 9.2다.
- 8.2.35, 8.3.36, 8.4.27, 8.5.12 이상으로 올린다. 8.1 이하는 지원 버전으로 옮긴다.
- CLI 버전이 아니라 웹 런타임 버전을 확인하고, 컨테이너는 다시 빌드한다.
- 임시 설정은 시간을 버는 수단일 뿐이다. 로그는 10/17부터 살핀다.