왜 Wazuh가 React2Shell을 놓쳤는가

발행: (2025년 12월 28일 오후 05:36 GMT+9)
11 분 소요
원문: Dev.to

Source: Dev.to

죄송합니다만, 번역하려는 실제 텍스트를 제공해 주시면 해당 내용을 한국어로 번역해 드리겠습니다. 현재는 링크만 제공되어 있어 번역할 본문이 없습니다. 텍스트를 복사해서 보내주시면 바로 번역해 드리겠습니다.

Introduction

보안 커뮤니티는 최근 React2Shell이라는 치명적인 무인증 원격 코드 실행(RCE) 취약점으로 큰 충격을 받았습니다. 이 취약점은 처음에 두 개의 CVE로 추적되었습니다:

  • CVE-2025-55182 – React
  • CVE-2025-66478 – Next.js

CVSS 점수 10.0을 받은 이 취약점은 **Next.js 15/16 (App Router)**와 React 19의 RSC 구현에 의존하는 모든 프레임워크에 영향을 미칩니다.

Wazuh는 이 취약점을 탐지하는 방법을 설명하는 훌륭한 블로그를 게시했습니다:

👉

하지만 더 깊이 연구해 보니, 이 CVE뿐만 아니라 대부분의 최신 애플리케이션 스택에 적용되는 중요한 제한 사항을 발견했습니다.

탐지 격차: SIEM이 로컬 프로젝트를 놓치는 이유

표준 취약점 탐지기(기본 Wazuh 모듈 포함)는 aptnpm install -g와 같은 시스템 관리자를 통해 설치된 패키지를 찾는 데 뛰어납니다. 하지만 현대 개발은 다른 곳에서 이루어집니다:

기술의존성의 일반적인 위치
Node.js각 프로젝트 내부의 로컬 node_modules 폴더
Python시스템과 격리된 가상 환경(venv, uv)
Docker컨테이너 레이어 내부( /var/lib/docker/...)에 패키지가 숨겨짐

SIEM이 이러한 로컬 경로를 살펴보지 않으면 눈이 멀게 됩니다. 따라서 프로젝트 디렉터리 안에 있는 취약한 버전의 Next.js나 React는 Wazuh가 놓치게 됩니다.

이 문제는 이전에 여기서 설명했습니다:

👉

Wazuh는 몇 가지 도움이 되는 기능을 제공하지만, 이 문제를 완전히 해결하지는 못합니다.

IT 위생 / 취약점 탐지

  • 문서:
  • 문서:

무엇을 하는가: 전역에 설치된 패키지만 감지합니다.

놓치는 것:

  • 로컬 Node.js 프로젝트
  • Python 가상 환경
  • Docker 컨테이너

파일 무결성 모니터링(FIM)

  • 문서:

무엇을 하는가: 파일 변경을 감지합니다.

추출하지 않는 것:

  • 패키지 이름
  • 버전
  • 취약점 상태

명령 모니터링

  • 문서:

장점: Wazuh 관리자로부터 스크립트를 실행할 수 있습니다.

단점:

  • 위험도가 높음 – 관리자가 손상되면 모든 에이전트가 손상됩니다.
  • 빈번한 스캔에는 권장되지 않음.

그래서 보다 안전하고 정확한 접근 방식이 필요했습니다.

Source:

솔루션: 맞춤형 인벤토리 워크플로

정적 스캐너만으로는 충분하지 않습니다. 우리는 다음을 수행하는 사전 대응 스크립트가 필요합니다:

  1. 파일 시스템을 탐색하여 package.json 파일을 찾습니다.
  2. 특정 고위험 라이브러리(react, next)의 버전 데이터를 추출합니다.
  3. 결과를 JSON 파일에 기록합니다.
  4. 해당 파일을 Wazuh에 전달하여 실시간 알림을 생성합니다.

Step 1: 인벤토리 스크립트

아래는 /var/lib/docker를 제외하지 않는 가벼운 Python 스크립트이며, 이를 통해 컨테이너 이미지 내부의 취약 라이브러리를 찾을 수 있습니다.

/usr/local/bin/scan_react2shell.py 로 저장하고 실행 권한을 부여하세요 (chmod +x /usr/local/bin/scan_react2shell.py).

#!/usr/bin/env python3

import json
import os

# --- CONFIGURATION -------------------------------------------------
LOG_FILE      = "/var/log/react2shell_scan.json"
SEARCH_PATHS = ["/"]                     # Roots to scan
EXCLUDE_DIRS = {"/proc", "/sys", "/dev", "/run", "/tmp", "/snap", "/boot"}

# ------------------------------------------------------------------
def scan_system():
    print(f"[*] Starting inventory scan on: {SEARCH_PATHS}")

    # Ensure log file exists
    if not os.path.exists(LOG_FILE):
        try:
            open(LOG_FILE, "w").close()
            print(f"[*] Created new log file at {LOG_FILE}")
        except IOError as e:
            print(f"[!] Could not create log file: {e}")
            return

    inspected = 0

    for root_dir in SEARCH_PATHS:
        for dirpath, dirnames, filenames in os.walk(root_dir, followlinks=True):
            # Skip excluded directories
            if any(excl in dirpath for excl in EXCLUDE_DIRS):
                dirnames[:] = []          # Prevent walk into sub‑dirs
                continue

            if "package.json" not in filenames:
                continue

            # Identify if the package is a top‑level react/next module
            parent_dir      = os.path.basename(dirpath)
            grandparent_dir = os.path.basename(os.path.dirname(dirpath))

            pkg_name = None
            if grandparent_dir == "node_modules":
                if parent_dir == "react":
                    pkg_name = "react"
                elif parent_dir == "next":
                    pkg_name = "next"

            if not pkg_name:
                continue

            pkg_path = os.path.join(dirpath, "package.json")
            try:
                with open(pkg_path, "r", encoding="utf-8", errors="ignore") as f:
                    data = json.load(f)
                version = data.get("version", "0.0.0")
                print(f"[+] Found {pkg_name} v{version} at {pkg_path}")
                inspected += 1

                # Tag Docker locations for easier downstream processing
                display_path = pkg_path
                if "/var/lib/docker" in pkg_path or "/var/lib/containerd" in pkg_path:
                    display_path = f"[DOCKER] {pkg_path}"

                # Append entry to log file
                entry = {
                    "package": pkg_name,
                    "version": version,
                    "path": display_path,
                    "timestamp": int(time.time())
                }
                with open(LOG_FILE, "a", encoding="utf-8") as log_f:
                    log_f.write(json.dumps(entry) + "\n")

            except (IOError, json.JSONDecodeError) as e:
                print(f"[!] Error reading {pkg_path}: {e}")

    print(f"[*] Scan complete – inspected {inspected} vulnerable packages.")
    return

if __name__ == "__main__":
    scan_system()

스크립트가 수행하는 작업

  • 전체 파일 시스템(시작점은 /)을 순회합니다.
  • 프로젝트 코드가 존재하지 않을 것으로 예상되는 잘 알려진 시스템 디렉터리를 건너뜁니다.
  • node_modules/react 또는 node_modules/next 내부에 있는 package.json 파일을 찾습니다.
  • version 필드를 추출하여 /var/log/react2shell_scan.json 에 JSON 라인 형태로 기록합니다.
  • Docker 저장 경로( /var/lib/docker 또는 /var/lib/containerd)에 위치한 결과는 [DOCKER] 태그를 붙여 추후 처리하기 쉽게 합니다.

rd`).

이 스크립트를 cron이나 systemd 타이머로 예약하고, Wazuh가 생성된 로그 파일을 수집하도록 구성하여 알림을 생성할 수 있습니다.

다음 단계

  1. Wazuh localfile 규칙을 생성하여 /var/log/react2shell_scan.json을 감시하고 JSON 항목을 파싱합니다.
  2. 알림 규칙을 정의하여 버전이 알려진 취약 범위와 일치할 때(예: React 및 Next.js의 취약 버전 범위) 트리거되도록 합니다.

참고: 이 스크립트는 package.json 파일을 스캔하고 reactnext와 같은 패키지를 표시하며, 해당 패키지가 Docker 경로 내부에 있는지도 감지합니다.

샘플 출력

{"timestamp":"2025-12-11T13:15:07","scan_type":"react_inventory","package":"react","version":"19.2.1","path":"[DOCKER]/var/lib/docker/.../node_modules/react/package.json"}
{"timestamp":"2025-12-11T13:15:08","scan_type":"react_inventory","package":"next","version":"15.4.5","path":"/home/user/app/node_modules/next/package.json"}

Source:

Step 2 – Wazuh Integration

스크립트가 /var/log/react2shell_scan.json 파일을 생성하면, Wazuh가 이를 수집하도록 설정합니다.

1. 에이전트 구성 (localfile)

Wazuh 에이전트 설정 파일(ossec.conf)에 다음 블록을 추가하거나 중앙 집중식 구성으로 적용합니다:

<localfile>
  <log_format>json</log_format>
  <location>/var/log/react2shell_scan.json</location>
</localfile>

2. 중앙에서 구성 푸시

모든 서버에서 ossec.conf를 일일이 수정하지 않으려면 Wazuh의 중앙 집중식 구성 기능을 사용합니다.

Wazuh Manager에서 기본 그룹에 대한 공유 구성을 편집합니다:

vim /var/ossec/etc/shared/default/agent.conf

<agent_config> 태그 안에 다음 블록을 삽입합니다:

<agent_config>
  <localfile>
    <log_format>json</log_format>
    <location>/var/log/react2shell_scan.json</location>
  </localfile>
</agent_config>

작동 방식

  • 동기화: Manager에서 파일을 저장하면 새로운 MD5 체크섬이 생성됩니다.
  • 전파: 다음 하트비트 시점에 에이전트가 변경을 감지하고, 업데이트된 agent.conf를 다운로드한 뒤 로그 수집기를 재시작합니다.
  • 가시성: Python 스크립트가 (cron으로 실행) /var/log/react2shell_scan.json을 업데이트할 때마다 해당 데이터가 Wazuh Manager로 스트리밍되어 규칙 평가가 이루어집니다.

3. 탐지 규칙 생성

관리자의 로컬 규칙 파일( /var/ossec/etc/rules/local_rules.xml)에 다음 규칙을 추가합니다:

<rule id="100500" level="12">
  <if_sid>100500</if_sid>
  <field name="type">json</field>
  <field name="program_name">react_inventory</field>
  <description>Inventory: Found $(package) version $(version) at $(path).</description>
</rule>

<rule id="100501" level="12">
  <if_sid>100500</if_sid>
  <field name="package">react</field>
  <field name="version">^19\.0\.0$|^19\.1\.[0-1]$|^19\.2\.0$</field>
  <cve>CVE-2025-55182</cve>
  <reference>https://nvd.nist.gov/vuln/detail/CVE-2025-55182</reference>
  <description>Vulnerable React2Shell detected: React $(version) at $(path)</description>
  <mitre> T1190 </mitre>
</rule>

<rule id="100502" level="12">
  <if_sid>100500</if_sid>
  <field name="package">next</field>
  <field name="version">^15\.0\.[0-4]$|^15\.1\.[0-8]$|^15\.4\.[0-7]$|^16\.0\.[0-6]$</field>
  <cve>CVE-2025-66478</cve>
  <reference>https://nvd.nist.gov/vuln/detail/CVE-2025-66478</reference>
  <description>Vulnerable React2Shell detected: Next.js $(version) at $(path)</description>
  <mitre> T1190 </mitre>
</rule>

Step 3 – Ansible를 통한 배포

스크립트, 크론 작업 및 권한을 많은 서버에 **Wazuh의 “Command Monitoring”**을 사용하지 않고 배포합니다.

---
- name: Deploy React2Shell inventory scanner
  hosts: all
  become: true

  vars:
    script_src: files/react2shell_scan.py
    script_dest: /usr/local/bin/react2shell_scan.py
    log_file: /var/log/react2shell_scan.json
    cron_job: "0 2 * * * /usr/bin/python3 {{ script_dest }}"

  tasks:
    - name: Copy Python scanner script
      copy:
        src: "{{ script_src }}"
        dest: "{{ script_dest }}"
        mode: '0755'

    - name: Ensure log file exists with correct permissions
      file:
        path: "{{ log_file }}"
        state: touch
        owner: wazuh
        group: wazuh
        mode: '0640'

    - name: Install daily cron job
      cron:
        name: "React2Shell inventory scan"
        minute: "0"
        hour: "2"
        job: "{{ cron_job }}"
        user: root

플레이북 실행:

ansible-playbook -i inventory.yml deploy-react2shell.yml

알림 / 메신저 통합

커뮤니티 통합을 사용하여 선호하는 채팅 플랫폼에서 실시간 알림을 받으세요.

Wazuh‑Telegram‑Slack‑Teams

  • Slack:
  • Telegram:

Microsoft Teams 통합

결론

“System Inventory”에서 “Application Inventory”로 전환함으로써, 로컬 node_modules 폴더에 숨겨져 있었을 가능성이 있는 CVSS 10.0 취약점을 포착했습니다.

Back to Blog

관련 글

더 보기 »