Step 164. 열거(Enumeration) 도구 총정리 — 돌 하나하나 뒤집는 체계

Step 164. 열거(Enumeration) 도구 총정리 — 돌 하나하나 뒤집는 체계

Level 2 — 보안 입문과 공격 스킬 기초 | 난이도 ★★★☆☆ | 예상 소요 시간 3시간

전제: Step 81(nmap), Step 126(linPEAS), Step 145(디렉토리 탐색)를 마쳤다. 파이썬으로 소켓 연결을 열 수 있다.

  • 준비물: 파이썬 3, 리눅스 랩(WSL 또는 Kali), 개인 위키(체크리스트 저장용)
  • ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다.
  • 성격 안내: 오늘은 [프로젝트] 챕터입니다. 새 도구보다 "지금까지의 도구를 상황별로 정리하고, 나만의 열거 스크립트를 완성하는" 것이 목표입니다.

열거(enumeration)는 "돌 하나하나 뒤집기"입니다. 포트, 서비스 버전, 웹 경로, 사용자, 공유 폴더 — 침투 테스트에서 찾아야 할 단서는 전부 열거에서 나옵니다. 문제는 사람이 체크리스트 없이 하면 반드시 빠뜨린다는 것입니다. 그래서 실무자들은 도구를 자동화로 묶고, 자신만의 점검 순서를 문서로 고정합니다.

오늘은 두 가지를 만듭니다. 지금까지 배운 열거 도구를 상황별로 정리한 지도 한 장, 그리고 포트 스캔부터 웹 경로 열거까지 스스로 굴러가는 나만의 열거 스크립트. 스크립트를 직접 만들다 보면 nmap과 gobuster가 내부에서 무슨 일을 하는지 몸으로 이해하게 됩니다 — 그리고 실제로 만들다가 예상 못 한 벽을 하나 만났는데, 그 사고가 오늘 최고의 교훈이 됐습니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • 열거 상황(외부 네트워크 / 웹 / 호스트 내부)에 맞는 도구를 지체 없이 고른다
  • "열거 체크리스트"를 자기 말로 작성하고 순서의 이유를 설명한다
  • 파이썬 소켓과 urllib로 포트 스캔 + 경로 열거 스크립트를 직접 만든다
  • 배너 그랩(banner grab)이 왜 서비스 식별의 첫걸음인지 실측으로 보여 준다
  • 자동화 도구의 결과를 "넓게 모으고, 사람이 깊게 읽는" 워크플로우로 운용한다

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 파이썬 3 (표준 라이브러리만) + 리눅스 쉘 (WSL 또는 Kali)
오늘의 명령 ss -tln, nmap -sV, (랩에서) autorecon
필요한 개념 열거의 3상황 분류, 배너 그랩, connect 스캔, 자동화 vs 수동의 역할 분담
오늘의 산출물 열거 체크리스트 1부 + my_enum.py (직접 만든 열거 스크립트)

2-1. 열거란 무엇인가 — 스캔과의 차이

포트 스캔이 "문이 열려 있나?"라면, 열거는 "문 너머에 무엇이 있나?"입니다. 열린 포트 뒤의 서비스 버전, 웹 서버의 숨은 경로, 시스템의 사용자 목록과 공유 폴더까지 — 공격에 쓸 수 있는 구체적 정보를 채취하는 단계입니다.

Step 81에서 배운 nmap의 -sV(버전 탐지)와 -sC(기본 스크립트)가 사실 열거의 시작이었고, Step 126의 linPEAS는 "호스트 내부 열거"의 자동화였습니다. 오늘은 그 조각들을 한 장의 지도로 합칩니다.

2-2. 상황별 도구 지도

지금까지 배운 도구를 "어디를 열거하느냐"로 정리하면 이렇습니다.

상황 묻는 질문 도구·명령 배운 곳
외부 네트워크 어떤 포트가 열렸나? nmap -sV -sC 대상 Step 81
웹 서비스 숨은 경로가 있나? gobuster dir -u URL -w 사전 Step 145
호스트 내부 (리눅스) 어떤 서비스가 듣고 있나? ss -tlnp Step 34
호스트 내부 (권한) setuid 파일은? find / -perm -4000 Step 97, 126
호스트 내부 (전체) 올라갈 틈이 어디지? linpeas.sh Step 126

읽는 법: 새 대상을 만나면 이 표의 위에서 아래로 내려갑니다. 외부에서 시작해, 발판을 얻으면 내부로. "지금 내가 어느 행에 있지?"를 항상 의식하는 것이 체계적 열거의 출발입니다.

2-3. 왜 자동화하는가, 그리고 왜 사람이 읽는가

사람이 체크리스트 없이 열거하면 반드시 빠뜨립니다. 그래서 AutoRecon 같은 자동화 프레임워크는 nmap, gobuster, enum4linux를 한 번에 돌려 결과를 폴더로 정리해 줍니다. 도구는 넓게 모으는 데 강합니다.

하지만 출력을 읽는 눈은 사람이 길러야 합니다. 자동화가 모아 온 수백 줄에서 "이상한 버전, 비표준 포트, 익명 접근 가능"을 집어내는 것은 도구가 못 합니다. 오늘 스크립트를 직접 만드는 이유가 여기 있습니다 — 자동화의 내부를 아는 사람만이 출력을 제대로 읽습니다.

2-4. 배너 그랩 — 서비스의 자기소개

많은 서비스는 연결만 해도 먼저 인사를 보냅니다. 이 첫 문장을 배너(banner)라 하고, 받아 읽는 행위를 배너 그랩이라 합니다. SSH가 대표적 — 접속하자마자 SSH-2.0-OpenSSH_...라고 자기 정체를 밝힙니다. 배너 한 줄에 운영체제와 소프트웨어 버전이 드러나는 경우가 많아, 열거의 첫 재료입니다.

단, HTTP처럼 "클라이언트가 먼저 말해야 답하는" 서비스는 배너가 없습니다. 오늘 실측에서 이 차이가 선명하게 드러납니다.


3. 따라 하기

3-1. 모사 대상 서버 띄우기

열거 스크립트를 시험할 "약간 숨겨진 것이 있는" 웹 서버를 만듭니다. Flask로 만든 이 서버는 127.0.0.1의 8090 포트에서만 듣습니다 (본 교재는 2026-09-09에 실측했습니다).

입력 (target164.py)

from flask import Flask, abort

app = Flask(__name__)

@app.route("/")
def index():
    return "Welcome to lab-server"

@app.route("/robots.txt")
def robots():
    return "User-agent: *\nDisallow: /backup\n", 200, {"Content-Type": "text/plain"}

@app.route("/admin")
def admin():
    abort(403)          # 존재하지만 금지 — 열거자에게는 정보

@app.route("/backup")
def backup():
    return "db dump (fake)", 200, {"Content-Type": "text/plain"}

@app.route("/api")
def api():
    return '{"status": "ok"}', 200, {"Content-Type": "application/json"}

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8090)

읽는 법: 포인트는 /admin이 404(없음)가 아니라 403(금지)을 돌려준다는 것입니다. "없다"와 "있지만 막혔다"는 열거자에게 완전히 다른 정보입니다. /robots.txtDisallow: /backup도 고전적인 정보 노출 — "보지 마세요"가 곧 지도입니다.

3-2. 나만의 열거 스크립트 v1 — 그리고 예상 못 한 추락

체크리스트의 첫 두 칸, "포트 스캔 → 웹 경로 열거"를 코드로 옮깁니다.

입력 (my_enum.py v1 — 핵심 부분)

import socket
import urllib.request
import urllib.error

TARGET = "127.0.0.1"
PORTS = [21, 22, 25, 53, 80, 443, 445, 3000, 3306, 5000, 8000, 8080, 8090]
PATHS = ["admin", "login", "backup", "robots.txt", ".git",
         "config", "api", "test", "uploads", "secret"]

def scan_ports(host, ports, timeout=0.5):
    open_ports = []
    for p in ports:
        s = socket.socket()
        s.settimeout(timeout)
        if s.connect_ex((host, p)) == 0:   # 0 = 연결 성공
            open_ports.append(p)
        s.close()
    return open_ports

def probe_http(host, port, paths):
    found = []
    for path in paths:
        url = f"http://{host}:{port}/{path}"
        try:
            with urllib.request.urlopen(url, timeout=2) as r:
                found.append((url, r.status))
        except urllib.error.HTTPError as e:
            if e.code in (401, 403):
                found.append((url, e.code))
        except urllib.error.URLError:
            return None
    return found

실행 (터미널 1에서 python target164.py를 띄운 뒤, 터미널 2에서)

python my_enum.py

출력 (2026-09-09 실측):

=== 1단계: 포트 스캔 (127.0.0.1, 13개 포트) ===
[+] 열림: 22/tcp
[+] 열림: 445/tcp
[+] 열림: 8090/tcp

=== 2단계: 웹 경로 열거 (10개 경로) ===
Traceback (most recent call last):
  ...
http.client.BadStatusLine: SSH-2.0-OpenSSH_for_Windows_9.5

읽는 법: 스크립트가 22번 포트에서 죽었습니다. 원인이 흥미롭습니다 — 열린 포트 전부에 HTTP를 시도했는데, 22번은 SSH였고, SSH는 연결하자마자 먼저 SSH-2.0-OpenSSH_for_Windows_9.5라는 배너를 보냈습니다. urllib는 그걸 HTTP 응답으로 해석하려다 실패한 것입니다.

그런데 뒤집어 보면, 에러 메시지가 서비스의 정체를 알려 줬습니다. 이 "사고"가 바로 배너 그랩의 발명 순간입니다.

3-3. v1.1 — 배너 그랩 단계 추가

실패를 기능으로 바꿉니다. 경로 열거 전에, 열린 포트에 잠깐 접속해 먼저 오는 인사말을 받는 단계를 넣습니다.

입력 (v1.1에 추가할 함수)

def grab_banner(host, port, timeout=1.0):
    """서비스가 먼저 인사하는 포트에서 배너를 받는다."""
    try:
        s = socket.socket()
        s.settimeout(timeout)
        s.connect((host, port))
        data = s.recv(64)
        s.close()
        return data.decode(errors="replace").strip()
    except (socket.timeout, OSError):
        return ""    # 인사 안 하는 서비스 (HTTP 등)

그리고 probe_httpexcept urllib.error.URLErrorexcept Exception으로 넓혀, HTTP가 아닌 포트는 조용히 건너뛰게 합니다.

출력 (2026-09-09 실측):

=== 1단계: 포트 스캔 (127.0.0.1, 13개 포트) ===
[+] 열림: 22/tcp
[+] 열림: 445/tcp
[+] 열림: 8090/tcp

=== 2단계: 배너 그랩 ===
[22/tcp] SSH-2.0-OpenSSH_for_Windows_9.5
[445/tcp] (배너 없음 — 먼저 말 걸어야 함)
[8090/tcp] (배너 없음 — 먼저 말 걸어야 함)

=== 3단계: 웹 경로 열거 (10개 경로) ===
[22/tcp] HTTP 아님 — 건너뜀
[445/tcp] HTTP 아님 — 건너뜀
[+] 403 http://127.0.0.1:8090/admin
[+] 200 http://127.0.0.1:8090/backup
[+] 200 http://127.0.0.1:8090/robots.txt
[+] 200 http://127.0.0.1:8090/api

=== 열거 완료 ===

읽는 법: 세 가지를 기록하세요. ① 22번에서 배너 하나로 "OpenSSH 9.5, 윈도우"까지 식별됐습니다. ② 445(SMB)와 8090(HTTP)은 먼저 인사하지 않습니다 — 프로토콜마다 성격이 다릅니다. ③ 경로 열거가 403과 200을 구분해 /admin(존재하지만 금지)과 /backup(노출)을 찾아냈습니다. 13개 포트, 10개 경로의 작은 스크립트가 체크리스트 세 칸을 스스로 수행했습니다.

3-4. 리눅스 내부 열거 — ss와 nmap -sV의 실제 출력

침투 후 발판을 얻으면 "이 상자 안에서 뭐가 돌고 있지?"가 다음 질문입니다. WSL(Ubuntu 24.04)에서 실측한 출력입니다.

입력

ss -tln

출력 (2026-09-09 실측):

State  Recv-Q Send-Q  Local Address:Port  Peer Address:PortProcess
LISTEN 0      4096    127.0.0.53%lo:53         0.0.0.0:*
LISTEN 0      1000   10.255.255.254:53         0.0.0.0:*
LISTEN 0      4096       127.0.0.54:53         0.0.0.0:*
LISTEN 0      4096        127.0.0.1:45381      0.0.0.0:*

읽는 법: 127.0.0.1에 묶인 서비스는 외부에서 못 보지만 내부 열거에는 보입니다 — "외부엔 막혔지만 내부엔 열린 서비스"를 찾는 것이 내부 열거의 핵심입니다.

이어서, 임시 웹 서버를 띄우고 nmap의 버전 탐지를 실측했습니다.

입력

python3 -m http.server 8000 &      # 터미널 1
nmap -sV -p 8000 127.0.0.1         # 터미널 2

출력 (2026-09-09 실측):

PORT     STATE SERVICE VERSION
8000/tcp open  http    SimpleHTTPServer 0.6 (Python 3.12.3)

읽는 법: 우리 스크립트가 "(배너 없음)"이라고만 했던 HTTP 포트에서도, nmap은 프로브를 보내 SimpleHTTPServer 0.6 (Python 3.12.3)까지 알아냅니다. 배너가 없으면 말을 걸어서 답변의 생김새로 식별하는 것 — 이것이 -sV의 내부 동작이고, 우리가 스크립트를 직접 만들었기에 읽을 수 있는 줄입니다.

3-5. 자동화 프레임워크 — AutoRecon (랩 실습, 출력 예시)

Kali↔MS2 랩에서 진행하세요. 출력은 출력 예시입니다.

pipx install git+https://github.com/Tib3rius/AutoRecon.git   # 설치 (또는 Kali 패키지)
sudo autorecon MS2_IP
[*] Scanning target MS2_IP
[!] [nmap-full-tcp] finished ... results/results/MS2_IP/scans/
...

읽는 법: AutoRecon은 전체 포트 스캔 → 발견된 서비스별 후속 스캔(웹이면 경로 스캔, SMB면 공유 열거)을 자동으로 이어 주고, 결과를 results/대상/scans/ 아래에 정리합니다. 공격적 스캔이므로 랩 전용입니다. 끝나면 3-6으로 넘어가세요.

3-6. 자동화 vs 내 손 — 비교표가 오늘의 산출물

AutoRecon 결과 폴더와 3-3 스크립트의 결과, 그리고 2-2의 체크리스트를 나란히 놓고 표로 비교합니다.

항목 자동화가 찾은 것 내가 찾은 것 비고
열린 포트 〇 (전수) 〇 (흔한 것만) 도구가 넓다
서비스 버전 △ (배너 있는 것만) -sV의 프로브는 강력
숨은 경로 〇 (큰 사전) △ (내 사전 크기만큼) 사전의 질이 승부
우선순위 판단 × "뭘 먼저 볼까"는 사람의 일

읽는 법: 마지막 행이 핵심입니다. 결과가 많을수록 "이상한 버전 → 비표준 포트 → 익명 접근 가능" 순으로 읽는 사람의 눈이 승부를 가릅니다. 도구는 넓게, 사람은 깊게 — 이 분업이 오늘 확정하는 워크플로우입니다.


4. 미션과 연습문제

미션 — 나만의 열거 스크립트와 체크리스트 완성

  1. 3-2~3-3의 스크립트를 완성해 내 대상 서버(3-1)에 대해 실행하고, 배너 그랩 결과를 포함한 출력 전문을 기록한다
  2. 스크립트에 "결과를 파일로 저장"하는 기능을 추가한다 (예: my_enum.py > enum_결과.txt가 아니라 코드 안에서 저장)
  3. 2-2의 도구 지도를 내 말로 다시 쓴 "내 열거 체크리스트"를 완성한다 — 최소 외부/웹/내부 3단계, 각 단계에 명령과 "언제 쓰는지" 포함
  4. (랩) AutoRecon으로 MS2를 스캔하고, 결과에서 "내가 놓쳤을 것" 한 가지를 찾아 기록한다
  5. 체크리스트 맨 아래에 "결과를 읽는 우선순위" 세 줄을 적는다

연습문제

문제 1. 포트 스캔과 열거의 차이를 "문"의 비유 없이 정의해 보세요.

문제 2. 3-2에서 v1 스크립트가 죽은 원인과, 그 사고가 왜 "배너 그랩"이라는 정식 기법의 발견이 됐는지 설명해 보세요.

문제 3. /admin이 404가 아니라 403을 돌려줄 때, 열거자가 얻는 정보는 무엇입니까?

문제 4. 자동화 도구를 쓰면서도 "내 체크리스트"를 별도로 유지해야 하는 이유를, 3-6의 비교표 근거로 설명해 보세요.


5. 모범 답안과 완료 기준

미션 모범 답안

체크리스트 골격 예시:

### 내 열거 체크리스트 v1
1. 외부: nmap -sV -sC 대상 → 열린 포트와 버전
   - 언제: 새 대상의 첫 5분
2. 웹 발견 시: 경로 사전 열거 (gobuster 류 / 내 스크립트)
   - 언제: 80, 443, 8000번대가 보이면 즉시
3. 내부 침투 후: ss -tlnp, find / -perm -4000, linpeas.sh
   - 언제: 셸을 얻은 직후
### 결과 읽는 우선순위
1. 이상한/오래된 버전 → 2. 비표준 포트 → 3. 익명 접근 가능 여부

검증하는 법: ① 스크립트가 배너 그랩을 포함해 끝까지(에러 없이) 도는가. ② 403과 404를 구분해 기록하는가. ③ 체크리스트 각 행에 "언제 쓰는지"가 있는가 — 이 칸이 없는 체크리스트는 명령어 모음일 뿐입니다. ④ 결과 저장 기능이 동작하는가.

연습문제 해답

문제 1 해답. 포트 스캔은 "어떤 포트가 연결을 받는가"를 확인하는 것이고, 열거는 열린 포트 너머의 구체 정보(서비스 버전, 경로, 사용자, 공유)를 채취하는 것입니다. 스캔은 목록을 만들고, 열거는 각 항목을 해부합니다.

문제 2 해답. 열린 포트 전부에 HTTP 요청을 보냈는데, SSH 포트가 먼저 보낸 배너를 HTTP 응답으로 해석하려다 BadStatusLine이 났습니다. 하지만 그 에러 메시지 자체가 서비스 정체였으므로, "연결 후 먼저 오는 데이터를 읽는다"는 정식 절차(배너 그랩)로 뒤집으면 실패가 식별 기능이 됩니다.

문제 3 해답. "경로가 존재한다"는 사실 자체입니다. 404는 없음, 403은 "있지만 막힘" — 공격자는 존재하는 관리 페이지를 목표로 삼을 수 있고, 인증 우회나 다른 경로로의 접근을 시도할 후보가 생깁니다.

문제 4 해답. 도구는 수집은 넓게 하지만 우선순위 판단을 못 합니다(3-6 표의 마지막 행). 그리고 도구가 없는 환경, 도구가 실패한 환경에서 움직일 수 있는 것은 체크리스트가 머리에 있는 사람뿐입니다. 체크리스트는 도구의 대체재가 아니라, 도구 출력을 읽는 절차서입니다.

완료 기준 체크리스트

  • [ ] 대상 서버와 열거 스크립트를 직접 만들어 실행했다
  • [ ] v1의 BadStatusLine 사고를 재현하고 v1.1로 고쳤다
  • [ ] 배너 그랩으로 SSH 배너를 실제로 받아 봤다
  • [ ] 403과 404의 정보 차이를 설명할 수 있다
  • [ ] "언제 쓰는지"가 붙은 내 열거 체크리스트를 완성했다
  • [ ] (랩) AutoRecon 결과와 수동 결과를 비교했다
  • [ ] 결과 읽는 우선순위 세 줄을 내 말로 적었다

6. 흔한 실수와 해결

벽 1. 스크립트가 중간에 죽는다 — http.client.BadStatusLine: SSH-2.0-...

증상: 경로 열거 도중 트레이스백과 함께 멈춥니다.
원인: 열린 포트 전부를 HTTP로 간주했습니다. SSH·FTP 등은 자기 프로토콜로 먼저 말을 겁니다.
해결: 3-3처럼 예외를 Exception까지 넓혀 "HTTP 아님 → 건너뜀"으로 처리하고, 그 포트는 배너 그랩으로 식별하세요. 에러가 곧 정보입니다.

벽 2. 배너가 전부 빈칸이다

증상: grab_banner가 전부 ""를 돌려줍니다.
원인: 정상일 수 있습니다 — HTTP, SMB 등은 클라이언트가 먼저 말해야 답합니다. timeout 안에 아무 데이터도 없으면 빈칸이 맞습니다.
해결: 빈칸이면 "수동형 서비스"로 기록하고, nmap -sV처럼 프로브를 보내는 단계로 넘어가세요. 배너 없음도 프로토콜 식별의 단서입니다.

벽 3. 포트 스캔이 한없이 느리다

증상: 13개 포트에 수십 초가 걸립니다.
원인: 닫힌 포트는 timeout(기본 0.5초)만큼 기다립니다. 포트 수 × timeout이 총시간입니다.
해결: 내 랩 실습에서는 정상입니다. 범위를 늘릴 때는 timeout을 줄이거나 스레드를 쓰세요 — 단, 랩 밖에서 대량 스캔은 그 자체로 탐지·차단 대상입니다.

벽 4. 결과가 너무 많아서 뭘 먼저 볼지 모르겠다

증상: AutoRecon 결과 폴더에 파일이 수십 개입니다.
원인: 읽는 순서를 정하지 않았습니다.
해결: 고정 순서를 쓰세요 — ① 이상하거나 오래된 버전, ② 비표준 포트(웹이 8080에? SSH가 2222에?), ③ 익명 접근 가능 여부(FTP anonymous, SMB 게스트). 이 세 줄을 체크리스트에 박아 두는 것이 오늘 미션 5번입니다.

벽 5. AutoRecon을 실제 네트워크에서 돌리고 싶어진다

증상: 회사·학교 네트워크에서 "테스트"로 돌려 보고 싶습니다.
원인: 도구의 편리함이 경계감각을 누릅니다.
해결: 안 됩니다. AutoRecon은 전 포트 스캔 + 공격적 스크립트를 쏟아붓는 도구로, 허가 없는 실행은 명백한 불법이고 IDS가 즉시 탐지합니다. 랩(MS2, 취약 VM)에서만 돌리세요.


7. 정리

오늘의 개념

개념 한 줄 설명
열거(enumeration) 열린 문 너머의 구체 정보(버전·경로·사용자)를 채취하는 단계
배너 그랩 연결 직후 서비스가 먼저 보내는 인사말로 정체를 식별
403 vs 404 "있지만 금지" vs "없음" — 403도 열거자에게는 정보
자동화 프레임워크 nmap+경로스캔+서비스 열거를 한 번에 돌려 정리 (AutoRecon 등)
넓게·깊게 분업 수집은 도구, 우선순위 판독은 사람

오늘의 명령어

명령 하는 일
ss -tln 이 호스트가 듣고 있는 TCP 포트 보기 (내부 열거)
nmap -sV -p 포트 대상 프로브를 보내 서비스 버전 식별
find / -perm -4000 setuid 파일 열거 (권한 상승 재료)
sudo autorecon 대상 랩 전용 열거 자동화
socket().connect_ex() (파이썬) 내 스크립트의 포트 스캔 심장

명령어보다 중요한 감각

오늘의 진짜 수확은 스크립트가 아니라 에러 하나를 기법으로 뒤집은 경험입니다. BadStatusLine은 실패가 아니라 서비스의 자기소개였습니다. 열거의 세계에서는 이 뒤집기가 일상입니다 — 403은 거절이자 지도이고, robots.txt는 금지 목록이자 보물지도이고, 빈 배너는 침묵이자 프로토콜의 성격 증명입니다.

그리고 도구가 아무리 좋아져도 바뀌지 않는 사실: 모으는 것은 기계가 더 잘하고, "이 수백 줄 중 어디가 이상한가"를 아는 것은 체크리스트를 가진 사람입니다. 오늘 만든 체크리스트는 Step 105의 기법 분류표처럼, 앞으로의 모든 침투 실습에서 첫 페이지로 펼쳐질 문서입니다. 갱신 규칙을 함께 적어 두세요.


전부 체크되면 Step 164 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.