DAY 1에서는 Python의
socket과ThreadPoolExecutor를 이용하여 여러 TCP Port를 빠르게 스캔하고 OPEN Port를 탐지했다.
DAY 2에서는 OPEN Port의 서비스 정보를 조회하고, 실제 HTTP 통신을 통해 HTTP Server Banner를 수집하는 기능을 추가한다.
DAY 1의 Scanner는 OPEN Port만 확인할 수 있었다.
[+] 127.0.0.1:135 OPEN
[+] 127.0.0.1:445 OPEN
[+] 127.0.0.1:8000 OPEN
하지만 Port가 열려 있다는 사실만으로는 어떤 서비스가 동작하고 있는지 알기 어렵다.
DAY 2에서는 다음과 같이 정보를 추가하는 것을 목표로 한다.
[+] 127.0.0.1:445 OPEN
| Service: microsoft-ds
| Banner: HTTP Banner 없음
[+] 127.0.0.1:8000 OPEN
| Service: 등록된 서비스 정보가 없음
| Banner: SimpleHTTP/0.6 Python/3.13.3
전체적인 흐름은 다음과 같다.
OPEN Port 탐지
↓
등록된 Service 이름 조회
↓
HTTP 요청 전송
↓
HTTP 응답 수신
↓
Server Header 분석
↓
HTTP Banner 출력
Python의 socket 모듈에는 Port 번호에 대응하는 서비스 이름을 조회할 수 있는 함수가 있다.
socket.getservbyport()
간단하게 테스트해보았다.
import socket
print(socket.getservbyport(22))
print(socket.getservbyport(80))
print(socket.getservbyport(443))
실행 결과
ssh
http
https
즉,
22 → ssh
80 → http
443 → https
와 같이 Port 번호에 등록된 서비스 이름을 확인할 수 있다.
여기서 주의해야 할 점이 있다.
getservbyport()는 실제 해당 Port에서 어떤 프로그램이 실행되고 있는지 확인하는 함수가 아니다.
예를 들어,
socket.getservbyport(80)
을 실행하면
80번 Port
↓
등록된 서비스 정보 조회
↓
http
와 같이 Port 번호에 등록된 서비스 이름을 조회한다.
따라서 실제 80번 Port에서 HTTP Server가 실행되고 있다는 것을 직접 확인한 것은 아니다.
이번에는 9999번 Port를 조회해보았다.
import socket
print(socket.getservbyport(9999))
실행 결과 오류가 발생했다.
OSError: port/proto not found
여기서 중요한 점은 9999번 Port가 존재하지 않는다는 의미가 아니라는 것이다.
9999번 Port도 사용할 수 있지만 getservbyport()가 참조하는 서비스 정보에서 해당 Port에 대응하는 서비스 이름을 찾지 못한 것이다.
9999번 Port를 사용할 수 있는가?
↓
YES
등록된 서비스 이름이 있는가?
↓
NO
↓
OSError
Scanner가 수많은 Port를 검사하다 보면 등록된 서비스 정보가 없는 Port를 만날 수 있다.
그때마다 프로그램이 종료되면 안 되기 때문에 try / except를 이용하여 예외를 처리한다.
import socket
try:
service = socket.getservbyport(9999)
print(service)
except OSError:
print("등록된 서비스 정보가 없음")
실행 결과
등록된 서비스 정보가 없음
동작 과정은 다음과 같다.
try
↓
getservbyport(9999)
↓
OSError 발생
↓
except OSError
↓
등록된 서비스 정보가 없음
이렇게 하면 오류가 발생해도 프로그램 전체가 종료되지 않는다.
서비스 조회 기능을 Scanner에서 계속 사용하기 위해 함수로 만들었다.
def get_service(port):
try:
service = socket.getservbyport(port)
return service
except OSError:
return "등록된 서비스 정보가 없음"
테스트
print(get_service(22))
print(get_service(80))
print(get_service(443))
print(get_service(9999))
실행 결과
ssh
http
https
등록된 서비스 정보가 없음
함수 안에서 print()를 사용하면 결과를 화면에 출력하는 것으로 끝난다.
하지만 return을 사용하면 함수에서 얻은 값을 다른 코드에서 다시 사용할 수 있다.
service = get_service(80)
print(service)
동작 과정
get_service(80)
↓
port = 80
↓
socket.getservbyport(80)
↓
service = "http"
↓
return service
↓
service = "http"
Scanner에서 Service 정보를 받아 출력해야 하기 때문에 return을 사용한다.
DAY 1에서 작성한 scan_port()에 get_service()를 연결했다.
if result == 0:
service = get_service(target_port)
print(f"[+] {target_ip}:{target_port} OPEN | Service: {service}")
이제 Scanner는 단순히 OPEN 여부만 출력하는 것이 아니라 등록된 Service 정보까지 출력할 수 있다.
실행 결과 예시
[+] 127.0.0.1:135 OPEN | Service: epmap
[+] 127.0.0.1:445 OPEN | Service: microsoft-ds
[+] 127.0.0.1:902 OPEN | Service: 등록된 서비스 정보가 없음
하지만 여기서 새로운 문제가 생긴다.
직접 Python HTTP Server를 실행해보았다.
python -m http.server 8000
분명히 8000번 Port에서 HTTP Server가 실행 중이다.
그런데 Scanner에서는 다음과 같이 출력되었다.
[+] 127.0.0.1:8000 OPEN | Service: 등록된 서비스 정보가 없음
즉,
실제 서비스
python -m http.server 8000
↓
HTTP
하지만
socket.getservbyport(8000)
↓
등록된 서비스 정보가 없음
이라는 차이가 발생했다.
이를 통해 getservbyport()는 실제 서비스를 탐지하는 기능이 아니라 등록된 서비스 정보를 조회하는 기능이라는 것을 확인할 수 있었다.
그래서 실제 서버와 통신하여 정보를 받아오는 기능을 추가하기로 했다.
먼저 테스트를 위해 Python HTTP Server를 실행한다.
python -m http.server 8000
8000번 Port에 TCP 연결을 시도한다.
import socket
target_ip = "127.0.0.1"
target_port = 8000
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2)
result = sock.connect_ex((target_ip, target_port))
print(result)
서버가 정상적으로 실행 중이면
0
이 출력된다.
connect_ex()의 반환값이 0이라는 것은 TCP 연결에 성공했다는 의미이다.
TCP 연결에 성공했으므로 이번에는 서버에 실제 HTTP 요청을 보내본다.
HTTP 요청 문자열을 생성한다.
request = "GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n"
사람이 보기 쉽게 표현하면 다음과 같다.
GET / HTTP/1.1
Host: 127.0.0.1
HTTP에서는 \r\n을 이용하여 줄을 구분한다.
마지막의
\r\n\r\n
은 HTTP Header가 끝났다는 것을 의미한다.
Socket을 통해 데이터를 전송할 때 문자열을 그대로 전송할 수 없기 때문에 bytes 형태로 변환한다.
request.encode()
변환 과정
str
↓
encode()
↓
bytes
변환된 데이터를 sendall()을 이용하여 서버에 전송한다.
sock.sendall(request.encode())
전체적인 흐름은 다음과 같다.
"GET / HTTP/1.1..."
↓
str
↓
encode()
↓
bytes
↓
sendall()
↓
HTTP Server
HTTP 요청을 보냈다면 서버의 응답도 받아야 한다.
response = sock.recv(4096)
4096은 Port 번호가 아니라 한 번에 최대 4096 bytes의 데이터를 수신하겠다는 의미이다.
처음에는 응답을 받지 않고 바로
sock.close()
를 실행했더니 HTTP Server에서 다음과 같은 오류가 발생했다.
ConnectionAbortedError
[WinError 10053]
동작 과정을 보면
Client
│
│ GET 요청
▼
Server
│
│ 200 OK 응답 준비
▼
Client가 먼저 연결 종료
↓
Server가 데이터를 보내지 못함
과 같은 상황이었다.
따라서 recv()를 사용하여 Server의 응답을 받은 후 연결을 종료하도록 수정하였다.
sock.sendall(request.encode())
response = sock.recv(4096)
sock.close()
recv()를 이용해 받은 데이터를 출력해보았다.
print(response)
결과는 다음과 같은 형태였다.
b'HTTP/1.0 200 OK\r\nServer: SimpleHTTP/0.6 Python/3.13.3...'
앞의 b는 해당 데이터가 bytes라는 의미이다.
사람이 읽기 편한 문자열로 변환하기 위해 decode()를 사용한다.
response = response.decode()
변환 과정
서버
↓
recv()
↓
bytes
↓
decode()
↓
str
결과
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.13.3
Date: ...
Content-type: text/html; charset=utf-8
Content-Length: 355
HTTP Server가 보낸 실제 응답을 확인할 수 있었다.
HTTP 응답 전체가 필요한 것은 아니다.
이번 Scanner에서 확인하고 싶은 것은 다음 정보이다.
Server: SimpleHTTP/0.6 Python/3.13.3
먼저 HTTP 응답을 줄 단위로 분리한다.
lines = response.split("\r\n")
예를 들어 다음 문자열이
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.13.3
Date: ...
Content-Type: text/html
split("\r\n")을 거치면 대략 다음과 같은 리스트가 된다.
[
"HTTP/1.0 200 OK",
"Server: SimpleHTTP/0.6 Python/3.13.3",
"Date: ...",
"Content-Type: text/html"
]
이제 for문으로 한 줄씩 확인한다.
for line in lines:
print(line)
Server:로 시작하는 문자열만 찾기 위해 startswith()를 사용한다.
for line in lines:
if line.startswith("Server:"):
print(line)
실행 결과
Server: SimpleHTTP/0.6 Python/3.13.3
startswith()는 문자열이 특정 문자열로 시작하는지 검사한다.
예를 들어
text = "Server: SimpleHTTP/0.6"
print(text.startswith("Server:"))
결과
True
가 된다.
현재 결과는 다음과 같다.
Server: SimpleHTTP/0.6 Python/3.13.3
여기에서 실제 Banner 정보만 필요하다.
SimpleHTTP/0.6 Python/3.13.3
먼저 :을 기준으로 문자열을 분리한다.
line.split(":")
결과
[
"Server",
" SimpleHTTP/0.6 Python/3.13.3"
]
두 번째 값이 필요하므로 [1]을 사용한다.
line.split(":")[1]
하지만 문자열 앞에 공백이 남아 있다.
SimpleHTTP/0.6 Python/3.13.3
↑
공백
따라서 strip()을 사용하여 앞뒤 공백을 제거한다.
banner = line.split(":")[1].strip()
최종 결과
SimpleHTTP/0.6 Python/3.13.3
지금까지 작성한 HTTP 통신 코드를 하나의 함수로 만들었다.
def get_http_banner(target_ip, target_port):
try:
# HTTP Banner 확인을 위한 TCP 소켓 생성
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 최대 2초 동안 응답 대기
sock.settimeout(2)
# 전달받은 IP와 Port에 TCP 연결
result = sock.connect_ex((target_ip, target_port))
if result == 0:
# HTTP 요청 생성
request = f"GET / HTTP/1.1\r\nHost: {target_ip}\r\n\r\n"
# HTTP 요청 전송
sock.sendall(request.encode())
# HTTP 응답 수신
response = sock.recv(4096)
# bytes → str
response = response.decode()
# HTTP 응답을 한 줄씩 분리
lines = response.split("\r\n")
# Server Header 검색
for line in lines:
if line.startswith("Server:"):
banner = line.split(":")[1].strip()
return banner
return "HTTP Banner 없음"
except (ConnectionResetError, TimeoutError):
return "HTTP Banner 없음"
HTTP Banner 함수를 445번 Port에 테스트해보았다.
banner = get_http_banner("127.0.0.1", 445)
print(banner)
TCP 연결 자체는 성공했지만 다음 오류가 발생했다.
ConnectionResetError
[WinError 10054]
현재 연결은 원격 호스트에 의해 강제로 끊겼습니다.
동작 과정
Scanner
│
│ TCP 연결
▼
445 Port
│
└─ 연결 성공
Scanner
│
│ GET / HTTP/1.1
▼
445 Port
│
└─ HTTP 요청을 처리하는 서비스가 아님
↓
연결 종료
Scanner
│
└─ recv()
↓
ConnectionResetError
즉,
Port OPEN = HTTP Service
가 아니라는 것을 알 수 있다.
OPEN은 단순히 해당 TCP Port에서 연결을 받아주는 서비스가 있다는 의미이다.
따라서 다음과 같이 예외 처리를 추가하였다.
except (ConnectionResetError, TimeoutError):
return "HTTP Banner 없음"
ConnectionResetError 또는 TimeoutError가 발생하면 프로그램을 종료하지 않고
HTTP Banner 없음
을 반환한다.
두 Port를 비교해보았다.
banner = get_http_banner("127.0.0.1", 445)
print(banner)
banner = get_http_banner("127.0.0.1", 8000)
print(banner)
결과
HTTP Banner 없음
SimpleHTTP/0.6 Python/3.13.3
즉,
445
↓
TCP OPEN
↓
HTTP 요청
↓
HTTP Banner 없음
8000
↓
TCP OPEN
↓
HTTP 요청
↓
HTTP 응답
↓
Server Header 발견
↓
SimpleHTTP/0.6 Python/3.13.3
으로 동작한다.
마지막으로 DAY 1에서 작성한 멀티스레드 Port Scanner와 오늘 만든 기능들을 합쳤다.
구조는 다음과 같다.
scan_port()
│
├── Port OPEN 여부 확인
│
├── get_service()
│ ↓
│ 등록된 Service 이름 조회
│
└── get_http_banner()
↓
실제 HTTP 응답 분석
OPEN Port가 발견되면 다음 두 함수를 호출한다.
if result == 0:
service = get_service(target_port)
banner = get_http_banner(target_ip, target_port)
print(
f"[+] {target_ip}:{target_port} OPEN "
f"| Service: {service} "
f"| Banner: {banner}"
)
import socket
from concurrent.futures import ThreadPoolExecutor
# 스캔할 대상 IP 입력
target_ip = input("스캔할 IP를 입력하세요: ")
# Port 번호에 등록된 Service 이름을 조회하는 함수
def get_service(port):
try:
service = socket.getservbyport(port)
return service
except OSError:
return "등록된 서비스 정보가 없음"
# HTTP 응답에서 Server Banner를 확인하는 함수
def get_http_banner(target_ip, target_port):
try:
# HTTP Banner 확인을 위한 IPv4 + TCP 소켓 생성
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 최대 2초 동안 응답 대기
sock.settimeout(2)
# 전달받은 IP와 Port에 TCP 연결
result = sock.connect_ex((target_ip, target_port))
if result == 0:
# HTTP GET 요청 생성
request = f"GET / HTTP/1.1\r\nHost: {target_ip}\r\n\r\n"
# 문자열을 bytes로 변환하여 HTTP 요청 전송
sock.sendall(request.encode())
# HTTP 응답을 최대 4096 bytes까지 수신
response = sock.recv(4096)
# bytes 데이터를 문자열로 변환
response = response.decode()
# HTTP 응답을 한 줄씩 분리
lines = response.split("\r\n")
# Server Header 검색
for line in lines:
if line.startswith("Server:"):
# "Server:" 뒤의 Banner 정보만 추출
banner = line.split(":")[1].strip()
return banner
return "HTTP Banner 없음"
except (ConnectionResetError, TimeoutError):
return "HTTP Banner 없음"
# 하나의 Port를 검사하는 함수
def scan_port(target_ip, target_port):
# IPv4(AF_INET) + TCP(SOCK_STREAM) 소켓 생성
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# TCP 연결 시 최대 0.5초 동안 응답 대기
sock.settimeout(0.5)
# 대상 IP와 Port에 TCP 연결 시도
result = sock.connect_ex((target_ip, target_port))
# connect_ex() 반환값이 0이면 TCP 연결 성공
if result == 0:
# Port 번호에 등록된 Service 이름 조회
service = get_service(target_port)
# 실제 HTTP 통신을 통한 Server Banner 확인
banner = get_http_banner(target_ip, target_port)
# OPEN Port + Service + Banner 출력
print(
f"[+] {target_ip}:{target_port} OPEN "
f"| Service: {service} "
f"| Banner: {banner}"
)
# 현재 Port 검사 종료
sock.close()
# 최대 50개의 작업자 스레드를 사용하는 Thread Pool 생성
with ThreadPoolExecutor(max_workers=50) as executor:
# 1번부터 8000번 Port까지 반복
for target_port in range(1, 8001):
# scan_port() 작업을 Thread Pool에 제출
executor.submit(scan_port, target_ip, target_port)
테스트를 위해 별도의 Terminal에서 Python HTTP Server를 실행하였다.
python -m http.server 8000
Scanner 실행 결과 일부
[+] 127.0.0.1:4441 OPEN | Service: 등록된 서비스 정보가 없음 | Banner: HTTP Banner 없음
[+] 127.0.0.1:5040 OPEN | Service: 등록된 서비스 정보가 없음 | Banner: HTTP Banner 없음
[+] 127.0.0.1:6463 OPEN | Service: 등록된 서비스 정보가 없음 | Banner: HTTP Banner 없음
[+] 127.0.0.1:7680 OPEN | Service: ms-do | Banner: HTTP Banner 없음
[+] 127.0.0.1:7895 OPEN | Service: 등록된 서비스 정보가 없음 | Banner: Microsoft-HTTPAPI/2.0
[+] 127.0.0.1:8000 OPEN | Service: 등록된 서비스 정보가 없음 | Banner: SimpleHTTP/0.6 Python/3.13.3
특히 8000번 Port를 보면
Service: 등록된 서비스 정보가 없음
이라고 나오지만 실제 HTTP 통신을 통해
Banner: SimpleHTTP/0.6 Python/3.13.3
이라는 정보를 확인할 수 있었다.
또한 테스트 과정에서 7895번 Port에서는
Service: 등록된 서비스 정보가 없음
Banner: Microsoft-HTTPAPI/2.0
이라는 결과도 확인할 수 있었다.
이를 통해 Port 번호에 등록된 서비스 이름을 조회하는 것과 실제 서비스의 응답을 분석하는 것은 서로 다른 과정이라는 것을 확인할 수 있었다.
| 구분 | getservbyport() | HTTP Banner 확인 |
|---|---|---|
| 확인 방법 | Port 번호 기반 | 실제 네트워크 통신 |
| TCP 연결 | 필요 없음 | 필요 |
| 데이터 송수신 | 없음 | 있음 |
| Service 정보 | 등록된 서비스 이름 | 실제 HTTP 응답 정보 |
| 예시 | 80 → http | Server: Apache |
| 한계 | 실제 실행 서비스와 다를 수 있음 | HTTP 응답을 제공하는 서비스 중심 |
| 함수 | 역할 |
|---|---|
socket.getservbyport() | Port 번호에 등록된 서비스 이름 조회 |
socket.socket() | Socket 생성 |
connect_ex() | TCP 연결 시도 |
settimeout() | 응답 대기 시간 설정 |
sendall() | 서버에 데이터 전송 |
recv() | 서버에서 데이터 수신 |
encode() | str → bytes 변환 |
decode() | bytes → str 변환 |
split() | 문자열 분리 |
startswith() | 특정 문자열로 시작하는지 확인 |
strip() | 문자열 앞뒤 공백 제거 |
return | 함수의 결과값 반환 |
try / except | 예외 처리 |
Socket 통신에서는 문자열을 그대로 전송하는 것이 아니라 bytes 형태의 데이터를 주고받는다.
str
↓
encode()
↓
bytes
↓
sendall()
↓
Server
Server
↓
recv()
↓
bytes
↓
decode()
↓
str
Target IP
↓
Port Scan
↓
OPEN / CLOSED 확인
출력
[+] 127.0.0.1:8000 OPEN
Target IP
↓
Port Scan
↓
OPEN Port
↓
Service 조회
↓
HTTP 요청
↓
HTTP 응답 분석
↓
Server Banner 추출
출력
[+] 127.0.0.1:8000 OPEN
| Service: 등록된 서비스 정보가 없음
| Banner: SimpleHTTP/0.6 Python/3.13.3
DAY 2에서는 단순히 새로운 기능만 추가한 것이 아니라 이전에 공부했던 Python 문법들이 실제 프로그램에서 어떻게 사용되는지 확인할 수 있었다.
함수
↓
매개변수
↓
return
↓
try / except
↓
for
↓
if
↓
f-string
↓
split()
↓
startswith()
↓
strip()
또한 Network Programming과 관련하여
Socket 생성
↓
TCP 연결
↓
HTTP Request
↓
sendall()
↓
Server
↓
HTTP Response
↓
recv()
↓
Banner 분석
과정을 직접 구현하였다.
특히 다음 차이를 이해하는 것이 중요했다.
Port가 OPEN이다.
≠
어떤 서비스인지 정확히 알고 있다.
그리고
getservbyport()
→ Port 번호에 등록된 서비스 정보 조회
HTTP Banner 확인
→ 실제 Server와 통신하여 응답 정보 확인
이라는 차이도 확인할 수 있었다.
DAY 1에서 만들었던 단순 TCP Port Scanner에
✔ Service 이름 조회
✔ OSError 예외 처리
✔ HTTP Request 전송
✔ HTTP Response 수신
✔ bytes ↔ str 변환
✔ HTTP Server Header 분석
✔ Banner 추출
✔ ConnectionResetError / TimeoutError 처리
✔ 기존 Multi Thread Scanner와 결합
기능을 추가하였다.
현재 Scanner의 구조는 다음과 같다.
Python Security Scanner
│
├── ThreadPoolExecutor
│
└── scan_port()
│
├── TCP Port Scan
│
├── get_service()
│ └── Service 이름 조회
│
└── get_http_banner()
├── HTTP Request
├── HTTP Response
└── Server Banner 추출
DAY 1에서는 "Port가 열려 있는가?"를 확인했다면,
DAY 2에서는 한 단계 발전하여 "열린 Port에서 어떤 서비스 정보를 얻을 수 있는가?"까지 확인할 수 있게 되었다.
처음 socket.getservbyport()를 테스트하면서 9999번 Port를 조회했더니 다음 오류가 발생했다.
OSError: port/proto not found
getservbyport()는 실제 Port가 열려 있는지 확인하는 함수가 아니라 Port 번호에 등록된 서비스 이름을 조회하는 함수이다.
따라서 해당 Port에 등록된 서비스 정보가 없으면 OSError가 발생한다.
try / except를 사용하여 예외를 처리했다.
def get_service(port):
try:
service = socket.getservbyport(port)
return service
except OSError:
return "등록된 서비스 정보가 없음"
결과
9999 → 등록된 서비스 정보가 없음
이 과정을 통해 Scanner는 등록되지 않은 Port를 만나더라도 종료되지 않고 계속 실행할 수 있게 되었다.
Python HTTP Server를 실행한 뒤 HTTP 요청을 전송했는데 Server Terminal에서 다음과 같은 오류가 발생했다.
ConnectionAbortedError
[WinError 10053]
현재 연결은 사용자의 호스트 시스템의 소프트웨어에 의해 중단되었습니다.
처음 작성한 코드에서는 HTTP 요청을 보낸 후 Server의 응답을 제대로 받지 않고 Socket 연결을 종료했다.
Scanner
│
│ HTTP Request
▼
HTTP Server
│
│ HTTP Response 전송
▼
Scanner가 먼저 연결 종료
Server 입장에서는 응답 데이터를 보내는 도중 연결이 종료된 것이다.
recv()를 이용하여 Server의 응답을 받은 후 Socket을 종료하도록 수정했다.
sock.sendall(request.encode())
response = sock.recv(4096)
sock.close()
이를 통해 HTTP의 기본적인 요청 → 응답 과정도 직접 확인할 수 있었다.
445번 Port가 OPEN 상태였기 때문에 HTTP Banner 함수를 테스트해보았다.
하지만 다음 오류가 발생했다.
ConnectionResetError
[WinError 10054]
현재 연결은 원격 호스트에 의해 강제로 끊겼습니다.
445번 Port에 TCP 연결하는 것 자체는 성공했다.
connect_ex()
↓
result == 0
↓
TCP 연결 성공
하지만 이후 Scanner가 HTTP 요청을 전송했다.
GET / HTTP/1.1
445번에서 동작하는 서비스는 HTTP 요청을 처리하는 웹 서버가 아니기 때문에 연결이 종료되었고, 이후 recv() 과정에서 ConnectionResetError가 발생했다.
즉,
Port OPEN
≠
HTTP Service
라는 것을 확인할 수 있었다.
try / except를 이용하여 연결 종료와 Timeout을 처리했다.
except (ConnectionResetError, TimeoutError):
return "HTTP Banner 없음"
이제 HTTP가 아닌 서비스에 요청을 보내더라도 Scanner 전체가 종료되지 않는다.
[+] 127.0.0.1:445 OPEN
| Service: microsoft-ds
| Banner: HTTP Banner 없음
테스트를 위해 직접 HTTP Server를 실행했다.
python -m http.server 8000
하지만 getservbyport()의 결과는 다음과 같았다.
Service: 등록된 서비스 정보가 없음
실제로는 HTTP Server가 실행되고 있는데 Service 정보를 확인하지 못한 것이다.
getservbyport()는 실제 동작 중인 서비스를 분석하는 것이 아니라 Port 번호에 등록된 서비스 이름을 조회하기 때문이다.
실제 서비스에 HTTP Request를 전송하고 HTTP Response의 Server: Header를 분석하는 Banner 확인 기능을 직접 구현했다.
결과
[+] 127.0.0.1:8000 OPEN
| Service: 등록된 서비스 정보가 없음
| Banner: SimpleHTTP/0.6 Python/3.13.3
이를 통해
Port 번호 기반 정보
↓
getservbyport()
실제 서비스 응답 기반 정보
↓
Banner Grabbing
의 차이를 확인할 수 있었다.
DAY 2에서 가장 중요하게 느낀 부분은 단순히 Port 번호만으로 실제 서비스를 판단할 수 없다는 점이다.
DAY 1에서는
8000 OPEN
까지만 알 수 있었다.
DAY 2에서는
8000 OPEN
↓
등록된 Service 정보 확인
↓
실제 HTTP Request 전송
↓
HTTP Response 수신
↓
Server Header 분석
↓
SimpleHTTP/0.6 Python/3.13.3
까지 확인할 수 있게 되었다.
또한 Python을 공부하면서 각각 따로 배웠던
함수
return
try / except
for
if
f-string
split()
startswith()
strip()
등의 문법이 실제 프로그램 안에서 어떤 목적으로 사용되는지도 조금씩 이해할 수 있었다.
특히 이번 DAY 2에서는 예외 처리가 왜 필요한지 직접 경험할 수 있었다.
Scanner처럼 여러 Port와 통신하는 프로그램에서는 모든 Port가 예상한 대로 응답하지 않는다.
따라서
정상적인 상황만 처리하는 코드
뿐만 아니라
응답이 없으면?
연결이 끊기면?
서비스 정보가 없으면?
HTTP 서비스가 아니면?
같은 상황도 처리해야 안정적인 프로그램을 만들 수 있다는 것을 알게 되었다.
현재 Scanner는 다음 단계까지 구현되어 있다.
Target IP 입력
↓
Multi Thread Port Scan
↓
OPEN Port 탐지
↓
Service 이름 조회
↓
HTTP Request 전송
↓
HTTP Banner 확인
하지만 아직 개선해야 할 부분이 있다.
현재는 OPEN Port가 발견되면 HTTP 서비스인지 알 수 없는 상태에서도 get_http_banner()를 실행한다.
445 OPEN
↓
HTTP 요청 전송
↓
HTTP가 아님
↓
예외 처리
8000 OPEN
↓
HTTP 요청 전송
↓
HTTP Server
↓
Banner 획득
또한 현재 결과는 Terminal에만 출력되고 프로그램이 종료되면 결과가 남지 않는다.
따라서 DAY 3에서는 Scanner의 결과를 조금 더 체계적으로 관리하는 방향으로 발전시켜 볼 예정이다.
예를 들어 최종적으로 다음과 같은 결과를 만들어보는 것이 목표이다.
========================================
Python Security Scanner
========================================
Target : 127.0.0.1
[+] 135 OPEN
Service : epmap
Banner : -
[+] 445 OPEN
Service : microsoft-ds
Banner : -
[+] 7895 OPEN
Service : Unknown
Banner : Microsoft-HTTPAPI/2.0
[+] 8000 OPEN
Service : Unknown
Banner : SimpleHTTP/0.6 Python/3.13.3
----------------------------------------
Open Ports : 4
Scan Time : 3.21 sec
----------------------------------------
그리고 이후에는 단순히 Port를 찾는 Scanner에서 끝나는 것이 아니라,
Port Scan
↓
Service Detection
↓
Banner Grabbing
↓
결과 저장
↓
결과 분석
처럼 하나씩 기능을 확장해 볼 예정이다.
DAY 1에서 "어떤 Port가 열려 있는가?"를 확인했다면, DAY 2에서는 실제 서비스와 통신하여 "열린 Port에서 어떤 정보를 얻을 수 있는가?"까지 확인해보았다.
다음은 DAY 3 - 스캔 결과 관리 및 Scanner 기능 개선 🚀