보안 가이드

긴급 PHP 업데이트 후 확인할 사항: 웹 PHP와 명령줄 PHP 비교, set -e와 diff 종료 코드, 침입 재점검

긴급 PHP 패치 후 웹 PHP와 명령줄(CLI) PHP가 서로 다를 수 있습니다. 각각을 확인하는 방법, set -e와 diff 종료 코드, 패치 후 침입 점검을 정리합니다.

게시 2026-11-01 업데이트 2026-11-01 최종 확인 2026-11-01 11분 읽기
이 페이지에서

대상 독자: PHP 사이트를 운영하며 긴급 PHP 업데이트를 적용하고 있거나 방금 적용했고 빈틈이 있을지 걱정하는 분들.

2026년 10월, 두 가지 치명적인 PHP 취약점(CVE-2026-109618 및 CVE-2026-109619)에 대한 수정 버전이 공개되었습니다. 이 사이트 운영자가 관리하는 한 사이트는 같은 날 밤 업데이트되었습니다. 이 페이지는 그곳에서 일어난 일, 잘못된 것으로 드러난 두 가지 초기 가정, 그리고 이후 모든 서버에 적용한 침입 재점검을 일반적인 단계로 정리합니다. 취약점과 수정 버전에 대해서는 PHP 치명적 취약점 두 건과 긴급 수정 릴리스(2026년 10월)에서 다룹니다.

무슨 일이 있었나 (이 사이트 운영자가 관리하는 사이트에서)

이 사이트는 여러 환경에서 PHP를 사용합니다. PHP 팀이 수정 버전 공개 시점을 미리 알렸기 때문에, 운영자는 공개 전에 점검 페이지로 전환했고 수정 버전이 나올 때까지 오프라인 상태를 유지했습니다. 공개 후에는 수정 버전으로 교체하고 점검 페이지를 해제했습니다.

  1. 공지 이후

    점검 페이지로 전환하고 수정 버전을 기다림
  2. 공개 이후

    수정 버전으로 교체하고 점검 페이지를 해제함
  3. 업데이트 후 점검

    웹 쪽은 업데이트되었지만 cron이 사용하는 명령줄 PHP는 여전히 이전 버전을 가리키고 있어 다시 연결함
  4. 같은 날 밤

    점검 모드를 해제하는 임시 스크립트가 아무것도 기록하기 전에 중단됨. 원인은 set -e와 diff의 조합이었음. 피해는 없었으며 스크립트는 다시 만들어짐
  5. 이후

    모든 서버에 같은 침입 점검 목록을 적용했고, 업데이트 이전에 도착한 의심스러운 요청 하나를 파일 비교로 확인함

미국 CISA는 이후 두 CVE를 악용이 확인된 취약점 목록(KEV, Known Exploited Vulnerabilities)에 추가했습니다(CISA 공지). 이는 서둘러 패치하기로 한 결정을 뒷받침하지만, 패치는 문을 닫을 뿐입니다. 이미 누군가 침입했는지 확인하는 것은 별도의 작업입니다.

가정 1: "웹 PHP가 업데이트되었으니 끝났다"

첫 번째 가정은 사이트가 새 버전에서 동작하면 그 서버의 모든 PHP가 업데이트되었으리라는 것이었습니다. 실제로는 cron이 호출하는 명령줄 PHP가 여전히 이전 버전을 가리키고 있었습니다.

PHP는 실행 방식(SAPI, Server API)에 따라 다른 프로그램일 수 있습니다. php.net은 CLI SAPI가 다른 SAPI와 여러 면에서 다르다고 설명합니다(PHP: 명령줄에서 PHP 사용하기). 한 기기에 여러 버전이 설치되어 있으면, 웹은 새 버전으로 실행되고 명령줄은 이전 버전으로 실행될 수 있습니다.

웹 요청 → PHP-FPM / Apache 모듈

업데이트된 버전으로 실행됨 (사이트 상태 등 화면에서 확인 가능)

cron 및 셸 스크립트 → 명령줄 PHP

여전히 이전 버전을 가리키며, 웹 쪽 점검으로는 보이지 않음

같은 서버에서도 웹이 사용하는 PHP와 명령줄이 사용하는 PHP는 서로 다른 버전의 별도 프로그램일 수 있습니다. 패치 후에는 둘 다 확인하세요.

웹과 CLI를 따로 확인하기

1

명령줄 PHP 버전 확인

php -v는 이 셸이 호출하는 PHP의 버전을 보여 줍니다. php.net에 따르면 php -v는 PHP가 CGI 빌드인지 CLI 빌드인지도 알려 줍니다. which php는 실제로 호출되는 실행 파일의 위치를 보여 줍니다.

2

여러 버전이 있는 Debian 계열 시스템에서 명령이 가리키는 위치 확인

update-alternatives --display php는 php 명령이 가리키는 버전을 보여 줍니다(update-alternatives 매뉴얼). --config로 바꿀 수 있지만 이는 서버 설정 변경이므로, 먼저 절차와 되돌리는 방법을 확인하세요.

3

cron 작업에는 PHP의 전체 경로 적기

cron은 로그인 셸과 분리된 자체 환경에서 작업을 실행합니다(crontab(5)). php만 적으면 어떤 PHP가 선택될지는 그 환경에 따라 달라집니다. 이 사이트의 권장 사항은 패치한 버전의 전체 경로를 적어, 업데이트 후 점검에서 답이 하나만 나오도록 하는 것입니다.

4

웹 쪽에서 웹 PHP 버전 확인

WordPress라면 관리자 화면의 사이트 상태(Site Health)에서 확인할 수 있습니다. 코드에서는 PHP_VERSION(버전)과 PHP_SAPI(실행 방식: CLI는 cli, PHP-FPM은 fpm-fcgi, Apache 모듈은 apache2handler)이라는 상수를 사용할 수 있습니다(php_sapi_name).

5

확인 직후 phpinfo() 페이지 삭제

phpinfo()를 호출하는 공개 파일은 버전을 보여 주지만, 설정과 서버 내부 정보도 함께 노출합니다. 확인이 끝나면 즉시 삭제하세요. 남겨 두지 않는 것이 이 방법을 쓰는 전제 조건입니다.

가정 2: "diff는 성공하거나 실패한다"

점검 모드를 해제하는 임시 스크립트는 무언가를 기록하기 전에 diff로 현재 내용과 새 내용의 차이를 확인하도록 작성되었습니다. 이 스크립트에는 set -e가 켜져 있었습니다.

diff는 차이를 발견하면 종료 상태 1을 반환합니다. GNU 매뉴얼에서 0은 차이 없음, 1은 차이 발견, 2는 문제 발생을 뜻합니다(GNU diffutils: diff). 차이를 발견하는 것은 스크립트가 기대한 바로 그 상황이었습니다. 그러나 set -e(errexit)는 조건문 등 일부 위치를 제외하고, 0이 아닌 상태를 만나면 셸을 중단시킵니다. 스크립트는 차이가 있다는 이유로 중단되었고, 기록 단계에 도달하지 못했습니다.

그래서 점검 모드를 해제하는 단계는 아무것도 기록하지 않고 멈췄고, 피해는 없었습니다. 안전한 쪽으로 실패한 것이지만, 반대로 멈춰야 할 때 멈추지 않는 조합은 위험했을 것입니다. 스크립트는 다시 만들어졌습니다.

종료 상태 1이 "오류"가 아닌 명령어

명령어1의 의미2의 의미출처
diff차이 발견문제 발생GNU diffutils: diff
cmp차이 발견문제 발생GNU diffutils: cmp
grep선택된 줄 없음(일치 없음)오류 발생GNU grep

안전한 패턴

GNU Bash 매뉴얼에 따르면, set -e 상태에서 if 검사의 일부로 명령이 0이 아닌 값을 반환하거나, 마지막 명령이 아닌 && 또는 || 목록에 속하거나, !로 상태가 반전된 경우에는 셸이 종료되지 않습니다(GNU Bash: The Set Builtin). 이 규칙을 활용하세요.

중단되거나, 실제 실패를 숨김

  • set -e 아래에서 diff a b를 그대로 쓰기(차이가 있다는 이유만으로 중단됨)
  • diff a b || true (차이와 실제 오류(2)를 모두 삼킴)
  • grep에 || true를 붙여 "일치 없음"과 "파일을 읽을 수 없음"을 같게 취급하기

종료 코드를 따로 처리

  • 종료 상태를 변수에 저장하고 0, 1, 2에 따라 분기하기(권장 패턴)
  • 차이가 있는지만 알면 되면 if diff -q a b; then …도 쓸 수 있지만, 2(오류)도 "차이 있음" 쪽으로 분류된다는 점에 유의하세요
  • || true를 쓴다면 먼저 2(실제 오류)를 놓쳐도 되는지 결정하세요

다음은 0, 1, 2를 구분하는 예시입니다. || 왼쪽의 실패는 errexit에서 제외되므로 스크립트가 중단되지 않고, rc에 종료 상태가 저장됩니다.

#!/usr/bin/env bash
set -euo pipefail
 
rc=0
diff -rq deployed/ live/ || rc=$?
 
case "$rc" in
  0) echo "차이 없음" ;;
  1) echo "차이 있음: 계속하기 전에 확인" ;;
  *) echo "diff 실패" >&2; exit 2 ;;
esac

set -e는 완전한 안전망이 아닙니다. 실수를 가릴 수 있는 예외가 많으므로, 중요한 분기에서는 종료 상태를 직접 읽고 판단하는 편이 낫습니다(이 사이트의 권장 사항).

패치 후: 모든 서버에서 같은 점검 실행

패치는 진입점을 막을 뿐입니다. 업데이트 이전에 누군가 들어왔는지는 알려 주지 않습니다. KEV 취약점에 대해 CISA는 업데이트 전 침입 여부 확인을 요청합니다(PHP 관련 글의 "악용 확인 및 CISA KEV 등재" 항목 참조). 서버마다 점검 내용이 달라지지 않도록, 운영자는 모든 서버에 같은 점검 목록을 적용했습니다.

항목확인할 내용예시 명령
로그인 키등록한 기억이 없는 공개 키authorized_keys 열기
사용자알 수 없는 사용자, 관리자 그룹 변경getent passwd, getent group sudo
패키지 소유 파일 변경패키지가 설치한 파일 중 배포 이후 바뀐 파일debsums -s(Debian 계열), rpm -Va(RHEL 계열)
어떤 패키지에도 속하지 않는 실행 파일/usr/local, /opt 또는 PATH에 있는 새 실행 파일 중 소유 패키지가 없는 것dpkg -S <파일>(Debian 계열) 또는 rpm -qf <파일>(RHEL 계열); 소유 패키지가 없는 파일을 찾기
SUID소유자의 권한으로 실행되는 새 파일find / -xdev -perm -4000 -type f
cron과 systemd알 수 없는 예약 작업과 타이머crontab -l, /etc/crontab, /etc/cron.d, systemctl list-timers
/tmp그곳에 놓인 실행 파일find /tmp /var/tmp -type f -perm /111
외부 연결알아볼 수 없는 목적지로의 연결ss -tunp
컨테이너 내부실행 중인 프로세스와 시작 이후 기록된 파일docker top, docker diff

crontab -l은 현재 사용자의 작업만 보여 주므로, /etc/crontab, /etc/cron.d, 사용자별 cron 스풀을 따로 확인하세요. ss -tunp는 모든 연결의 프로세스를 보려면 root 권한이 필요합니다. dpkg -S와 rpm -qf는 파일을 소유한 패키지를 알려 줍니다. "소유 패키지 없음"은 직접 배치한 파일일 수도 있으므로, 설치한 기억과 대조하세요.

find -perm -4000은 지정한 모든 비트가 설정된 파일을 찾습니다(find(1)). docker diff는 컨테이너 생성 이후 변경된 파일을 나열하고(A: 추가, C: 변경, D: 삭제), docker top은 실행 중인 프로세스를 보여 줍니다(docker container diff, docker container top). 컨테이너는 보통 수정하기보다 다시 빌드하므로, docker diff에 나타난 실행 파일은 예상하지 못한 것으로 보고 조사하세요(이 사이트의 권장 사항).

각 항목을 읽는 방법과 공유 호스팅, WordPress, VPS의 절차는 웹사이트나 서버가 해킹되었는지 확인하는 방법에 있습니다.

파일 비교로 의심스러운 로그 항목 정리하기

이 점검 중 팀은 업데이트 이전에 Tor 출구 노드(발신자를 숨기는 네트워크의 출구 지점)에서 문의 양식으로 POST 요청이 도달한 사실을 발견했습니다. 로그는 요청이 도착했다는 사실만 보여 줄 뿐, 그 결과로 무언가 기록되었는지는 보여 주지 않습니다.

그래서 팀은 실시간 공개 파일을 배포 시점에 보관해 둔 사본과 비교했습니다. 둘은 정확히 일치했고, 추가된 PHP 파일도 없었습니다. 이 점검에서는 어떤 파일도 변경되었다는 징후를 찾지 못했습니다.

1

배포한 내용의 사본을 서버 밖에 보관

비교 대상이 서버 안에만 있다면, 동시에 바뀐 내용은 알아채지 못합니다. 배포할 때마다 게시된 상태를 서버 밖에 보관하세요.

2

실시간 파일과 비교

diff -rq로 다른 파일을 찾거나, 파일별 해시 목록(sha256sum)을 비교하세요. 차이가 있다면 의도한 변경인지 확인하세요.

3

추가된 파일도 확인

내용이 같더라도 새로 놓인 PHP 파일은 원본 사본에 없으므로 별도로 찾아야 합니다. diff -rq는 한쪽에만 있는 파일도 보고합니다.

4

일치가 곧 안전을 뜻하지는 않음

이 방법은 비교한 파일이 변경되지 않았다는 사실만 보여 줍니다. 데이터베이스, 업로드 폴더, 서버 외부의 변경은 보지 못합니다. 다른 점검과 함께 판단하세요(이 사이트의 권장 사항).

이 사이트의 견해: 긴급 상황 전에 점검 목록을 정해 두기

급하게 처리하는 밤에는 서버마다 무엇을 확인했는지가 달라지기 쉽습니다. 점검 항목을 미리 정해 두면 모든 서버에 같은 점검이 적용되고 빈틈이 드러납니다. 이번에 예전 명령줄 PHP를 찾아낼 수 있었던 것도, 업데이트 후 점검이 웹에서 끝나지 않았기 때문입니다.

오늘 할 일

  1. 서버에 로그인할 수 있다면 php -v와 which php를 실행하고, cron 작업에 PHP의 전체 경로를 적으세요
  2. 사이트 상태(Site Health) 등 웹 쪽에서 웹 PHP 버전을 확인하세요(사용 후 phpinfo() 페이지는 바로 삭제하세요)
  3. set -e 스크립트에서 diff, grep, cmp를 호출하는 부분을 찾고, 종료 상태 1을 어떻게 처리할지 정하세요
  4. 게시한 내용의 사본을 서버 밖에 보관하고, 업데이트 후 비교하세요
  5. 침입 점검 목록을 모든 서버에 동일하게 적용하고, 항목별로 확인하세요

출처

다음으로 읽기

FAQ

QPHP를 업데이트했는데도 여전히 이전 버전일 수 있나요?
A

네. 웹 요청을 처리하는 PHP(PHP-FPM 또는 Apache 모듈)와 셸이나 cron에서 호출하는 명령줄 PHP(CLI)는 버전이 다른 별도의 프로그램일 수 있습니다. php.net은 PHP가 실행되는 방식을 SAPI라고 부르며, CLI SAPI가 다른 SAPI와 여러 면에서 다르다고 밝히고 있습니다. 웹 쪽은 패치되었는데 cron이 사용하는 PHP는 여전히 이전 버전일 수 있습니다.

Q실제로 실행 중인 PHP 버전은 어떻게 확인하나요?
A

명령줄에서는 php -v를 실행합니다. 웹에서는 웹 쪽에서 확인하세요. WordPress라면 관리자 화면의 사이트 상태(Site Health)에서 볼 수 있습니다. phpinfo() 페이지를 사용한다면 확인이 끝나는 즉시 삭제하세요. which php는 호출되는 실행 파일을 보여주며, 여러 버전이 설치된 Debian 계열 시스템에서는 update-alternatives --display php로 php 명령이 가리키는 위치를 확인할 수 있습니다. cron 작업에서는 PHP의 전체 경로를 적어 두면 어떤 PHP가 실행되는지 혼동을 없앨 수 있습니다.

Qset -e를 사용한 셸 스크립트가 diff를 쓰면 왜 중단되나요?
A

diff는 차이를 발견하면 종료 상태 1을 반환합니다. GNU 매뉴얼은 0을 차이 없음, 1을 차이 있음, 2를 문제 발생으로 정의합니다. set -e(errexit)는 0이 아닌 상태에서 스크립트를 중단시키므로, 차이를 발견한 것만으로도 중단됩니다. 명령을 if 조건에 넣거나 종료 상태를 변수에 저장해 0, 1, 2를 각각 구분해 처리하세요. grep(1은 일치 없음)과 cmp(1은 차이 있음)도 마찬가지입니다.

Q로그 항목이 의심스러울 때 침해를 당했는지 어떻게 판단하나요?
A

로그만으로는 판단할 수 없습니다. 로그는 도착한 시도를 기록할 뿐, 반드시 성공을 의미하지는 않습니다. 실시간 공개 파일을 배포 시점에 보관해 둔 사본과 비교해 서로 일치하는지, 추가된 PHP 파일이 없는지 확인하세요. 일치한다면 파일 변조가 발견되지 않았다고 말할 수 있습니다. 그러나 이는 안전을 증명하는 것이 아니므로 다른 점검과 함께 보세요.