Step 171. OSINT 심화 — 서브도메인과 자산 열거
Level 2 — 보안 입문과 공격 스킬 기초 | 난이도 ★★★☆☆ | 예상 소요 시간 2시간 30분
전제: Step 33(DNS)의 이름 해석 구조, Step 170(OSINT 수집 원칙)을 마쳤다. 파이썬 반복문을 쓸 수 있다.
- 준비물: 파이썬 3, 리눅스 랩(WSL 또는 Kali). 외부 질의 없이 원리 실습이 가능합니다.
- ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다.
- 주의: 서브도메인 열거의 대상도 허가받은 조직뿐입니다. 이 챕터에서는 실제 도메인을 질의하지 않고, 원리는 로컬 시뮬레이션으로, 도구 출력은 예시로 배웁니다.
회사의 서버는 회사도 다 모릅니다. dev.company.com, old.company.com, vpn.company.com — 만들 때는 이유가 있었지만 담당자가 바뀌면 잊힙니다. 그런데 공격자는 잊지 않습니다. 서브도메인 하나하나가 문이고, 방치된 테스트 서버 하나가 침입의 시작점입니다. 오늘은 공격자가 조직의 공격 표면(attack surface)을 그리는 방법을 배우고, 뒤집어 "내 자산 목록이 방어의 출발점"이라는 사실을 확인합니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 수동(passive) 열거와 능동(active) 열거의 차이를 설명한다
- 인증서 투명성 로그(crt.sh)가 왜 서브도메인 목록이 되는지 설명한다
- 사전 대입(wordlist brute-force) 열거의 원리를 파이썬으로 시연한다
도메인 → 서브도메인 → 서비스 → 버전의 자산 목록 구조를 만든다- 열거와 무단 접근의 경계를 명확히 말할 수 있다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 파이썬 3, 리눅스 쉘 (getent, /etc/hosts) |
| 오늘의 도구 | (원리) 사전 대입 열거, (개념 소개) crt.sh · subfinder · httpx |
| 필요한 개념 | DNS 복습(Step 33), 수동/능동 열거, 인증서 투명성 로그, 공격 표면 |
| 오늘의 산출물 | 열거 원리 시뮬레이션 출력 + 자산-목록-구조.md 1부 |
2-1. 수동 열거 vs 능동 열거
서브도메인을 찾는 방법은 두 갈래입니다.
수동 열거(passive enumeration)는 대상에게 한 번도 접속하지 않고, 이미 공개된 기록을 뒤집니다. 검색엔진, DNS 수집 서비스, 그리고 오늘의 주인공인 인증서 투명성 로그가 여기 속합니다. 조용하지만 기록된 것만 찾을 수 있습니다.
능동 열거(active enumeration)는 대상에게 직접 질의합니다. "이 이름 있어?"를 수만 번 묻는 사전 대입이 대표적입니다. 더 많이 찾지만, 대상의 로그에 흔적이 남습니다. 허가받은 대상에게만 사용합니다.
2-2. 인증서 투명성 로그 — 공개된 등록부
HTTPS 인증서는 발급될 때 인증서 투명성(Certificate Transparency) 로그라는 공개 장부에 기록됩니다. 사기 인증서를 잡기 위한 장치인데, 부수 효과가 있습니다 — 누구든 이 장부를 검색해 "이 도메인으로 발급된 인증서 목록"을 볼 수 있습니다. 인증서에는 서브도메인이 적혀 있으므로, 이 장부가 곧 서브도메인 목록이 됩니다. crt.sh는 이 장부의 검색 창구입니다.
2-3. 사전 대입 — 있을 법한 이름을 전부 묻기
능동 열거의 원리는 단순합니다. 흔한 이름 목록(www, mail, dev, vpn, test …)을 도메인 앞에 붙여 DNS에 묻고, 응답이 오는 것만 모읍니다. Step 33에서 배운 이름 해석이 그대로 도구의 심장입니다 — 도구가 특별한 것이 아니라, "단어 목록 × DNS 질의 × 결과 기록"의 반복입니다.
2-4. 공격 표면 — 공격자가 보는 우리 조직
공격 표면(attack surface)은 외부에서 닿을 수 있는 모든 것의 총합입니다. 도메인 밑에 서브도메인, 그 밑에 서비스, 그 안에 버전 — 이 트리가 커질수록 지킬 문이 늘어납니다. 그래서 조직 방어의 첫 과제는 "우리 문이 몇 개인가"를 아는 것이고, 그 목록의 이름이 자산 목록입니다. 공격자의 열거와 방어자의 자산 관리는 같은 행위의 양면입니다.
3. 따라 하기
3-1. 이름 해석 복습 — 가장 작은 DNS
Step 33의 구조를 손끝으로 되살립니다. 리눅스에서 이름이 IP가 되는 첫 단계는 로컬 파일 /etc/hosts입니다.
입력 (리눅스 랩):
cat /etc/hosts
getent hosts localhost
python3 -c "import socket; print(socket.gethostbyname('localhost'))"
출력 (2026-09-09 실측, WSL Ubuntu 24.04):
127.0.0.1 localhost
127.0.1.1 XI3492.localdomain XI3492
...(IPv6 항목들)...
::1 localhost
127.0.0.1
읽는 법: localhost라는 이름이 /etc/hosts에 적혀 있고, getent와 파이썬이 그 규칙대로 주소를 돌려줍니다. 서브도메인 열거란 이 질문을 수만 개의 이름에 대해 반복하는 것입니다 — 질의 방법은 이미 아는 것이고, 오늘 새로운 것은 목록 만들기입니다.
3-2. 사전 대입 시뮬레이션 — 열거의 심장
외부 DNS를 건드리지 않고 원리를 봅니다. 가상의 영역(zone) 데이터에 단어 목록을 대입합니다.
입력: enum_lab.py:
# 가상의 DNS 영역 — 실제라면 여기가 DNS 서버의 대답
zone = {
"www.lab.example": "10.0.0.10",
"mail.lab.example": "10.0.0.25",
"dev.lab.example": "10.0.0.99", # 방치된 개발 서버 가정
"vpn.lab.example": "10.0.0.1",
}
wordlist = ["www", "mail", "ftp", "dev", "blog", "vpn", "test", "shop"]
found = []
for word in wordlist:
name = f"{word}.lab.example"
if name in zone: # 실제라면 여기서 DNS 질의
found.append((name, zone[name]))
print(f"단어 {len(wordlist)}개 시도 → {len(found)}개 발견")
for name, ip in found:
print(f" {name:22s} -> {ip}")
print("관리자가 잊고 있던 후보:", "dev.lab.example")
출력 (2026-09-09 실측):
단어 8개 시도 → 4개 발견
www.lab.example -> 10.0.0.10
mail.lab.example -> 10.0.0.25
dev.lab.example -> 10.0.0.99
vpn.lab.example -> 10.0.0.1
관리자가 잊고 있던 후보: dev.lab.example
읽는 법: 여덟 번 물어 네 개를 찾았습니다. 실제 도구의 단어 목록은 수만~수십만 개입니다. 그리고 마지막 줄을 보세요 — dev.lab.example은 개발할 때 잠깐 쓰고 방치된 서버라는 가정입니다. 공격자가 찾는 것은 바로 이런 "관리자가 잊은 문"입니다. 목록이 없는 이름(blog, test)은 침묵으로 돌아옵니다 — 없으면 안 나온다, 그것이 전부입니다.
왜 하는가: "수만 번의 질의"라고 하면 거창해 보이지만, 뼈대는 이 15줄입니다. 도구의 원리를 알면 도구 출력을 읽는 눈이 달라집니다.
3-3. 없는 이름을 물으면 — 실제 에러의 얼굴
실제 코드에서 없는 이름을 DNS에 물으면 어떤 일이 일어나는지 봅시다.
입력 (리눅스 랩):
python3 -c "import socket; socket.gethostbyname('no-such-host-zzz.invalid')"
출력 (2026-09-09 실측):
socket.gaierror: [Errno -2] Name or service not known
읽는 법: "이름을 알 수 없다"는 오류 — 이것이 사전 대입에서 "없다"의 신호입니다. 열거 도구는 이 오류를 예외가 아니라 데이터로 씁니다. 오류가 나면 목록에서 빼고, 주소가 오면 목록에 넣습니다. 실제 도구를 짤 때는 try/except socket.gaierror로 이 신호를 잡습니다.
3-4. 수동 열거의 창구들 — 화면 예시
실제 조회는 외부 접속이 필요하므로, 두 도구의 출력 모양을 화면 예시로 익힙니다.
crt.sh(인증서 로그 검색)에 %.도메인으로 물었을 때의 모양:
화면 예시 (crt.sh 조회 결과의 모양):
발급 대상(Common Name) 등록 시각
www.example-corp.com 2025-11-02
mail.example-corp.com 2025-11-02
dev-internal.example-corp.com 2026-01-19 ← 내부용으로 만든 흔적
old-shop.example-corp.com 2024-03-30 ← 잊힌 서비스 후보
subfinder(여러 공개 출처를 합치는 도구)의 모양:
화면 예시 (subfinder -d 도메인 -silent 실행 결과의 모양):
www.example-corp.com
mail.example-corp.com
vpn.example-corp.com
dev-internal.example-corp.com
...
[INF] Found 38 subdomains for example-corp.com in 12 seconds
읽는 법: 수동 열거는 대상에 접속하지 않고 이 목록을 얻습니다 — crt.sh는 인증서 장부에서, subfinder는 수십 개 공개 출처를 합쳐서. 목록에서 dev, old, test, internal 같은 접두어가 눈에 띄면 그것이 "살펴볼 문"입니다. 예시의 도메인과 이름은 전부 가공 데이터이며, 실제 조회는 허가받은 대상에 대해서만 합니다.
3-5. 자산 목록 구조 만들기
열거의 결과를 방어자의 문서로 뒤집습니다. 트리 구조로 씁니다.
# 자산 목록 구조 (작성일: ____)
도메인: (대상 도메인)
├── 서브도메인: www — 서비스: 웹(443) — 버전: ____ — 담당: ____
├── 서브도메인: mail — 서비스: SMTP(25) — 버전: ____ — 담당: ____
├── 서브도메인: dev — 서비스: 웹(8080) — 버전: ____ — 담당: 없음 ⚠️
└── 서브도메인: old — 서비스: 웹(80) — 버전: 낡음 ⚠️ — 조치: 폐기 검토
읽는 법: ⚠️가 붙는 칸이 방어의 시작점입니다 — 담당이 없거나, 버전이 낡았거나, 존재를 아무도 기억하지 못하는 자산. 공격자의 열거 목록과 방어자의 자산 목록은 같은 표이고, 방어자는 이 표에 "담당"과 "조치" 칸을 더합니다.
4. 미션과 연습문제
미션 — 자산 목록 구조 문서 1부 완성
- 3-2의 시뮬레이션 단어 목록을 20개 이상으로 확장하고 실행합니다
- 3-5의 트리 구조로
자산-목록-구조.md를 만듭니다 — 항목 5개 이상, 각 항목에 서비스·버전·담당 칸 포함 - 수동/능동 열거 두 줄 요약을 문서 맨 앞에 씁니다 — 각각 "대상에 접속하는가?"로 구분
- "관리자가 잊은 문" 후보를 고르는 자기만의 규칙(접두어, 버전, 담당 공백 등)을 세 가지 적습니다
- 문서 어디에도 실제 타사 도메인을 질의한 결과를 넣지 않습니다 — 예시는 가공 표기
연습문제
문제 1. 수동 열거가 "조용한" 이유와, 그 대가로 가지는 한계를 설명해 보세요.
문제 2. 인증서 투명성 로그가 본래 목적(사기 인증서 탐지)과 다르게 공격 표면 지도로 쓰이는 이유를 설명해 보세요.
문제 3. 3-3에서 "이름을 알 수 없다"는 오류가 왜 열거 도구에게는 실패가 아니라 데이터인지 설명해 보세요.
문제 4. 공격자의 서브도메인 열거와 방어자의 자산 관리가 "같은 행위의 양면"이라는 말의 뜻을 설명해 보세요.
5. 모범 답안과 완료 기준
미션 모범 답안
문서 맨 앞의 두 줄 요약 예시:
수동 열거: 대상에 접속하지 않고 공개 기록(인증서 로그, 검색엔진)을 뒤진다 — 조용하지만 기록된 것만.
능동 열거: 사전 목록으로 DNS에 직접 질의한다 — 더 찾지만 로그에 남는다. 허가 필수.
"잊힌 문" 선별 규칙의 예시: ① dev/test/old/backup 접두어가 붙은 이름. ② 서비스 버전이 현재 지원 버전보다 낡은 것. ③ 담당 칸을 채울 사람이 없는 것. 세 규칙의 공통점은 "관심이 닿지 않는 자산 = 패치가 닿지 않는 자산"이라는 것입니다.
검증하는 법: ① 단어 목록 20개로 시뮬레이션을 돌렸는가. ② 문서에 서비스·버전·담당 칸이 있는가. ③ 수동/능동 구분이 "접속 여부"로 쓰였는가. ④ 타사 도메인 실측 결과가 없는가. 전부 ‘예’이면 완성입니다.
연습문제 해답
문제 1 해답. 수동 열거는 대상 서버에 패킷을 하나도 보내지 않고 제3의 공개 기록만 읽으므로 대상 로그에 아무 흔적이 남지 않습니다. 대가는 "기록된 것만 찾을 수 있다"는 점입니다 — 인증서를 받지 않은 서브도메인, 검색엔진이 모르는 자산은 수동 열거로는 보이지 않습니다.
문제 2 해답. 투명성 로그는 "누가 어떤 이름의 인증서를 받았는가"를 전부 공개하는 장부입니다. 인증서에는 서브도메인이 적히므로, 이 장부를 뒤지는 것만으로 조직의 이름 목록이 만들어집니다. 방어를 위해 만든 공개성이 정보 수집의 재료가 되는 것 — 보안 장치의 부수 효과가 공격 표면 지도가 되는 대표 사례입니다.
문제 3 해답. 사전 대입의 목적은 "있는 것"과 "없는 것"을 가르는 것입니다. socket.gaierror는 "없다"는 정상적인 답이고, 도구는 이 신호를 받아 후보를 지우고 다음 단어로 넘어갑니다. 프로그램이 멈춰야 할 예외가 아니라 반복문이 매번 처리하는 분기일 뿐입니다 — 오류를 데이터로 읽는 관점이 네트워크 도구 작성의 기본입니다.
문제 4 해답. 둘 다 "이 조직에 어떤 문이 있는가"를 목록으로 만드는 같은 일을 합니다. 다른 것은 목적과 그다음 행동입니다 — 공격자는 목록에서 약한 문을 골라 두드리고, 방어자는 목록의 모든 문에 담당과 패치를 붙입니다. 그래서 방어자가 먼저 이 목록을 만들어 두면, 공격자가 아는 것을 우리도 알게 됩니다. 자산 목록은 방어의 첫 페이지입니다.
완료 기준 체크리스트
- [ ] 수동/능동 열거를 "접속 여부"로 구분해 설명할 수 있다
- [ ] 인증서 투명성 로그의 원리와 부수 효과를 말할 수 있다
- [ ] 사전 대입 시뮬레이션을 실행하고 확장했다
- [ ]
socket.gaierror가 "없다"의 신호임을 실측했다 - [ ]
/etc/hosts와 이름 해석 흐름을 복습했다 - [ ] 도메인→서브도메인→서비스→버전 트리 문서를 완성했다
- [ ] 열거와 무단 접근의 경계를 자기 말로 쓸 수 있다
6. 흔한 실수와 해결
벽 1. socket.gaierror: [Errno -2] Name or service not known에 프로그램이 멈춘다
증상 (2026-09-09 실측):
socket.gaierror: [Errno -2] Name or service not known
원인: 없는 이름을 물었을 때의 정상 응답을 예외 처리하지 않은 것입니다. 사전 대입에서는 이 오류가 수만 번 나는 것이 정상입니다.
해결: 질의를 try/except socket.gaierror로 감싸고, 오류가 나면 "없음"으로 기록하고 다음 단어로 넘어가게 하세요. 오류는 데이터입니다 (3-3).
벽 2. crt.sh의 %가 뭔지 몰라 검색이 안 된다
증상: crt.sh에서 도메인을 쳤는데 서브도메인이 안 나옵니다.
원인: crt.sh의 와일드카드는 %입니다 — %.example.com처럼 써야 "이 도메인으로 끝나는 모든 이름"이 검색됩니다.
해결: 검색창에 %.도메인 형식으로 입력하세요. %가 "아무 글자나"라는 뜻의 SQL 와일드카드입니다.
벽 3. 수집한 목록을 보고 접속부터 해 버린다
증상: 발견한 서브도메인에 바로 접속하거나 포트를 두드립니다.
원인: "찾기"와 "두드리기"를 구분하지 않은 것입니다.
해결: 이 단계는 수집까지입니다. 발견된 자산에 접근하는 것은 별도의 허가가 필요한 다음 단계입니다. 열거가 합법이라고 접근까지 합법인 것은 아닙니다 — 목록을 만드는 손과 문을 두드리는 손은 다른 허가를 받습니다.
벽 4. 시뮬레이션에서 발견이 0개다
증상: 단어를 늘렸는데 발견 수가 그대로입니다.
원인: zone에 없는 단어만 추가했거나, 이름 조합의 철자가 영역 데이터와 다릅니다 — dev와 dev.lab.example의 연결 부분을 확인하세요.
해결: 시뮬레이션의 장점은 정답(zone의 내용)을 내가 안다는 것입니다. 단어 목록과 영역의 이름을 나란히 놓고 대조하세요. 실전에서는 정답지가 없으니, 이 연습에서 "목록의 품질이 결과를 결정한다"는 감각을 얻어 가면 됩니다.
벽 5. 목록만 크고 문서가 없다
증상: 서브도메인 수백 개의 텍스트만 있고 정리가 없습니다.
원인: 수집을 목적으로 착각한 것입니다 — 목록은 원료이지 산출물이 아닙니다.
해결: 3-5의 트리로 옮기세요. 서비스·버전·담당 칸이 붙는 순간 목록은 자산 문서가 되고, ⚠️가 붙는 순간 작업 계획이 됩니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 수동 열거 | 공개 기록만 뒤진다 — 조용하지만 기록된 것만 |
| 능동 열거 | DNS에 직접 질의한다 — 더 찾지만 흔적이 남는다 |
| 인증서 투명성 로그 | 인증서 발급의 공개 장부 — 서브도메인 목록이 되는 부수 효과 |
| 사전 대입 | 흔한 이름 목록 × DNS 질의 반복 — 응답 오는 것만 채취 |
| 공격 표면 | 외부에서 닿을 수 있는 모든 것의 총합 |
| 자산 목록 | 공격 표면에 담당·버전·조치를 붙인 방어 문서 |
| crt.sh / subfinder | 수동 열거의 창구 — 인증서 장부 검색 / 공개 출처 통합 |
오늘의 명령어·코드
| 도구 | 하는 일 |
|---|---|
cat /etc/hosts |
가장 작은 이름 해석 규칙 확인 (Step 33 복습) |
getent hosts 이름 |
이름 해석 질의 |
socket.gethostbyname() |
파이썬에서 이름 → 주소 |
try/except socket.gaierror |
"없다"는 신호를 데이터로 처리 |
f"{word}.{도메인}" |
단어 목록으로 후보 이름 조합 |
(화면 예시) subfinder -d 도메인 -silent |
다중 출처 서브도메인 수집 |
명령어보다 중요한 감각
오늘의 핵심은 도구가 아니라 관점입니다 — "조직의 서버는 조직도 다 모른다"는 사실, 그리고 잊힌 문이 가장 먼저 열린다는 사실. 사전 대입 15줄을 직접 돌려 본 여러분은 이제 subfinder의 출력이 마법이 아니라 반복문이라는 것을 압니다. 그리고 같은 목록을 방어자가 만들면 자산 문서가 됩니다. 공격자의 첫 페이지와 방어자의 첫 페이지가 같다는 것 — 이것이 OSINT 심화의 진짜 결론입니다.
전부 체크되면 Step 171 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.