네트워크
Step 82. 프로젝트 — 네트워크 지도와 위험도 평가
Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★★★☆ | 예상 소요 시간 4시간
전제: Step 81을 마쳤다. nmap으로
-sV스캔과-oN파일 저장을 할 수 있다. 파이썬 기본 문법(파일 읽기, 정규표현식, 리스트)을 안다.
- 준비물: 리눅스 터미널, nmap, 파이썬 3. 그리고 Step 81에서 저장한 스캔 리포트 파일.
- 주의: ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다. 이 챕터의 모든 실측은
127.0.0.1스캔 파일만을 대상으로 수행했습니다.
공격자가 네트워크에 들어와 제일 먼저 하는 일이 무엇인지 아십니까? “뭐가 있는지 지도를 그리는 것”입니다. 방어자의 첫 일도 정확히 같습니다 — 모르는 것은 지킬 수 없기 때문입니다. 오늘은 Step 81의 스캔 결과를 재료로, “발견 → 정리 → 우선순위 → 행동”이라는 보안 진단의 전체 뼈대를 한 번 돌려 봅니다. 스캔까지가 nmap의 일이라면, 그 결과를 위험도 표로 바꾸는 것은 여러분의 파이썬 실력이 할 일입니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 자산·공격 표면·위험을 정의합니다.
- 호스트와 원본 근거를 유지하며 Nmap 출력을 파싱합니다.
- 서비스별 검토 질문을 만들고 위험도는 UNASSESSED로 둡니다.
- 부족한 근거를 조사하고 담당자·다음 조치를 기록합니다.
- 명시적으로 허가한 실습 대상의 검토표를 만듭니다.
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 리눅스 터미널 + 파이썬 3 (스크립트 파일) |
| 오늘의 명령·기능 | Nmap 근거, 호스트를 보존하는 파싱, 딕셔너리, 검토 질문 |
| 필요한 개념 | 자산, 공격 표면, 근거, 불확실성, 가능성과 영향 |
| 오늘의 산출물 | riskmap.py — 스캔 파일을 위험도 표로 바꾸는 변환기 + 완성된 평가표 한 장 |
2-1. 자산, 공격 표면, 위험도
오늘 쓸 세 용어를 정리합니다.
- 자산은 정보·장비·서비스 같은 가치 있는 자원이며 자산 목록은 그것들을 나열합니다.
- 공격 표면은 노출된 인터페이스·상호작용이며 열린 포트는 일부입니다.
- 위험은 노출·보호조치 등의 맥락에서 가능성과 영향을 고려합니다. 서비스 이름만으로 등급을 정할 수 없습니다.
2-2. 왜 파싱인가 — 파일이 곧 데이터다
Step 81의 스캔 출력은 사람이 읽기 좋은 표이지만, 사실 규칙적인 텍스트입니다. 규칙적인 텍스트는 프로그램이 읽을 수 있습니다. 스캔 파일을 파이썬으로 파싱하면 이런 것이 가능해집니다.
- 각 호스트를 유지하며 일관되게 파싱합니다.
- 범위·방식이 같은 관찰끼리 비교합니다.
- 재현 가능한 검토 질문을 만들고 사람의 평가는 별도로 기록합니다.
파서는 여러 호스트의 관찰을 유지할 수 있습니다. 큰 목록에서도 명시적 허가·입력 검증·일관된 검사 조건·사람의 검토가 필요하며 표를 확장한다고 위험 평가가 검증되지는 않습니다.
2-3. 위험도 평가의 사고법
위험도는 감이 아니라 비교입니다. 이런 질문들을 던져 보세요.
- 이 서비스는 암호화되어 오가는가, 평문인가? (telnet/ftp/http는 평문)
- 인증이 걸려 있는가? 기본 암호는 아닌가?
- 서비스 목적과 소유자를 아는가? 부족한 근거는 위험 등급을 정하기 전에 조사해야 합니다.
- 이 서비스가 뚫리면 무엇이 새어 나가는가?
답이 나쁠수록 위험도는 올라갑니다. 정밀한 점수가 아니라, “어디부터 손볼까”의 순서를 정하는 것이 오늘의 목표입니다.
3. 따라 하기
3-1. 재료 준비 — 스캔 파일 다시 보기
Step 81에서 저장한 전수 스캔 파일을 열어 봅니다. 없다면 다시 만듭니다.
입력
nmap -sV --version-intensity 2 -p- -oN scan_full.txt 127.0.0.1
cat scan_full.txt
출력 (2026-09-09 실측, 핵심 부분):
# Nmap 7.94SVN scan initiated Wed Sep 9 13:37:10 2026 as: nmap -sV ... -p- -oN scan_full.txt 127.0.0.1
Nmap scan report for localhost (127.0.0.1)
Host is up (0.0000010s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
8000/tcp open http SimpleHTTPServer 0.6 (Python 3.12.3)
33211/tcp open unknown
뒤따르는 포트 행의 맥락으로 Nmap scan report 호스트 줄을 유지하세요. 호스트를 버리면 서로 다른 자산이 잘못 합쳐집니다.
3-2. 파서 만들기 — 규칙적인 줄만 골라내기
입력 (riskmap.py 1단계)
"""Review worksheet from your own Nmap normal output, not a risk score."""
import argparse
import csv
import re
from pathlib import Path
def parse_nmap(path):
host = None
rows = []
for line in Path(path).read_text(encoding="utf-8").splitlines():
if line.startswith("Nmap scan report for "):
host = line.removeprefix("Nmap scan report for ").strip()
match = re.match(r"^(\d+)/(tcp|udp)\s+(\S+)\s+(\S+)\s*(.*)$",
line.strip())
if match:
if host is None:
raise ValueError("port row has no host identity")
rows.append({"host": host, "port": int(match[1]),
"proto": match[2], "state": match[3],
"service": match[4], "version": match[5].strip()})
return rows
def assess(service):
questions = {
"ftp": "Check transport protection, authentication and exposure",
"telnet": "Check plaintext exposure and replacement options",
"http": "Check data sensitivity, TLS termination and authorization",
"http-alt": "Confirm service identity and transport protection",
"ssh": "Check identity, keys, authentication, patching and exposure",
}
return "UNASSESSED", questions.get(service, "Identify service and owner")
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("files", nargs="+")
parser.add_argument("--csv", type=Path)
args = parser.parse_args(argv)
report = []
for path in args.files:
for row in parse_nmap(path):
grade, question = assess(row["service"])
row.update(source=path, risk=grade, action=question)
report.append(row)
print(row["host"], f'{row["port"]}/{row["proto"]}',
row["state"], row["service"], grade, question, sep=" | ")
print("Rows requiring evidence review:", len(report))
if args.csv:
fields = ["source", "host", "port", "proto", "state", "service",
"version", "risk", "action"]
with args.csv.open("x", newline="", encoding="utf-8") as fp:
writer = csv.DictWriter(fp, fieldnames=fields)
writer.writeheader()
writer.writerows(report)
if __name__ == "__main__":
main()
출력 (2026-09-09 실측):
{'port': 8000, 'proto': 'tcp', 'state': 'open', 'service': 'http', 'version': 'SimpleHTTPServer 0.6 (Python 3.12.3)'}
{'port': 33211, 'proto': 'tcp', 'state': 'open', 'service': 'unknown', 'version': ''}
정규식은 포트 필드를 추출하고 파서는 현재 호스트를 별도로 유지합니다. 이 학습용 파서는 제시한 일반 출력 형식을 처리합니다. 일반적인 기계 처리에는 Nmap XML이 더 적합합니다.
왜: “문서를 데이터로 바꾸는 것”이 모든 자동화의 첫걸음입니다. Step 48의 정규표현식이 여기서 실전 데뷔합니다.
3-3. 검토 질문 선택하기
입력 (riskmap.py 완성형)
"""Review worksheet from your own Nmap normal output, not a risk score."""
import argparse
import csv
import re
from pathlib import Path
def parse_nmap(path):
host = None
rows = []
for line in Path(path).read_text(encoding="utf-8").splitlines():
if line.startswith("Nmap scan report for "):
host = line.removeprefix("Nmap scan report for ").strip()
match = re.match(r"^(\d+)/(tcp|udp)\s+(\S+)\s+(\S+)\s*(.*)$",
line.strip())
if match:
if host is None:
raise ValueError("port row has no host identity")
rows.append({"host": host, "port": int(match[1]),
"proto": match[2], "state": match[3],
"service": match[4], "version": match[5].strip()})
return rows
def assess(service):
questions = {
"ftp": "Check transport protection, authentication and exposure",
"telnet": "Check plaintext exposure and replacement options",
"http": "Check data sensitivity, TLS termination and authorization",
"http-alt": "Confirm service identity and transport protection",
"ssh": "Check identity, keys, authentication, patching and exposure",
}
return "UNASSESSED", questions.get(service, "Identify service and owner")
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("files", nargs="+")
parser.add_argument("--csv", type=Path)
args = parser.parse_args(argv)
report = []
for path in args.files:
for row in parse_nmap(path):
grade, question = assess(row["service"])
row.update(source=path, risk=grade, action=question)
report.append(row)
print(row["host"], f'{row["port"]}/{row["proto"]}',
row["state"], row["service"], grade, question, sep=" | ")
print("Rows requiring evidence review:", len(report))
if args.csv:
fields = ["source", "host", "port", "proto", "state", "service",
"version", "risk", "action"]
with args.csv.open("x", newline="", encoding="utf-8") as fp:
writer = csv.DictWriter(fp, fieldnames=fields)
writer.writeheader()
writer.writerows(report)
if __name__ == "__main__":
main()
교정 후 예시 파일의 예상 출력 형식:
localhost (127.0.0.1) | 8000/tcp | open | http | UNASSESSED | Review transport, data and access controls localhost (127.0.0.1) | 33211/tcp | open | unknown | UNASSESSED | Identify service and owner Rows requiring evidence review: 2
노출·보호조치·영향을 검토하기 전에는 모든 행을 UNASSESSED로 유지합니다. 서비스 이름은 최종 등급이 아니라 질문을 선택합니다. 미확인 서비스는 정체·관리자 조사 대상으로 둡니다.
재현 가능한 파싱은 증거 보존에 도움이 됩니다. 재현 가능한 규칙이라고 타당한 위험 모델이 되는 것은 아니므로 사람이 평가한 근거와 판단 과정을 함께 기록하세요.
3-4. 예측해 보기 — unknown을 어떻게 처리해야 할까
예측입니다. unknown 서비스에 위험도를 매긴다면 ① 하(알 수 없으니 낮게) ② 중 ③ 상(모르니 높게) ④ 매기지 않고 조사 목록으로 분리 — 어느 쪽이 맞을까요?
직접 확인: 우리 코드는 ④를 선택했습니다. 그리고 Step 81 3-7절에서 이 포트의 정체를 추적해 봤습니다 — -sV 지문에서 HTTP/1.0 404 Not Found 응답을 확인하고, ss -tlnp로 컨테이너 관리 프로그램이 연 포트임을 최종 확인했죠 (2026-09-09 실측).
읽는 법: “모른다”를 등급으로 얼버무리지 않고 조사 행동으로 연결하는 것이 ④의 힘입니다. 위험도 표의 ?는 빈칸이 아니라 할 일입니다.
왜: 실제 침해 사고는 대개 “누가 열었는지 아무도 모르는 서비스”에서 시작됩니다. 모르는 것을 모른다고 기록하고 끝까지 쫓는 습관이 지도의 정확도를 만듭니다.
3-5. 지도 완성 — 표를 문서로
마지막으로 파이썬의 출력을 사람이 읽는 평가표로 옮깁니다. 스프레드시트나 마크다운에 이런 형식으로요.
| 자산 | 포트 | 관찰 서비스 | 위험도 | 다음 조치 |
|---|---|---|---|---|
| 소유한 루프백 실습 | 8000/tcp | HTTP 응답 | 미평가 | 목적·수신 범위 확인 후 실습 종료 시 중지 |
| 과거 예시 호스트 | 33211/tcp | 식별되지 않은 응답 | 미평가 | 등급 전 관리자·보호조치 확인 |
서비스 정체 확인은 불확실성 하나를 해소할 뿐 자동으로 LOW가 되지 않습니다. 노출·보호조치·가능성·영향을 검토하고 판단 근거를 기록하기 전에는 UNASSESSED로 유지하세요.
왜: 발견(스캔) → 정리(표) → 우선순위(위험도) → 행동(조치)의 흐름은 기업의 보안 점검과 정확히 같은 뼈대입니다. 오늘의 한 장이 그 축소판입니다.
4. 미션과 연습문제
미션 — 우리 집 네트워크 지도 (동의를 구한 뒤)
준비가 된 분만, 가족의 양해를 구하고 진행하세요. 랩만 있다면 랩 네트워크로 해도 됩니다.
- 검사하도록 허가받은 소유 실습 주소·포트를 정확히 나열하세요.
- 합의한 방식으로 대상만 검사하고 원본 파일을 저장하세요.
- riskmap.py에 파일을 전달하고 필요하면 새 –csv 파일명을 정하세요.
- 각 행에 호스트·포트·상태·출처·검토 질문·UNASSESSED 위험도를 유지하세요.
- 서비스 정체·노출·보호조치·영향을 조사하고 담당자·다음 조치를 기록하세요.
연습문제
문제 1. 자산, 공격 표면, 위험도의 관계를 한 문장씩으로 설명해 보세요.
문제 2. riskmap.py의 정규표현식이 Not shown: 65533 closed tcp ports (reset) 같은 줄을 건너뛰는 이유를 패턴의 관점에서 설명해 보세요.
문제 3. 위험도 규칙에서 unknown에 ?를 주고 조사 목록으로 분리하는 설계가 하를 주는 설계보다 나은 이유는 무엇인가요?
문제 4. 같은 네트워크를 한 달 뒤 다시 스캔했습니다. riskmap.py에 어떤 기능을 추가하면 “지난달과 비교해 새로 열린 포트”를 자동으로 찾을 수 있을까요? 코드 없이 아이디어로 설명해 보세요.
해설과 완료 기준 · 펼치기/접기
5. 모범 답안과 완료 기준
미션 모범 답안
위의 완성 riskmap.py는 여러 입력 파일과 선택적 –csv report.csv를 지원합니다. 기존 보고서를 보존하도록 새 출력 파일명을 사용하세요.
"""Review worksheet from your own Nmap normal output, not a risk score."""
import argparse
import csv
import re
from pathlib import Path
def parse_nmap(path):
host = None
rows = []
for line in Path(path).read_text(encoding="utf-8").splitlines():
if line.startswith("Nmap scan report for "):
host = line.removeprefix("Nmap scan report for ").strip()
match = re.match(r"^(\d+)/(tcp|udp)\s+(\S+)\s+(\S+)\s*(.*)$",
line.strip())
if match:
if host is None:
raise ValueError("port row has no host identity")
rows.append({"host": host, "port": int(match[1]),
"proto": match[2], "state": match[3],
"service": match[4], "version": match[5].strip()})
return rows
def assess(service):
questions = {
"ftp": "Check transport protection, authentication and exposure",
"telnet": "Check plaintext exposure and replacement options",
"http": "Check data sensitivity, TLS termination and authorization",
"http-alt": "Confirm service identity and transport protection",
"ssh": "Check identity, keys, authentication, patching and exposure",
}
return "UNASSESSED", questions.get(service, "Identify service and owner")
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("files", nargs="+")
parser.add_argument("--csv", type=Path)
args = parser.parse_args(argv)
report = []
for path in args.files:
for row in parse_nmap(path):
grade, question = assess(row["service"])
row.update(source=path, risk=grade, action=question)
report.append(row)
print(row["host"], f'{row["port"]}/{row["proto"]}',
row["state"], row["service"], grade, question, sep=" | ")
print("Rows requiring evidence review:", len(report))
if args.csv:
fields = ["source", "host", "port", "proto", "state", "service",
"version", "risk", "action"]
with args.csv.open("x", newline="", encoding="utf-8") as fp:
writer = csv.DictWriter(fp, fieldnames=fields)
writer.writeheader()
writer.writerows(report)
if __name__ == "__main__":
main()
예시 원본은 8000과 33211을 호스트가 포함된 별도 행으로 유지하고 위험도는 모두 UNASSESSED로 둡니다. 과거 포트가 현재도 존재한다고 기대하지 말고 자신의 환경을 확인하세요.
호스트, 프로토콜, 포트, 상태, 원본 파일명, 불확실성, 조사 조치를 확인하세요. 실습에 명시적으로 허가된 주소·포트만 검사합니다.
연습문제 해답
답 1: 자산은 가치 있는 자원이고 목록은 그것을 나열합니다. 공격 표면은 노출된 인터페이스·상호작용입니다. 위험은 노출·보호조치와 함께 가능성·영향을 고려합니다. 이 표는 관찰과 검토 조치를 정리하며 위험 등급을 자동 검증하지 않습니다.
문제 2 해답. 패턴이 ^(\d+)/(tcp|udp)로 시작하기 때문입니다. Not shown:은 숫자로 시작하지 않고, 8000/tcp 형태의 “숫자/규약” 머리가 없으므로 첫 글자에서 매치가 실패합니다. 헤더의 # 줄들도 마찬가지입니다.
답 3: 미확인 서비스에는 임의의 LOW·HIGH 대신 조사가 필요합니다. 여기서는 근거 있는 평가에 필요한 증거가 모일 때까지 모든 행을 UNASSESSED로 유지합니다. 정체 확인만으로 충분하지 않습니다.
동일한 범위·방식에서 (호스트, 프로토콜, 포트)를 키로 관찰을 비교하세요. 새 응답이 새 물리 장비를 뜻하거나 응답 부재가 서비스 종료를 뜻하지는 않습니다. 서비스 이름은 별도 증거로 유지하세요.
완료 기준 체크리스트
- [ ] 자산·공격 표면·위험을 구분한다.
- [ ] 파서가 호스트·출처를 보존한다.
- [ ] 위험 등급을 지어내지 않고 검토 질문을 만든다.
- [ ] 미확인 근거에 조사 조치·담당자가 있다.
- [ ] 같은 검사 조건에서 비교한다.
- [ ] 명시적으로 허가한 대상·포트만 검사했다.
6. 흔한 실수와 해결
벽 1. 파싱 결과가 텅 비어 있어요
증상: rows가 빈 리스트로 나옵니다.
원인: 파일 경로가 틀렸거나, -oN이 아닌 화면 출력을 그대로 복사해 저장한 경우 줄 모양이 미묘하게 다를 수 있습니다.
해결: cat scan_full.txt로 파일 내용을 눈으로 확인하고, 정규표현식을 한 단계씩 줄이며(^(\d+)부터) 어디서 안 맞는지 찾으세요. 디버깅도 정찰입니다.
벽 2. 한글이 들어간 출력이 깨져요
증상: 표의 한글 열 정렬이 어긋나거나 글자가 깨집니다.
원인: 한글은 화면에서 두 칸을 차지하는데 f-문자열의 폭 맞추기는 글자 수만 셉니다. 저장 인코딩이 cp949인 파일을 UTF-8로 열어도 깨집니다 (Step 50 복습).
해결: 정렬은 예쁘게 안 맞아도 데이터가 맞으면 됩니다. 파일 열기는 encoding="utf-8"을 명시하세요.
벽 3. 위험도를 매기는 기준이 애매해요
증상: 상/중/하를 어디에 둬야 할지 매번 망설여집니다.
서비스 이름만으로 위험도를 확정할 수 없습니다. 노출 범위, 인증·인가, 유지보수 버전, 자산 민감도, 가능한 영향을 기록하고 근거가 없으면 미확인으로 남기세요. 도구는 검토 질문을 정리하며 위험도는 미평가로 둡니다. 의심 항목 수를 검증된 점수 모델로 취급하지 마세요.
벽 4. 집에서 스캔했더니 기기가 예상보다 적게 잡혀요
증상: 분명 TV도 켜져 있는데 목록에 없습니다 (출력 예시 상황).
원인: 절전 중이거나, 그 기기가 핑에 답하지 않거나, 게스트 네트워크처럼 다른 대역에 있습니다.
해결: 기기를 깨우고 재스캔하세요. 공유기 관리 페이지의 접속자 목록과 교차 확인하면 빠진 기기를 찾을 수 있습니다. “안 잡힘”은 “없음”이 아니라 “대답 안 함”입니다.
벽 5. 스캔하다 가족 기기에 경고가 떴어요
증상: 스캔한 기기에서 보안 경고가 울렸습니다.
실습 전에 소유자와 대상·포트·시간·허용 부하를 정하세요. 허가는 범위를 정할 뿐 모든 탐침이 무해함을 보장하지 않습니다. 예상하지 못한 영향이 생기면 멈추고 재검토하세요.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 자산(asset) | 가치 있는 자원이며 자산 목록은 자원을 나열 |
| 공격 표면 | 노출된 인터페이스·상호작용이며 열린 포트보다 넓은 개념 |
| 위험도 | 노출·보호조치·근거와 함께 평가한 가능성과 영향 |
?의 의미 |
부족한 근거는 명시적인 조사가 필요하며 위험도는 UNASSESSED로 유지 |
| 재현 가능한 판단 | 반복 가능한 파싱·검토 질문은 근거 있는 평가를 돕지만 대신하지 않음 |
오늘의 명령어와 코드
| 명령·코드 | 하는 일 |
|---|---|
nmap -sV -p- -oN 파일 대상 |
재료(스캔 리포트) 수집 |
re.match(r"^(\d+)/(tcp|udp)\s+(\S+)\s+(\S+)\s*(.*)$", 줄) |
스캔 줄에서 포트·서비스 추출 |
| 딕셔너리 리스트 + 규칙 리스트 | 발견을 위험도 표로 변환 |
sys.argv[1:] |
여러 스캔 파일을 한 번에 처리 |
| 집합 차집합(이번 달 − 지난 달) | “새로 열린 포트” 변화 감시 (미션 확장) |
명령어보다 중요한 감각
유용한 검토는 관찰 사실·남은 불확실성·다음 조사 담당자를 기록합니다. 물음표가 줄었다는 사실만으로 위험 감소가 입증되지는 않으며 근거의 품질과 완료된 조치를 평가하세요.
마지막으로, 이 지도는 양면입니다. 같은 지도가 침입자에게는 약점 목록이 되므로, 지도 파일 자체도 보호 대상입니다. 그리고 그릴 권한이 없는 네트워크의 지도는 처음부터 그리지 않는 것 — 그것이 이 기술의 사용 면허입니다.
전부 체크되면 Step 82 완료입니다.
한 단계 더 가까이
수업을 마쳤나요?
본문의 완료 기준을 확인한 뒤 직접 표시하세요.