Nuclei 템플릿을 직접 써서 탐지 로직 만들기

🛡️ 강사 제공 인가된 실습 환경에서 수행

Nuclei란

YAML 템플릿 기반의 오픈소스 취약점 스캐너다. 내가 만든 YAML 탐지 규칙을 읽어서, 서버에 자동으로 HTTP 요청을 보내고, 응답을 분석해 “취약점이 있다/없다”를 판단한다.

단계Nuclei가 자동으로 하는 것
요청 전송YAML에 정의된 요청 자동 전송
쿠키 처리받은 쿠키 자동 저장/재사용
응답 분석정의한 키워드가 있는지 자동 확인
결과 출력1 matches found 자동 출력

강점은 규모다. 수천 개 서버도 템플릿 하나로 한 번에 스캔할 수 있고, 전 세계 보안 연구자들이 만든 템플릿 수만 개를 공유한다. 새 취약점을 발견하면 YAML 템플릿을 만들어 커뮤니티에 올리는 흐름이다.

YAML과 용어 정리

YAML은 데이터를 사람이 읽기 쉽게 표현하는 설정 파일 형식이다.

# JSON 방식
{"이름": "홍길동", "나이": 20}

# YAML 방식 (중괄호/따옴표 없이 깔끔)
이름: 홍길동
나이: 20

규칙은 세 개뿐이다. 키: 값, 들여쓰기(스페이스 2칸)로 계층 표현, - 로 목록 표현.

용어를 헷갈리기 쉬운데 이렇게 갈린다.

용어의미
CVE취약점에 붙는 번호/식별자 (예: CVE-2015-8562)
YAML 템플릿그 취약점을 탐지하는 파일
PoC 코드취약점을 실제로 증명하는 코드

CVE는 이름표고, YAML은 탐지 도구다.

템플릿 구조

id: 템플릿 식별자

info:
  name: 템플릿 이름
  severity: 심각도 (high 등)
  description: 설명
  tags: 태그

http:
  - method: GET           # 어떤 방식으로 요청
    path: 요청 경로        # 어디로 요청
    matchers: 탐지 조건    # 어떤 응답이면 취약?
    extractors: 추출 값    # 결과에서 뭘 뽑을지

matchers 는 응답이 조건을 만족하면 “취약점 발견”으로 판단하는 부분이다.

타입설명
status상태코드 확인 (예: 200)
word특정 단어가 응답에 있는지
regex정규식 패턴 매칭
dsl복합 조건식

extractors 는 응답에서 버전 번호 같은 특정 값을 뽑아낸다.

단일 요청과 다중 요청

여기가 이 실습의 핵심이었다.

단일 요청다중 요청
요청 횟수1번2번 이상
flow 설정없음flow: http(1) && http(2)
정확도낮음 (오탐 가능)높음
방식바로 공격확인 후 공격
&& = AND (둘 다 성공해야)
|| = OR  (하나만 성공해도)

http(1) && http(2)
= 1번 요청 성공 AND 2번 요청 성공

동작 흐름은 이렇다.

1번 요청 실패 → 탐지 안 함 (2번 요청 안 보냄)
1번 요청 성공 → 2번 요청 전송

2번 요청 실패 → 탐지 안 함
2번 요청 성공 → 1 matches found!

다중 요청을 쓰는 이유는 오탐을 줄이기 위해서다.

1번 요청: "이 서버가 진짜 대상 맞아?" 확인
        ↓ 맞으면
2번 요청: "실제로 취약해?" 공격/확인
        ↓ 둘 다 성공해야
탐지!

여기서 짚고 넘어갈 게 있다. 서버는 요청을 한 번 받든 두 번 받든 똑같이 취약하다. 정교해지는 건 서버가 아니라 YAML 템플릿의 탐지 조건 쪽이다.

알아둘 옵션

옵션의미
flow: http(1) && http(2)두 요청 모두 성공해야 탐지
rawHTTP 요청을 직접 작성하는 영역
cookie-reuse: true1차 요청 쿠키를 2차 요청에 자동 재사용
internal: true결과를 외부 출력 안 하고 내부 확인용으로만
req-condition: true각 요청 응답을 status_code_1, status_code_2 로 구분
redirects: true리다이렉트 발생해도 따라감
max-redirects: 3최대 3번까지 리다이렉트 허용

cookie-reuse 는 필요할 때와 아닐 때가 갈린다.

방식cookie-reuse
세션 기반 공격 (페이로드 전송)필요
단순 파일/버전 확인불필요
Path Traversal불필요

필요한 이유는 세션 때문이다.

1번 요청 → 서버가 쿠키(세션) 발급

2번 요청 → 그 쿠키로 세션 읽어야 취약점 트리거

쿠키가 없으면 다른 세션이 되어 페이로드가 실행되지 않는다.

명령어

# 템플릿 문법 검증
nuclei -validate -t 템플릿.yaml
# → All templates validated successfully

# 스캔 실행
nuclei -u http://대상주소 -t 템플릿.yaml
# → 1 matches found

# 템플릿 업데이트
nuclei -update-templates

# 실제 요청/응답 확인
nuclei -u http://대상 -t 템플릿.yaml -debug
옵션의미
-u단일 대상 지정
-l대상 목록 파일 (여러 개)
-t사용할 템플릿
-validate문법 검증
-debug상세 요청/응답 출력
-disable-update-check업데이트 체크 끄기

1 matches found 는 YAML 템플릿의 조건을 만족하는 결과를 1개 찾았다는 뜻, 즉 탐지 성공이다.

실습한 CVE 두 개

CVE-2021-43798 (Grafana Path Traversal) — 단일 요청

URL 경로 조작으로 시스템 파일 읽기
/public/plugins/alertlist/../../../../etc/passwd

응답에 root:x:0:0 있으면 탐지

단순한 텍스트 비교라 Nuclei로 쉽게 잡힌다. curl로 확인할 때는 --path-as-is 옵션을 줘야 ../ 가 정규화되지 않고 그대로 전달된다.

CVE-2015-8562 (Joomla RCE) — 다중 요청

User-Agent에 PHP 객체 주입 → 역직렬화 → RCE

1번 요청: Mozilla/5.0으로 쿠키 받기
2번 요청: 쿠키 재사용 + 페이로드 전송

응답에 "PHP Version" 있으면 phpinfo() 실행됨 = 탐지

유형별 탐지 난이도

유형예시난이도
Path TraversalCVE-2021-43798쉬움
버전 노출헤더/바디 확인쉬움
SQL Injection에러 메시지 확인보통
PHP Object InjectionCVE-2015-8562어려움 (null byte 문제)
Blind RCE응답에 증거 없음매우 어려움

응답에 증거가 남지 않을수록 탐지가 어려워진다. 이게 템플릿 설계의 본질적인 한계다.

CVE-2015-8562 는 왜 이렇게 도는가

1. User-Agent에 PHP 직렬화 객체 담아 전송
2. Joomla가 검증 없이 세션에 저장
3. 세션 읽을 때 unserialize() 실행 (역직렬화)
4. PHP Object Injection 발생
5. __destruct() 자동 실행
6. assert("phpinfo()") 실행
7. "PHP Version" 출력 = RCE 가능성 확인
용어의미
serialize()객체 → 문자열 변환 (직렬화)
unserialize()문자열 → 객체 복원 (역직렬화)
__destruct()객체 소멸 시 자동 실행되는 메서드
assert()문자열을 PHP 코드로 실행하는 함수
phpinfo()PHP 정보 출력 함수 (PoC 증명용)

원인을 정확히 짚으면 역직렬화 자체가 아니라 입력값을 검증 없이 역직렬화하는 것이 문제다.

안전: 검증 → unserialize()
취약: 검증 없이 바로 unserialize()  ← 문제

PHP 직렬화 형식의 기호도 알아두면 페이로드를 읽을 수 있다.

기호의미
OObject (객체)
sString (문자열)
aArray (배열)
iInteger (숫자)
bBoolean

phpinfo() 가 실행되면 항상 “PHP Version”, “PHP Extension” 같은 단어를 포함한 페이지가 출력된다. 그래서 이 단어가 응답에 있으면 코드 실행에 성공한 것으로 판단할 수 있다 — 탐지 조건으로 이걸 쓰는 이유다.