Skip to content
신선한 자몽 농장
Go back

php2shell과 node2shell 취약점 점검

개요

지난 포스트에서 MCP로 Blender와 Figma를 조작하면서 AI 덕분에 생산성이 얼마나 올라가는지 체감했다. 그런데 그 편의성은 공격자에게도 똑같이 주어진다. 9월 말부터 10월까지 이어진 국내 금융권 연쇄 해킹에서 AI 침투테스트 도구가 쓰인 정황이 나왔고, 10월 20일 전후로는 PHP와 Node.js 런타임 자체에서 원격 코드 실행(RCE) 취약점이 연달아 터졌다.

이 글에서는 다음 내용을 정리한다.

이 글은 2026년 10월 26일 기준으로 공개된 자료를 바탕으로 작성했다. 수치나 패치 버전은 이후 바뀔 수 있으니 반드시 공식 권고문을 함께 확인하자.


1. AI가 만든 새로운 보안 위험

1-1. ARTEX를 사용한 국내 금융권 해킹

9월 27일 현대캐피탈과 웰컴저축은행, KB국민은행을 시작으로 국내 금융사를 노린 외부 공격이 잇따랐다. 가장 크게 알려진 건 신한은행이다. 대출모집인용 모바일 홈페이지 ‘M신한’에서 본인확인을 우회하고 고객번호 등에 값을 무작위로 대입해 조회하는 방식으로, 고객 25,729명의 대출 관련 개인·신용정보가 유출됐다. IP를 차단하면 공격자는 IP를 바꿔 다시 들어왔고, 신한은행은 9월 30일 새벽에야 공격을 최종 차단했다.

10월 3일, 금융보안원은 신한은행 로그에서 ARTEX(아르텍스) 관련 흔적을 확인했다고 발표했다. ARTEX는 공개 저장소에 “LLM 다중 에이전트 기반 자율 침투 시스템”으로 소개된 오픈소스 침투테스트 도구다. 원래 취약점 점검 용도로 만들어졌지만 이번 공격에 악용된 것으로 보인다.

이후 피해는 계속 늘어났다.

날짜내용
9/27현대캐피탈, 웰컴저축은행, KB국민은행 공격 시작
9/28 ~ 9/30신한은행 ‘M신한’ 비정상 접근, 고객 25,729명 유출
9/30하나은행 영업지원시스템(ODS) 공격
10/1BNK부산은행 웹서버 공격 탐지
10/3금융보안원, 신한은행 로그에서 ARTEX 흔적 확인 발표
10/4금융위원회 긴급 점검회의, 약 500개사에 공격 IP 전파
10/7크라우드스트라이크, ARTEX 설정 파일과 AI 에이전트 세션 기록 발견 발표
10/8ARTEX 개발자, 공개 개발 중단 발표
10/10 ~ 10/15새마을금고중앙회, 산은캐피탈, OK저축은행 등 후속 피해
10/14금융위원회, 침해 금융사 16개사·유출 약 10만 건 발표

금융위원회의 16개사·10만 건 집계는 같은 기간 신고된 침해사고를 모은 것이다. 모든 사고가 같은 공격자나 ARTEX에 의한 것인지는 아직 확인되지 않았다.

1-2. ARTEX는 어떻게 동작하는가

SK쉴더스와 크라우드스트라이크의 분석을 보면 구조는 지난 포스트에서 다룬 AI 에이전트와 크게 다르지 않다.

Goals    : 최종 목표를 세부 과제로 분해
   │
Planner  : 지금까지의 결과를 보고 다음 작업 결정
   │
Worker × N : 명령어 실행, HTTP 요청 등 실제 작업 수행
   │
Retester : 발견한 취약점을 다시 검증

Worker의 결과가 공유 저장소에 쌓이면 Planner가 이를 읽고 다음 작업을 정하는 루프를 돈다. 사람이 단계마다 명령을 입력하지 않아도 정찰부터 공격까지 여러 단계를 스스로 이어 간다. LLM은 외부 API로 붙이는 구조였고, 크라우드스트라이크는 DeepSeek 계열 모델이 주로 쓰였다고 밝혔다.

1-3. 무엇이 달라졌나

이번 사건에서 쓰인 기법 자체는 새롭지 않았다. 금융당국과 SK쉴더스가 꼽은 원인은 다음과 같다.

DB에 악성 쿼리를 주입하기보다 정상 조회 기능을 끈질기게 반복 호출하는 방식이었다. 예전에도 가능했던 공격이지만, 사람이 일일이 하기엔 귀찮고 오래 걸리던 작업을 AI 에이전트가 쉬지 않고 대신해 준 셈이다. 즉 AI가 새로운 취약점을 만들어 낸 게 아니라, 이미 있던 취약점을 찾아내고 악용하는 비용을 크게 낮췄다.

이런 상황에서 서버 런타임 자체에 인증 없이 코드를 실행할 수 있는 취약점이 공개되면, 공격 자동화 도구에 바로 들어가는 건 시간 문제다. 그래서 php2shell과 node2shell을 그냥 넘길 수 없었다.


2. php2shell과 node2shell

2-1. 왜 주목받는가

두 취약점은 특정 프레임워크나 라이브러리가 아니라 런타임 자체의 결함이다.

이름의 2shell은 Log4Shell처럼 “원격에서 셸을 얻을 수 있다”는 의미로 붙은 별명이다.

2-2. 취약점 목록

별칭CVE대상유형CVSS v4.0
php2shellCVE-2026-109618PHP요청 초기화 중 메모리 부족(OOM)으로 인한 메모리 손상9.2
php2shellCVE-2026-109619PHP해제된 요청 메모리를 전역 상태가 계속 참조(Use-After-Free)9.2
node2shellCVE-2026-111497Node.js (내장 Brotli)Brotli 디코더 범위 밖 쓰기(Out-of-Bounds Write)9.2

2-3. php2shell (CVE-2026-109618, CVE-2026-109619)

두 CVE는 따로 쓰이기보다 연결해서 악용된다.

  1. CVE-2026-109618: 헤더가 매우 많은 multipart 요청처럼, 겉보기엔 제한 안에 있지만 요청을 준비하는 과정에서 메모리 부족을 일으키는 요청을 보낸다. 그러면 내부 데이터가 반쯤 만들어진 상태로 남고, 정리 코드는 이 데이터를 그대로 신뢰한다.
  2. CVE-2026-109619: 치명적 오류로 요청이 끝나도 PHP 워커는 재사용된다. 이때 전역 상태가 이미 해제된 메모리를 가리킨 채 남아 있어서, 같은 워커가 처리하는 다음 요청이 이 오래된 데이터를 사용하게 된다. 세션 업로드 진행률(session.upload_progress) 경로를 통하면 애플리케이션이 session_start()를 호출하지 않아도 이 지점에 도달할 수 있다.

로그인 없이 웹 서버에서 임의 코드를 실행할 수 있다는 점이 핵심이다.

항목내용
영향 SAPIPHP-FPM, Apache mod_php, php-cgi, LiteSpeed, FrankenPHP 등 웹 요청을 처리하는 모든 방식
가장 위험한 환경Apache mod_php (애플리케이션 코드가 실행되기 전에 공격 성립)
공개 시점10월 20일 패치 전 세부 내용 유출, 10월 21일 PHP 보안 릴리스
실제 악용watchTowr가 10월 21일부터 실제 공격 관측, Unit 42는 유럽 정부기관 공격 사례 보고

영향 버전과 패치 버전

브랜치취약 버전패치 버전
8.5.x8.5.12 미만8.5.12
8.4.x8.4.27 미만8.4.27
8.3.x8.3.36 미만8.3.36
8.2.x8.2.35 미만8.2.35
7.4, 8.0, 8.1전체공식 릴리스 없음 (배포판 패키지 또는 소스 백포트)
7.3 이하 (PHP 5 포함)전체패치 없음, 지원 브랜치로 업그레이드 필요

PHP 측은 “설정 변경만으로는 이 취약점을 완전히 막을 수 없다”고 밝혔다. 아래에 나올 설정 변경은 어디까지나 패치 전 임시 조치다.

2-4. node2shell (CVE-2026-111497)

Node.js는 Brotli 압축 해제 라이브러리를 런타임 안에 포함해서 배포한다. 취약한 디코더는 블록 타입을 바꾸는 잘못된 스트림을 받았을 때 이를 거부하지 않고 계속 처리하다가 버퍼 밖에 데이터를 쓴다. 상황에 따라 프로세스 크래시, 메모리 유출, 코드 실행으로 이어질 수 있다.

어떤 경우에 노출되는가

경로설명
Express 5, body-parser 2.xContent-Encoding: br 요청 본문을 기본으로 압축 해제
내장 fetch()Brotli 응답을 자동으로 압축 해제 (악성·침해된 서버에 요청하는 경우)
node:zlib 직접 사용신뢰할 수 없는 입력에 brotliDecompress, createBrotliDecompress 호출

즉 외부 요청을 받는 서버뿐 아니라, 웹훅이나 사용자가 입력한 URL로 요청을 보내는 서버도 대상이 된다.

영향 버전과 패치 버전

릴리스 라인상태패치 버전
Node.js 26.xCurrent26.12.1
Node.js 24.xLTS24.22.1
Node.js 22.xLTS22.23.4
Node.js 25.x, 23.x, 20.x 이하지원 종료패치 없음
Bun1.4.4 미만 취약1.4.4
항목내용
공개 시점10월 21일 Node.js 보안 릴리스
PoC10월 22일 공개, 연구 환경에서 Node.js 24.22.0 대상 코드 실행 보고
실제 악용10월 23일 기준 실제 공격 보고 없음
연관 영향같은 디코더를 포함한 Python Brotli, brotlicffi 패키지도 영향 (일명 HTTP2Shell)

현재 이 블로그 플랫폼도 Node.js를 사용하였기에, 이번 일로 배포 워크플로도 이번 일로 Node.js를 22.23.4로 고정했다.


3. 점검 방법

점검은 두 단계로 진행한다.

  1. 버전 확인: 설치된 런타임이 패치 버전 이상인지 확인한다.
  2. 흔적 확인: 이미 공격받았는지 프로세스와 파일 변경을 확인한다.

이번 취약점은 POST 본문에서 발생하는데, 기본 access log에는 POST 본문이 남지 않는다. 로그가 깨끗하다고 안전하다고 단정하면 안 된다.

3-1. php2shell 점검

Windows

PHP는 XAMPP, WAMP나 각종 업무용 솔루션에 php.exe, php-cgi.exe가 따로 들어 있는 경우가 많다. PATH에 잡힌 PHP 하나만 보면 놓치기 쉽다.

PATH에 등록된 PHP부터 확인한다.

php -v
Get-Command php -All | Select-Object Source

드라이브 전체에서 PHP 실행 파일을 찾아 버전을 출력한다.

Get-ChildItem -Path C:\ -Include php.exe, php-cgi.exe -Recurse -ErrorAction SilentlyContinue |
ForEach-Object {
    $ver = & $_.FullName -r 'echo PHP_VERSION;' 2>$null
    "{0}  →  {1}" -f $_.FullName, $ver
}

php-cgi.exe는 -r 옵션이 동작하지 않을 수 있으니, 같은 폴더의 php.exe로 버전을 확인하거나 파일 속성의 버전 정보를 확인한다.

(Get-Item "C:\xampp\php\php-cgi.exe").VersionInfo.ProductVersion

업로드 진행률 설정도 확인한다.

php -i | Select-String "session.upload_progress.enabled|session.use_strict_mode|Loaded Configuration File"

Rocky Linux 9

Rocky Linux는 보안 패치를 백포트하기 때문에 php -v의 버전이 위 표의 패치 버전보다 낮아도 패치가 적용되어 있을 수 있다. 버전 숫자보다 패키지의 CVE 반영 여부를 봐야 한다.

설치된 PHP와 모듈 스트림을 확인한다.

php -v
rpm -qa | grep -E '^php' | sort
dnf module list php

설치된 패키지에 CVE가 반영됐는지 확인한다.

rpm -q --changelog php-common | grep -E 'CVE-2026-10961[89]'

아직 적용하지 않은 보안 업데이트가 있는지 확인한다.

dnf updateinfo list --cve CVE-2026-109618
dnf updateinfo list --cve CVE-2026-109619

결과가 나오면 아직 적용하지 않은 업데이트가 있다는 뜻이다. 업데이트한 뒤에는 반드시 PHP 워커를 재시작해야 한다. 워커가 재사용되는 구조라 재시작하지 않으면 이전 코드가 계속 메모리에 남아 있다.

sudo dnf update 'php*'
sudo systemctl restart php-fpm httpd

/opt/remi, /opt/alt 같은 경로에 별도로 설치한 PHP가 있다면 각각 따로 확인한다.

for p in /usr/bin/php /usr/bin/php[0-9]* /opt/remi/*/root/usr/bin/php /opt/alt/php*/usr/bin/php; do
    [ -x "$p" ] && echo "$p → $($p -r 'echo PHP_VERSION;')"
done

패치 전 임시 조치

바로 패치할 수 없다면 php.ini나 /etc/php.d/ 아래 설정 파일에서 다음 값을 적용한다.

session.upload_progress.enabled = 0
session.use_strict_mode = 1

이 설정은 공개된 세션 업로드 진행률 공격 경로만 막는다. 근본 해결이 아니므로 패치 전까지만 쓰고, 패치할 수 없는 서버는 외부 접근부터 막는 게 우선이다. 파일 업로드를 받지 않는 서비스라면 앞단 웹 서버나 WAF에서 multipart 요청을 차단하는 것도 방법이다.

침해 흔적 확인 (Rocky Linux 9)

PHP 워커가 셸이나 다운로드 도구를 실행했다면 강하게 의심해야 한다.

# php-fpm, httpd 하위에서 실행 중인 셸·다운로더 확인
ps -eo pid,ppid,user,cmd --forest | grep -E -B2 'php-fpm|httpd' | grep -E 'sh -c|bash|python|perl|curl|wget|/dev/tcp|mkfifo'

# 최근 7일간 웹 루트에 새로 생긴 PHP 파일 (웹셸 확인)
find /var/www -name '*.php' -mtime -7 -ls

# 웹 서버 계정의 cron 확인
sudo crontab -u apache -l

Windows에서는 php-cgi.exe나 httpd.exe의 자식 프로세스로 cmd.exe, powershell.exe가 실행된 기록이 있는지 확인한다.

Get-CimInstance Win32_Process |
Where-Object { $_.Name -match 'cmd|powershell' } |
ForEach-Object {
    $parent = Get-Process -Id $_.ParentProcessId -ErrorAction SilentlyContinue
    if ($parent.Name -match 'php|httpd|w3wp') { $_ | Select-Object ProcessId, Name, CommandLine, @{n='Parent';e={$parent.Name}} }
}

3-2. node2shell 점검

Windows

Node.js는 공식 설치본 외에 nvm-windows, Volta, 각종 데스크톱 앱 안에 포함된 런타임까지 여러 개가 설치되어 있는 경우가 많다.

node --version
Get-Command node -All | Select-Object Source

# nvm-windows를 쓴다면 설치된 버전 전체 확인
nvm list

드라이브 전체에서 node.exe를 찾아 버전을 출력한다.

Get-ChildItem -Path C:\ -Filter node.exe -Recurse -ErrorAction SilentlyContinue |
ForEach-Object {
    $ver = & $_.FullName --version 2>$null
    "{0}  →  {1}" -f $_.FullName, $ver
}

Electron 기반 데스크톱 앱에 들어 있는 런타임은 직접 업데이트할 수 없다. 앱 자체 업데이트를 확인하자.

Rocky Linux 9

node --version
rpm -qa | grep -E '^nodejs' | sort
dnf module list nodejs

Rocky Linux 9의 nodejs 패키지는 내장 Brotli 대신 시스템 Brotli 라이브러리를 쓸 수 있다. 이 경우 Node.js 버전이 아니라 Brotli 패키지 업데이트로 패치가 들어오므로 두 가지를 같이 확인한다.

# 시스템 Brotli 패키지
rpm -qa | grep -i brotli

# CVE가 반영된 업데이트가 있는지 확인
dnf updateinfo list --cve CVE-2026-111497
rpm -q --changelog brotli | grep CVE-2026-111497

nvm이나 공식 바이너리로 설치한 Node.js는 내장 Brotli를 쓰므로 버전 표 기준으로 판단하면 된다.

ls ~/.nvm/versions/node/ 2>/dev/null

컨테이너는 이미지 태그만 믿지 말고 실행 중인 컨테이너 안에서 직접 확인한다. node:24처럼 고정되지 않은 태그는 캐시 때문에 이전 버전이 남아 있을 수 있다.

docker ps --format '{{.Names}}' | while read c; do
    echo "$c → $(docker exec "$c" node --version 2>/dev/null)"
done

프로젝트 의존성 확인

Windows, Rocky Linux 모두 프로젝트 폴더에서 다음을 확인한다.

# 요청 본문을 기본으로 압축 해제하는 패키지
npm ls express body-parser

# 코드에서 Brotli 압축 해제를 직접 호출하는 곳
grep -rnE 'brotliDecompress|createBrotliDecompress' --include='*.js' --include='*.ts' . --exclude-dir=node_modules

Python 프로젝트에도 같은 디코더가 들어 있을 수 있으니 같이 확인한다. 패치 버전은 각 패키지의 권고문을 확인하자.

pip show Brotli brotlicffi 2>/dev/null | grep -E 'Name|Version'

AI CLI 에이전트 점검

서버를 운영하지 않는다고 해서 안심할 수는 없다. 요즘 많이 쓰는 AI CLI 에이전트는 대부분 npm으로 설치한다.

npm install -g @anthropic-ai/claude-code   # Claude Code
npm install -g @openai/codex               # Codex

이렇게 설치한 CLI는 PC에 깔린 Node.js 위에서 동작한다. 그리고 AI 에이전트는 웹 페이지 조회, 원격 MCP 서버 연결, 문서 검색처럼 외부 서버로 요청을 보내고 응답을 받는 작업을 계속 한다. 2-4절에서 본 fetch() 경로가 그대로 해당되는 셈이다. 에이전트가 악성 페이지나 침해된 서버에서 Brotli로 압축된 응답을 받으면 그 응답이 공격 입력이 될 수 있다.

게다가 에이전트는 내 계정 권한으로 돌아가고, 셸 실행 권한과 API 키, SSH 키, 소스 코드에 접근할 수 있다. 일반 웹 서버보다 뚫렸을 때 피해가 더 클 수 있다.

전역으로 설치한 npm 패키지를 먼저 확인한다.

npm ls -g --depth=0

CLI가 어떤 Node.js로 실행되는지도 확인한다. Node.js가 여러 개 깔려 있으면 node --version에 나온 버전과 CLI가 실제로 쓰는 버전이 다를 수 있다.

# Rocky Linux 9: 실행 파일 위치와 첫 줄(shebang) 확인
which claude codex 
head -1 "$(readlink -f "$(which claude)")"
# Windows: npm이 만든 실행 래퍼(.cmd, .ps1) 위치 확인
Get-Command claude, codex -All -ErrorAction SilentlyContinue | Select-Object Name, Source

여기서 주의할 점이 있다. CLI 패키지를 업데이트한다고 Node.js가 같이 패치되지는 않는다. 취약점은 Node.js 런타임에 있으므로 두 가지를 모두 업데이트해야 한다.

  1. Node.js를 패치 버전(26.12.1 / 24.22.1 / 22.23.4) 이상으로 업데이트
  2. AI CLI 패키지도 최신 버전으로 업데이트
npm install -g @anthropic-ai/claude-code@latest @openai/codex@latest 

nvm으로 Node.js 버전을 바꿨다면 전역 패키지는 버전별로 따로 설치되므로, 새 버전에서 CLI를 다시 설치해야 한다.

nvm install 24.22.1
nvm use 24.22.1
npm install -g @anthropic-ai/claude-code @openai/codex

npm이 아닌 공식 설치 스크립트나 단독 실행 파일로 설치한 CLI는 자체 런타임을 포함하고 있을 수 있다. 이 경우 시스템 Node.js 업데이트와 관계없이 CLI 자체 업데이트로 패치를 받아야 하니, 각 CLI의 릴리스 노트를 확인하자.

패치 전 임시 조치

Node.js 공식 권고문에는 별도 우회 방법이 없다. 런타임 업데이트가 유일한 해결책이다. 당장 업데이트할 수 없다면 리버스 프록시에서 Content-Encoding: br 요청을 거부하는 방법이 있다. 다만 서버가 외부로 요청해서 받는 Brotli 응답(fetch())은 막지 못한다.

# nginx 예시
if ($http_content_encoding ~* "br") {
    return 415;
}

침해 흔적 확인

실패한 공격은 대부분 프로세스 크래시로 끝난다. Node.js 프로세스가 원인 없이 죽었다가 재시작된 기록이 반복되면 확인해 볼 필요가 있다.

# systemd로 띄운 서비스라면 크래시·재시작 기록 확인
journalctl -u <서비스명> --since "2026-10-21" | grep -iE 'segfault|core dumped|exited|signal'

# node 프로세스 하위에서 셸이 실행 중인지 확인
ps -eo pid,ppid,user,cmd --forest | grep -E -A3 '[n]ode ' | grep -E 'sh -c|bash|curl|wget'

Windows에서는 이벤트 뷰어의 응용 프로그램 로그에서 node.exe의 Application Error(Event ID 1000) 기록을 확인한다.

Get-WinEvent -FilterHashtable @{ LogName='Application'; Id=1000; StartTime='2026-10-21' } -ErrorAction SilentlyContinue |
Where-Object { $_.Message -match 'node.exe' } |
Select-Object TimeCreated, Message

핵심 요약

구분php2shellnode2shell
CVECVE-2026-109618, CVE-2026-109619CVE-2026-111497
원인OOM 메모리 손상 + 해제된 메모리 재사용내장 Brotli 디코더 범위 밖 쓰기
패치 버전8.5.12 / 8.4.27 / 8.3.36 / 8.2.3526.12.1 / 24.22.1 / 22.23.4
실제 악용10/21부터 관측10/23 기준 보고 없음 (PoC 공개)
Windows 점검php.exe, php-cgi.exe 전체 검색node.exe 전체 검색, nvm 확인
Rocky Linux 9 점검dnf updateinfo --cve, changelogNode.js + 시스템 Brotli 패키지 같이 확인
임시 조치session.upload_progress.enabled = 0프록시에서 Content-Encoding: br 차단
패치 후PHP 워커 재시작 필수컨테이너 이미지 재빌드
놓치기 쉬운 곳솔루션에 포함된 php-cgi.exenpm으로 설치한 AI CLI 에이전트

정리하며

이번 금융권 해킹에서 쓰인 기법은 본인확인 우회, 권한 검증 누락, 반복 조회처럼 이미 알려진 것들이었다. 달라진 건 공격자가 이를 찾고 시도하는 데 드는 시간과 노력이다. AI 에이전트는 지치지 않고, IP가 막히면 바꿔서 다시 들어오고, 결과를 보고 다음 작업을 스스로 정한다.

지난 포스트에서 Blender MCP를 쓰면서 인증 없이 임의 코드를 실행할 수 있는 포트를 네트워크에 열지 않기로 했던 것도 같은 이유다. 사람이라면 귀찮아서 그냥 지나갈 포트 하나, 패치 하나를 AI는 놓치지 않는다.

php2shell처럼 패치보다 공격이 먼저 나오는 상황에서는 “언젠가 업데이트해야지”가 통하지 않는다. 그러니 최소한 다음 세 가지는 지금 확인해 두자.

AI가 편의성을 주는 만큼, 그 편의성을 쓰는 상대방도 있다는 걸 잊지 말자.


참고 자료



Next Post
Hyper-V를 껐는데도 VBS가 계속 실행되는 문제 해결