본문 바로가기

네트워크 프로그래밍 기초

모의 해킹·정보보안 도구 중 상당 부분은 네트워크 위에서 패킷을 주고받는 프로그램입니다. 그렇기에 네트워크 프로그래밍 기초는 정보보안 담당자, 보안 도구 개발자, 모의 해킹을 수행하는 화이트해커의 필수 소양입니다.

이번 장에서는 TCP/IP 4계층과 HTTP 메시지 구조를 보안 도구 제작자의 눈으로 다시 정리한 뒤, 파이썬 socket·http.server·requests로 서버와 클라이언트를 직접 만들어 주고받는 메시지를 확인해봅니다. 이어서 가능한 공격들을 바이브 코딩으로 정리하고 직접 구현해보며, 해커가 어떤 관점으로 이런 도구들을 만드는지 체화합니다.

1. TCP/IP 4계층

정보보안 담당자에게 네트워크 지식은 "이론"이 아니라 "언어"입니다. 스캐너가 왜 특정 포트에서 응답이 없는지, 왜 SYN만 보내고 ACK를 돌려주지 않는지를 설명하지 못하면, 도구에도 보고서에도 공백이 생깁니다. SYN 하나만 알아도 시도해볼 수 있는 공격이 많은데, SYN이 무엇인지 모른다면 공격의 상당 부분이 블랙박스가 됩니다.

SYN flooding, SYN 스캔 같은 기법은 TCP 연결 수립 과정에서 SYN 패킷만 보내고 ACK를 돌려주지 않는 방식으로 동작합니다. SYN flooding은 서버의 연결 대기 큐(backlog)를 고갈시켜 DoS를 유발하고, SYN 스캔은 연결을 완성하지 않기 때문에 로그에 잘 남지 않아 스텔스 스캔으로 불립니다. SYN이 무엇인지 모르면 이런 공격이 왜 가능한지도 이해할 수 없습니다.

TCP/IP는 인터넷 통신을 위한 프로토콜 모음(Protocol Suite)으로, 데이터를 패킷으로 나누어 전송하고 수신 측에서 재조립하는 과정을 4개의 계층(네트워크 접근 → 인터넷 → 전송 → 응용)으로 나누어 담당합니다.

1.1 편지 비유로 이해하기

계층보내는 쪽받는 쪽보안 도구 관점
애플리케이션편지 내용 작성편지 읽기HTTP 요청/응답, 로그인 폼, 쿠키
트랜스포트페이지 단위로 나누기 (TCP: 순서·재전송 보장)시퀀스 번호로 재조립포트 스캐너, 3-way handshake
인터넷봉투에 IP 주소 쓰기봉투에서 내용 꺼내기IP 위조, ICMP, 라우팅
링크우체국에 발송 (MAC)봉투 수신패킷 캡처, ARP 스푸핑

1.2 캡슐화

HTTP 메시지는 TCP 세그먼트로 쪼개지고, 각 세그먼트는 IP 패킷에 담기고, IP 패킷은 이더넷 프레임에 실려 전송됩니다. 각 계층은 위 계층이 만든 데이터를 통째로 "페이로드"로 취급하고, 자기 일에 필요한 정보를 담은 헤더를 앞에 붙여 아래 계층으로 넘깁니다. 와이어샤크(Wireshark)로 패킷을 분석할 때 우리는 이 양파 껍질을 거꾸로 벗기는 셈입니다.

1.3 계층별로 보는 캡슐화

아래 표는 보내는 쪽에서 거치는 순서대로, 위에서 아래로 읽습니다. 이 책에서는 IPv4 기준으로 설명하며, 업계에서 흔히 말하는 L2/L3/L4/L7은 OSI 번호 기준임을 덧붙입니다(L2=링크, L3=IP, L4=TCP/UDP, L7=애플리케이션).

계층이 계층이 만드는 단위앞에 붙는 헤더의 핵심 정보
애플리케이션 (HTTP)메시지(Message)없음 — 메시지 자체가 페이로드
트랜스포트 (TCP)세그먼트(Segment)출발지·목적지 포트, 시퀀스/ACK 번호, 플래그(SYN/ACK/FIN 등)
인터넷 (IP)패킷(Packet)출발지·목적지 IP 주소, TTL, 프로토콜 번호(TCP=6)
링크 (Ethernet/Wi-Fi)프레임(Frame)출발지·목적지 MAC 주소, 타입(IPv4=0x0800), FCS

보내는 쪽은 위에서 아래로 내려가며 헤더를 붙이고(캡슐화), 받는 쪽은 아래에서 위로 올라가며 헤더를 떼어냅니다(디캡슐화, decapsulation). 각 계층은 바로 위·아래 계층만 알면 됩니다. TCP는 자기가 실리는 링크가 이더넷인지 Wi-Fi인지 신경 쓰지 않고, 이더넷도 자기가 실어나르는 IP 패킷 안에 든 것이 TCP인지 UDP인지, 더 위의 HTTP인지 SSH인지 모릅니다.

표에 나온 낯선 단어들 — 한 줄 정리 — 바로 뒤 예시에서 곧장 쓰이니 먼저 눈에 익혀두세요.

  • 시퀀스 번호(sequence number, 줄여서 seq) — TCP가 바이트마다 매기는 일련번호. "이 세그먼트의 데이터가 전체에서 몇 번째 바이트부터 시작하는지"를 알려줍니다. 중간에 하나가 유실되면 그 seq만 재전송하고, 도착 순서가 뒤엉켜도 seq로 원래 순서대로 재조립할 수 있습니다. 참고로 첫 시퀀스 번호는 예측 불가능하게 무작위로 정해지는데, 이를 ISN(Initial Sequence Number)이라 부릅니다. 뒤에서 다룰 시퀀스 번호 예측 공격과 직결되는 개념입니다.
  • ACK(Acknowledgement) 번호 — "여기까지 잘 받았다"는 수신 확인. 받은 마지막 바이트 + 1을 돌려주면서 "다음엔 이 번호부터 보내라"고 알려주는 번호입니다.
  • 플래그(SYN / ACK / FIN / RST / PSH / URG) — TCP 헤더의 1비트 스위치들. SYN(Synchronize)은 연결 시작 요청, ACK는 수신 확인, FIN(Finish)은 연결 종료 요청, RST(Reset)는 연결 강제 종료입니다. 여기서는 3-way handshake에 쓰이는 SYN·ACK·FIN 세 개만 먼저 살펴보고, 포트 스캔 응답 해석에 핵심인 RST와 나머지 플래그는 뒤에서 다시 다룹니다.
  • MSS(Maximum Segment Size) — 한 TCP 세그먼트에 실을 수 있는 최대 데이터 크기. 실제 이더넷 환경에서는 보통 1,460바이트지만, 뒤의 예시에서는 감을 잡기 쉽도록 4바이트로 과장해 씁니다.
  • TTL(Time To Live) — IP 패킷이 살아남을 수 있는 남은 홉(hop, 라우터 통과) 수. 라우터를 하나 지날 때마다 1씩 줄고, 0이 되면 버려집니다. 경로에 무한 루프가 생겨도 패킷이 영원히 떠돌지 않게 하는 안전장치입니다.
  • MTU(Maximum Transmission Unit) — 링크 한 구간이 한 번에 전송할 수 있는 최대 바이트 수. 경로 중간에 MTU가 더 작은 구간을 만나면 IP가 패킷을 또 한 번 쪼개는데, 이를 IP 단편화라 부릅니다. 단편화는 뒤에서 Teardrop·Ping of Death 같은 공격 기법의 배경으로 다시 등장합니다.
  • MAC 주소 — NIC(Network Interface Card, 흔히 말하는 "랜카드")에 박힌 고유한 물리 주소. 같은 LAN 안에서 "바로 옆 기기"를 가리키는 데 씁니다.
  • FCS(Frame Check Sequence) — 프레임 끝에 붙는 체크섬. 전송 도중 비트가 뒤집히지 않았는지 수신 측이 검사합니다.
  • 프로토콜 번호 — IP 헤더가 "내 안에 든 내용물이 뭔지" 표시하는 숫자. IPv4에서 TCP는 6, UDP는 17, ICMP는 1입니다.

1.4 예시

4계층 (애플리케이션) — 서버의 파이썬 코드가 "HTTP/1.0 200 OK\n\nabcdef" 문자열 22바이트를 완성합니다. 이 시점까지는 하나의 덩어리입니다.

3계층 (트랜스포트) — TCP가 이 22바이트를 MSS(한 세그먼트에 실을 수 있는 최대 크기, 예시에선 감을 잡기 쉽도록 4바이트로 과장) 단위로 쪼개 시퀀스 번호(seq)를 붙입니다. seq는 "이 조각의 첫 바이트가 전체에서 몇 번째인지"를 나타내므로, 예시에서 seq=1, 5, 9, …처럼 4씩 늘어납니다. 중간에 한 조각이 유실되면 해당 seq만 재전송되고, 도착 순서가 뒤집혀도 seq로 원래 자리에 재조립됩니다.

세그먼트 1 : src=80, dst=51321, seq=1,  data="HTTP"
세그먼트 2 : src=80, dst=51321, seq=5,  data="/1.0"
세그먼트 3 : src=80, dst=51321, seq=9,  data=" 200"
세그먼트 4 : src=80, dst=51321, seq=13, data=" OK\n"
세그먼트 5 : src=80, dst=51321, seq=17, data="\nabc"
세그먼트 6 : src=80, dst=51321, seq=21, data="def"

2계층 (인터넷) — 각 TCP 세그먼트 앞에 IP 헤더가 붙습니다. 이때 비로소 "어느 호스트로 갈지"가 결정됩니다. 이 헤더의 TTL(Time To Live, 남은 홉 수)은 라우터를 지날 때마다 1씩 줄고 0이 되면 경로 중간에서 버려집니다. 경로상의 MTU(링크 한 구간이 한 번에 전송할 수 있는 최대 크기)보다 패킷이 크면 IP가 또 한 번 쪼개는 IP 단편화가 일어납니다.

IP 패킷 1 : src=203.0.113.10, dst=198.51.100.7, ttl=64, proto=TCP, payload=[세그먼트 1]
IP 패킷 2 : src=203.0.113.10, dst=198.51.100.7, ttl=64, proto=TCP, payload=[세그먼트 2]
... (세그먼트 수만큼 반복)

1계층 (링크) — 각 IP 패킷은 바로 다음 홉(next hop, 경로상 다음 라우터)의 MAC 주소(NIC에 박힌 물리 주소)를 목적지로 한 이더넷 프레임 안에 실립니다. 라우터를 지날 때마다 IP 주소는 그대로지만 MAC 주소는 매 구간마다 바뀝니다 — 최종 목적지는 IP가, "옆자리 누구에게 넘길지"는 MAC이 맡기 때문입니다. 프레임 끝에는 전송 중 비트가 뒤집히지 않았는지 검사하는 체크섬인 FCS(Frame Check Sequence)가 붙습니다.

프레임 1 : dst_mac=AA:BB:..(게이트웨이), src_mac=11:22:..(내 NIC), type=IPv4, payload=[IP 패킷 1], FCS=...
프레임 2 : dst_mac=AA:BB:..(게이트웨이), src_mac=11:22:..(내 NIC), type=IPv4, payload=[IP 패킷 2], FCS=...

수신 측은 이 과정을 거꾸로 밟습니다. NIC(랜카드)가 프레임의 FCS로 전송 오류를 검사해 IP 패킷을 꺼내고, IP 계층이 단편을 합쳐 TCP 세그먼트를 넘기고, TCP가 앞서 붙여둔 seq 번호 순서대로 바이트 스트림을 재조립해 애플리케이션에게 원래의 "HTTP/1.0 200 OK\n\nabcdef"를 건넵니다.

1.5 IP 주소와 포트

  • IP 주소: 네트워크에 연결된 호스트 식별자. IPv4는 32비트(192.0.2.10), IPv6는 128비트(2001:db8::10).
  • 포트: 한 호스트 안에서 통신하는 프로세스를 구분하는 16비트 번호(0 ~ 65535).
    • 0 ~ 1023 — Well-Known Ports (HTTP 80, HTTPS 443, SSH 22, DNS 53 …)
    • 1024 ~ 49151 — Registered Ports
    • 49152 ~ 65535 — Dynamic / Ephemeral Ports

포트 스캐닝은 결국 "이 호스트의 어느 창문이 열려 있는가"를 확인하는 작업입니다. 열린 포트 목록은 그대로 공격 표면(attack surface)이 됩니다.

1.6 TCP vs UDP

항목TCPUDP
연결연결 지향 (3-way handshake)비연결
신뢰성재전송·순서 보장없음
속도상대적으로 느림빠름
대표 프로토콜HTTP(S), SSH, FTPDNS, DHCP, VoIP
비유전화 통화편지 발송

파이썬에서는 소켓 타입으로 구분합니다.

import socket

tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # TCP
udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)   # UDP

보안 도구에서 자주 쓰는 세 가지는 모두 TCP 기반입니다. 아래와 같은 도구들이 있습니다.

  • 포트 스캐너(Port Scanner) — 대상 호스트의 여러 포트에 TCP 연결을 시도해 어떤 서비스가 열려 있는지 확인합니다.
  • 배너 그래버(Banner Grabber) — 열린 포트에 연결한 뒤 서버가 먼저 보내주는 응답(배너)을 읽어 서비스명·버전을 수집합니다. CVE(Common Vulnerabilities and Exposures, 공개된 보안 취약점에 매겨지는 고유 번호 — 예: CVE-2021-44228) 매칭의 출발점입니다.
  • 리버스 쉘(Reverse Shell) — 대상 호스트가 공격자 쪽으로 먼저 TCP 연결을 여는 원격 셸. 방화벽이 보통 내부→외부 아웃바운드는 허용하는 점을 이용합니다. 본인이 통제하는 실습 환경에서만 사용합니다.

1.7 3-way Handshake

TCP 연결은 세 번의 패킷 교환으로 시작됩니다.

  • SYN 스캔 — 3-way handshake를 완료하지 않고 SYN-ACK만 받고 끊기. 세션 로그가 남지 않아 "stealth scan"이라 부릅니다.
  • TCP Connect 스캔 — 파이썬 socket.connect()처럼 OS에 완전한 연결을 맡기는 방식. 로그에 흔적이 남지만 구현이 간단합니다.

다음 장에서 직접 멀티스레드 포트 스캐너를 만들 때 이 두 방식의 차이를 꺼내 쓰게 됩니다.

2. HTTP — 보안 실무에서 가장 많이 다루는 프로토콜

HTTP(HyperText Transfer Protocol)는 클라이언트와 서버 간에 데이터를 주고받기 위한 약속(프로토콜)입니다. 우리가 만들 대부분의 웹 취약점 점검 도구(SQLi(SQL Injection, 입력값에 SQL 문법을 끼워 넣어 DB 쿼리의 의미를 바꾸는 공격)·XSS(Cross-Site Scripting, 피해자의 브라우저에서 악성 스크립트가 실행되게 하는 공격) 스캐너, 디렉터리 브루트포서, 세션 하이재커 탐지기 등)는 결국 "HTTP 요청을 조금씩 변형해서 응답의 차이를 관찰하는" 프로그램입니다.

Protocol(프로토콜) — 약속입니다. 송신자와 수신자가 "이 포맷으로 주고받자"라고 미리 정해둔 규칙. HTTP는 그 규칙 중 가장 많이 쓰이는 애플리케이션 계층 프로토콜이며, 대부분 TCP 위에서 동작합니다.

2.1 HTTP 버전

버전출시년도설명
HTTP/1.11997현재 가장 많이 사용, 보안 도구의 기본 타깃
HTTP/22015바이너리 프레이밍, 헤더 압축
HTTP/32022UDP(QUIC, 구글이 설계한 저지연 전송 프로토콜) 기반, 점진적 도입 중

보안 도구를 처음 만들 때는 HTTP/1.1에 집중하는 것이 좋습니다. 메시지가 사람 눈에 보이는 텍스트라 디버깅이 쉽고, 대부분의 취약점 실습 환경(DVWA, bWAPP 등)이 이 버전을 씁니다.

2.2 HTTPS

HTTPS는 HTTP + TLS(Transport Layer Security, 통신을 암호화·인증하는 프로토콜로 이전 이름은 SSL)입니다. TLS 아래 흐르는 HTTP 메시지 자체는 동일합니다. requests가 알아서 암복호화를 해주므로 코드는 http:// ↔ https:// 교체 한 글자면 충분합니다.

3. HTTP 메시지 구조

HTTP 요청과 응답은 동일한 포맷을 공유합니다. 첫 줄 + 헤더 + 빈 줄 + 바디.

빈 줄(\r\n\r\n)이 헤더와 바디를 가르는 유일한 신호입니다. socket으로 HTTP를 직접 구현할 때 이 빈 줄을 잊어버리면 브라우저가 응답을 영원히 기다립니다.

3.1 요청 메시지

GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Firefox/93.0
Accept: text/html,application/xhtml+xml,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
구성내용보안 도구 관점
요청 라인GET /index.html HTTP/1.1메서드·경로·버전 파싱의 기준
헤더Host, User-Agent, Accept, Cookie, Referer …User-Agent·Referer 위조, 쿠키 탈취
빈 줄헤더 끝파서 버그가 가장 자주 나는 경계
바디GET은 일반적으로 없음, POST/PUT에 포함SQL 인젝션 페이로드가 들어가는 자리

3.2 응답 메시지

HTTP/1.1 200 OK
Date: Fri, 29 Mar 2024 10:30:00 GMT
Server: Apache/2.4.41 (Ubuntu)
Content-Type: text/html; charset=UTF-8
Content-Length: 1234
Connection: keep-alive

<!DOCTYPE html>
<html>
  <head><title>Example Page</title></head>
  <body><h1>Welcome</h1></body>
</html>

실제 브라우저 개발자 도구의 Network 탭에서 이 내용을 직접 확인할 수 있습니다.

3.3 주요 헤더 — 보안 관점

모든 헤더를 다 외우고, 그에 따른 취약점을 전부 이해하는 것은 현실적으로 불가능합니다. 보안 도구 개발자라면 자주 나오는 헤더 중 보안과 직결된 것들을 중심으로 익히는 전략이 필요합니다. 아래 표는 그런 헤더들을 정리한 것입니다. 별도로 날짜를 잡고 하나씩 AI와 공부해가며 실제 서버를 구축해 공부하는 것을 권해드립니다.

헤더설명보안 도구가 보는 이유
Server서버 소프트웨어·버전배너 그래빙 → CVE 매칭
X-Powered-By언어·프레임워크 (예: PHP/8.1)공격 표면 식별
Set-Cookie세션 쿠키 발급HttpOnly·Secure 플래그 점검
Content-Security-PolicyXSS 방어 정책우회 포인트 탐색
Strict-Transport-SecurityHTTPS 강제누락 시 MITM 취약
Access-Control-Allow-OriginCORS(Cross-Origin Resource Sharing, 서로 다른 출처 간 요청을 허용할지 정하는 브라우저 정책) 허용 출처* 남용 여부
Referer이전 페이지SSRF(Server-Side Request Forgery, 서버가 공격자가 지정한 URL로 대신 요청하도록 속이는 공격)·권한 우회 시 위조
User-Agent클라이언트 식별스캐너 차단 우회

4. HTTP 요청 메서드

HTTP 메서드는 단순히 "용도"를 넘어 서버가 요청을 어떻게 해석하고 인가할지 결정하는 단서입니다. 같은 엔드포인트라도 메서드가 바뀌면 서버 로직의 다른 분기가 열리고, 여기서 많은 취약점을 발견할 수 있습니다.

방어자 입장에서는 설계로 보안을 끌어올릴 수 있습니다. 예를 들어 원래 GET으로 충분한 조회 요청도, URL·접근 로그에 쿼리스트링이 남는 것을 피하려고 POST로 받도록 설계하기도 합니다. 공격자 입장에서는 반대로, 서버가 예상치 못한 메서드로 요청을 보내 인증·인가 로직을 우회하기도 합니다(예: GET만 막아둔 엔드포인트에 PUT·DELETE로 접근).

실무에서 흔히 발견되는 패턴은 "GET만 막아두고 PUT·DELETE는 열려 있는" 식의 메서드별 인가 불일치입니다. 특히 REST API를 뒤늦게 얹은 레거시 시스템이나, 프론트엔드 전용이라고 가정하고 메서드별 검증을 생략한 백엔드에서 자주 관찰됩니다. 공격자는 OPTIONS로 허용 메서드 목록을 뽑고, HEAD로 조용히 대상을 훑은 뒤, 권한 검증이 느슨한 메서드로 진입합니다.

방어자가 할 일은 단순합니다. 모든 메서드에 대해 동일한 인가 검증을 적용하고, 사용하지 않는 메서드는 명시적으로 405 Method Not Allowed를 반환하도록 설정하는 것입니다. 여기에 더해 GET으로 주고받던 민감한 조회 요청을 POST로 돌리면, 쿼리스트링이 URL·접근 로그·Referer·브라우저 히스토리에 잔존하는 문제를 피할 수 있습니다.

메서드용도보안 관점
GET리소스 취득 (쿼리스트링)URL·접근 로그에 민감 정보 잔존
POST리소스 생성·전송 (바디)로그인·인젝션 주요 진입점, 비멱등
PUT리소스 전체 교체인증 누락 시 임의 파일 업로드, 멱등
DELETE리소스 삭제인증 누락 시 데이터 파괴
PATCH리소스 일부 수정권한 검증 누락 빈발
HEAD헤더만 요청빠른 생존 확인·풋프린팅(초기 정보 수집)
OPTIONS지원 메서드 조회CORS 프리플라이트, 허용 메서드 노출 점검
TRACE경로 추적과거 XST(Cross-Site Tracing) 공격 진입점, 현재도 비활성화 권장
CONNECT프록시 터널링오픈 프록시 악용, 내부망 피봇팅

표에서 특히 눈여겨볼 것은 "서버가 어떤 메서드를 허용하는가"입니다. 많은 취약점은 "GET만 막아두고 PUT·DELETE는 열려 있는" 식의 메서드별 인가 불일치에서 시작됩니다. 공격자는 OPTIONS로 허용 메서드 목록을 뽑고, HEAD로 조용히 대상을 훑은 뒤, 권한 검증이 느슨한 메서드로 진입합니다.

HEAD + OPTIONS는 본격 스캔 전 "얌전한 풋프린팅" 도구의 단골 조합입니다. GET보다 트래픽이 작고, 서버가 어떤 메서드를 열어두었는지 빠르게 파악할 수 있어 IDS·WAF의 주의도 덜 끕니다.

5. HTTP 상태 코드

서버는 요청 처리 결과를 세 자리 숫자로 알려줍니다.

범주의미대표 코드
1xx정보100 Continue
2xx성공200 OK, 201 Created, 204 No Content
3xx리다이렉트301 Moved Permanently, 302 Found, 304 Not Modified
4xx클라이언트 오류400, 401 Unauthorized, 403 Forbidden, 404 Not Found, 405 Method Not Allowed
5xx서버 오류500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable

상태 코드는 거짓말을 합니다. 스캐닝 방어를 위해 404여야 할 페이지를 200으로 돌려주거나, 500 에러를 200 빈 페이지로 숨기는 운영이 흔합니다. 코드 하나로 판단하지 말고 본문 길이·응답 시간·타이틀을 함께 보세요.

5.1 파이썬으로 "거짓말하는 서버" 재현

# fake_404.py
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

class FakeHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/favicon.ico":
            self.send_response(204)
            self.end_headers()
            return

        body = b"<h1>Page found, but I say 404.</h1>"
        # 상태 라인은 404지만, 바디는 멀쩡히 페이지를 내려줍니다.
        # 상태 코드와 본문의 "의미"가 반드시 일치하지 않는다는 걸 눈으로 확인해보세요.
        self.send_response(404)
        self.send_header("Content-Type", "text/html")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

if __name__ == "__main__":
    with ThreadingHTTPServer(("", 8000), FakeHandler) as httpd:
        print("[+] serving on :8000 — try http://localhost:8000/")
        try:
            httpd.serve_forever()
        except KeyboardInterrupt:
            print("\n[+] shutting down")

서버를 띄우고 아래 두 방법 중 편한 쪽으로 확인해 보세요.

① 웹 브라우저로 직접 접근 — 주소창에 http://127.0.0.1:8000 을 입력. 개발자 도구(F12) → Network 탭에서 상태 코드는 404인데 페이지는 정상 렌더링되는 모순을 눈으로 확인할 수 있습니다.

② 파이썬 requests로 점검

아래 실습을 하기 전 pip install requests로 requests 라이브러리를 설치하세요.

# probe_fake_404.py
import requests

res = requests.get('http://127.0.0.1:8000', timeout=5)
print('status_code :', res.status_code)      # 404
print('reason      :', res.reason)           # 404 but actually 200?
print('body_length :', len(res.text))        # > 0
print('looks_like_200_page :', '<h1>' in res.text)

if res.status_code == 404 and len(res.text) > 100:
    print('[!] 코드 404인데 본문이 풍부함 — 거짓 404 의심')

이런 모순을 잡아내는 검사는 디렉터리 브루트포서(gobuster, ffuf 계열)가 내부적으로 수행하는 일입니다. 우리 스캐너도 상태 코드 하나만 믿지 않도록 같은 방식으로 설계합니다.

6. 파이썬으로 직접 HTTP 다루기

이제 개념을 코드로 옮깁니다. 파이썬은 네 가지 레벨의 HTTP 도구를 제공합니다.

레벨모듈용도
가장 낮음socket프로토콜 자체를 공부, 비표준 페이로드 실험
낮음http.server / http.client가벼운 실습 서버, 원시 요청 확인
중간urllib.request표준 라이브러리로 간단 요청
실무requests / httpx대부분의 보안 도구가 사용

6.1 socket으로 바닥부터 쌓는 HTTP 서버

# raw_http_server.py — HTTP가 결국 TCP 위의 "텍스트 약속"임을 체감하는 예제
import socket

CRLF = "\r\n"

def handle_request(request: str) -> bytes:
    # 요청 라인 = "GET /index.html HTTP/1.0"
    request_line = request.split(CRLF, 1)[0] if CRLF in request else request.split("\n", 1)[0]
    try:
        method, target, version = request_line.split()
    except ValueError:
        body = b"Bad Request"
        return (
            f"HTTP/1.0 400 Bad Request{CRLF}"
            f"Content-Type: text/plain{CRLF}"
            f"Content-Length: {len(body)}{CRLF}"
            f"Connection: close{CRLF}{CRLF}"
        ).encode() + body

    # 쿼리스트링은 잘라냄 (예: /search?q=admin → /search)
    path = target.split("?", 1)[0]
    if path == "/":
        path = "/index.html"

    # ⚠ 의도적으로 경로 검증을 생략한 "취약한" 서버입니다.
    #    /../../etc/passwd 같은 요청으로 프로세스가 읽을 수 있는 임의 파일이 노출됩니다.
    #    뒤 장의 "경로 트래버설"에서 이 점을 직접 실습·완화합니다.
    try:
        with open(path[1:], "rb") as f:
            body = f.read()
        status = "200 OK"
    except FileNotFoundError:
        body = b"File Not Found"
        status = "404 Not Found"

    headers = (
        f"HTTP/1.0 {status}{CRLF}"
        f"Content-Type: text/html; charset=utf-8{CRLF}"
        f"Content-Length: {len(body)}{CRLF}"
        f"Connection: close{CRLF}{CRLF}"
    ).encode()
    return headers + body


def main():
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
        server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server.bind(("localhost", 8888))
        server.listen(1)
        print("[+] raw server ready on :8888 (Ctrl+C to stop)")
        while True:
            conn, _ = server.accept()
            with conn:
                req = conn.recv(4096).decode("utf-8", errors="replace")
                # recv는 한 번만 호출 — 요청이 4096바이트를 넘으면 잘립니다.
                # 실습 목적상 충분하지만, 실제 서버는 루프로 끝까지 읽어야 합니다.
                print(req)
                conn.sendall(handle_request(req))


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\n[+] shutting down")

이 코드가 중요한 이유는 HTTP/1.0 200 OK\n\n...이라는 문자열 규칙 하나만 지키면 브라우저가 응답을 해석한다는 사실을 확인할 수 있기 때문입니다.

6.2 정적 파일 서버

이 코드는 아주 단순한 정적 파일 서버입니다.

  1. 브라우저가 GET / HTTP/1.1을 보내면, headers[0].split()[1]이 경로 /를 꺼냅니다.
  2. 경로가 /이면 /index.html로 바꿉니다.
  3. 맨 앞 /를 떼어내 index.html을 open() 합니다. 이때 경로는 절대경로가 아니라 스크립트를 실행한 현재 작업 디렉터리(CWD) 기준입니다.
  4. 해당 파일이 없으면 FileNotFoundError가 나고, "HTTP/1.0 404 NOT FOUND\n\nFile Not Found"를 응답합니다.

6.3 실습

그냥 실행만 하면 브라우저에 "File Not Found"가 뜹니다. 버그가 아니라 스크립트를 실행한 폴더에 index.html이 없기 때문입니다. 같은 폴더에 파일을 하나 만들어두고 다시 접속해 보세요.

# 스크립트 실행 폴더에서
echo "<h1>Hello from raw socket</h1>" > index.html
python raw_http_server.py

이제 http://localhost:8888/ 에 접속하면 200 OK와 함께 페이지가 보입니다. foo.txt, bar.html 같은 파일을 같은 폴더에 두고 http://localhost:8888/foo.txt 식으로 요청하면 그것도 그대로 서빙됩니다.

서버 터미널에는 print(req) 덕분에 브라우저가 보낸 요청 원문이 찍히는데, 접속 한 번에 보통 두 줄씩 올라옵니다. 두 번째는 브라우저가 자동으로 가져가는 GET /favicon.ico이며, 이 파일이 없으니 404가 나는 게 정상입니다.

6.4 일부러 규칙 깨 보기

이 서버가 지키는 유일한 규칙은 "상태 라인 + 빈 줄 + 바디"입니다. 반대로 이 규칙을 일부러 깨면 브라우저가 어떻게 반응하는지 실험해 보세요. 이렇게 규칙을 깨보는 과정을 퍼징(Fuzzing)이라고 합니다. 보안 도구는 결국 "조금씩 변형된 요청을 보내면서 응답의 차이를 관찰하는" 프로그램이므로, 퍼징 감각을 키우는 것이 중요합니다.

  • 'HTTP/1.0 200 OK\n\n' → 'HTTP/1.0 200 OK\n' — 빈 줄을 하나 줄입니다. 브라우저는 "헤더가 아직 안 끝났다"고 판단해 응답을 계속 기다리다 멈춥니다.
  • 'HTTP/1.0 200 OK' → 'HTTP/9.9 200 OK'나 'WAT/1.0 200 OK' — 버전 문자열을 망가뜨리면 브라우저별로 반응이 다릅니다.

이처럼 프로토콜 규칙을 미세하게 변형하면서 상대방의 반응을 관찰하는 것이 바로 퍼징(Fuzzing) 감각의 출발점입니다.

보안 주의 — 디렉터리 트래버설(Path Traversal) — 이 코드는 요청 경로를 그대로 파일 이름으로 사용하기 때문에, 예를 들어 http://localhost:8888/../../etc/passwd 같은 경로가 들어오면 상위 폴더의 민감한 파일까지 열려고 시도합니다. 공격 중 빈도가 높은 공격이기도 합니다.

6.5 http.server로 만드는 실습용 서버

socket을 매번 직접 다루면 피곤합니다. 표준 라이브러리 http.server는 GET/POST 분기, 헤더 파싱, 스레드까지 대부분 제공합니다.

import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer


class EchoHandler(BaseHTTPRequestHandler):
    def _raw(self) -> str:
        raw = f"{self.command} {self.path} {self.request_version}\n"
        raw += str(self.headers)
        length = self.headers.get("Content-Length")
        if length:
            body = self.rfile.read(int(length)).decode("utf-8", errors="replace")
            raw += f"\n{body}"
        return raw

    def _send_json(self, payload: dict):
        body = json.dumps(payload).encode()
        body = b"<h1>hi</h1>"
        self.send_response(200)
        # send_header에 Content-Type을 3가지 타입으로 모두 보내보세요.
        # 같은 바디가 클라이언트에서 어떻게 다르게 해석되는지 확인해보세요.
        # 1. text/plain        → 문자열 그대로 출력 (위 body를 b"<h1>hi</h1>"로 바꿔보세요)
        # 2. application/json  → 브라우저가 JSON 뷰어로 포매팅 (기본 body 그대로)
        # 3. text/html         → HTML로 렌더링 (위 body를 b"<h1>hi</h1>"로 바꿔보세요)
        self.send_header("Content-Type", "text/html")
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Access-Control-Allow-Origin", "*")
        self.end_headers()
        self.wfile.write(body)

    def do_GET(self):
        if self.path == "/favicon.ico":
            self.send_response(204)  # No Content
            self.end_headers()
            return
        print("[GET]\n" + self._raw() + "\n")
        self._send_json({"message": "ok"})

    def do_POST(self):
        print("[POST]\n" + self._raw() + "\n")
        self._send_json({"message": "received"})


if __name__ == "__main__":
    with ThreadingHTTPServer(("", 8000), EchoHandler) as httpd:
        print("[+] serving on :8000")
        httpd.serve_forever()

서버를 띄운 뒤 브라우저 주소창에 http://localhost:8000/search?q=admin 을 입력하거나, 아래 폼 파일을 더블클릭해 POST를 보내봅니다. 터미널에 요청 라인 + 헤더 + 빈 줄 + 바디가 그대로 찍히는 것을 확인하세요.

<!-- post_test.html — 파일 더블클릭으로 열기 -->
<form action="http://localhost:8000" method="post">
  <input type="text" name="username" value="admin" />
  <input type="password" name="password" value="' OR 1=1 --" />
  <input type="submit" value="Login" />
</form>

바이브 코딩으로 직접 해보세요

  1. 지금 서버는 GET 쿼리스트링이나 POST 본문의 값을 파싱해서 예쁘게 출력하진 않습니다. Claude에게 "이 서버 코드에서 GET의 q 파라미터와 POST의 username, password 값을 따로 꺼내서 터미널에 출력하도록 수정해줘"라고 부탁해 보세요
  2. _send_json 메서드의 Content-Type을 text/plain, application/json, text/html로 바꿔가며 브라우저에서 같은 바디가 어떻게 다르게 보이는지 실험해 보세요. 필요하면 Claude에게 "세 가지 Content-Type을 각각 /plain, /json, /html 경로로 나눠서 응답하는 서버로 고쳐줘"라고 요청해도 좋습니다
  3. 파이썬 requests로 위 서버에 요청을 보내는 클라이언트를 직접 작성해보세요. GET, POST 폼, POST JSON 세 가지를 모두 시도하면 Content-Type에 따라 서버 터미널 출력이 어떻게 달라지는지 한눈에 들어옵니다

6.6 requests로 클라이언트 만들기

앞서 서버를 만들어 봤지만, 실무 보안 도구는 대부분 클라이언트입니다. "요청을 보내고 응답을 읽는" 일만 반복하면 되지요. 이 역할은 requests 라이브러리 하나면 충분합니다.

requests는 파이썬에서 가장 널리 쓰이는 HTTP 클라이언트로, 세션·쿠키·인증·JSON 변환까지 전부 내장하고 있어 스캐너·크롤러·API 테스터의 기본기가 됩니다.

pip install requests

6.7 첫 GET 요청

아래 URL은 크롤링 연습 전용으로 공개된 페이지라, 마음 놓고 반복 요청해도 괜찮습니다. 실제 서비스는 허락 없이 이렇게 두드리면 안 됩니다(6.10 끝의 법·윤리 주의 참고).

import requests

res = requests.get('https://paullab.co.kr/stock.html')
print(res.status_code)                  # 200
print(res.headers.get('Content-Type'))  # text/html; charset=UTF-8
print(res.text[:200])                   # HTML 첫 200자만 살짝

응답 객체 res에는 많은 정보가 들어 있습니다. 자주 쓰는 것만 먼저 익혀두세요.

속성/메서드의미
res.status_codeHTTP 상태 코드 (200, 404, 500 …)
res.reason상태 문구 ("OK", "Not Found")
res.text응답 본문(문자열)
res.content응답 본문(바이트 — 이미지·PDF 받을 때)
res.json()본문을 JSON으로 파싱해 파이썬 딕셔너리로
res.headers응답 헤더 딕셔너리
res.cookies서버가 내려준 쿠키
res.url리다이렉트를 따라간 최종 URL
res.history리다이렉트 체인
res.elapsed응답까지 걸린 시간 — 블라인드 SQLi·타이밍 공격에 유용

6.8 쿼리 스트링·헤더 바꾸기

?q=phone&page=1 같은 쿼리 스트링은 params=에 딕셔너리로 넘기면 requests가 인코딩을 알아서 해줍니다. 헤더도 headers=로 덮어 씁니다.

import requests

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
                  '(KHTML, like Gecko) Chrome/120.0 Safari/537.36',
    'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8',
}
params = {'q': 'phone', 'page': 1}
res = requests.get('https://dummyjson.com/products/search', params=params, headers=headers)
print(res.url)       # https://dummyjson.com/products/search?q=phone&page=1
print(res.json())    # {"products": [...], "total": 23, ...}

보안 도구 관점 — requests의 기본 User-Agent는 python-requests/2.x입니다. 많은 방화벽·WAF가 이걸 자동화 트래픽으로 인식해 차단합니다. User-Agent·Referer 위조는 "탐지 회피" 기법인 동시에, 방어 측에서는 "왜 python-requests 기본 UA로 들어오는 트래픽이 있지?"를 찾아내는 탐지 시그널이기도 합니다. 나중에 SQL 인젝션 스캐너를 만들 때는 params 값에 admin' OR 1=1 -- 같은 페이로드를 대입하며 응답이 어떻게 달라지는지 관찰하는 식으로 확장합니다.

6.9 REST API 실습 — 블로그 CRUD

이 책을 위한 실습용 블로그 API를 하나 준비해 뒀습니다. 자유롭게 CRUD(Create·Read·Update·Delete)를 해볼 수 있는 환경입니다.

  • 베이스 URL: https://dev.wenivops.co.kr/services/fastapi-crud/1
  • 엔드포인트: /blog, /blog/{id}, /signup, /login

먼저 조회와 작성만 해봅니다. JSON 바디를 보낼 때는 json=이 가장 편합니다. 파이썬 딕셔너리를 알아서 JSON 문자열로 바꿔주고, Content-Type: application/json 헤더까지 자동으로 붙여줍니다.

import requests

BASE = 'https://dev.wenivops.co.kr/services/fastapi-crud/1'

# ① 목록 조회 (GET)
res = requests.get(f'{BASE}/blog', timeout=5)
blogs = res.json()
print(f'총 {len(blogs)}개의 게시글')

# ② 작성 (POST)
new_blog = {
    'title': 'requests 라이브러리 실습',
    'content': 'POST로 게시글을 올려봤습니다.',
}
res = requests.post(f'{BASE}/blog', json=new_blog, timeout=5)
print(res.status_code, res.json())

data= vs json= — data=dict는 폼 전송(application/x-www-form-urlencoded)으로, HTML <form> 제출과 동일합니다. json=dict는 JSON 바디로, 최근 REST API의 표준입니다. 서버가 어떤 형식을 기대하는지 문서를 먼저 확인하세요.

바이브 코딩으로 직접 해보세요

  1. 위 코드를 토대로 수정(PUT)과 삭제(DELETE)를 추가해 블로그 CRUD를 완성해 보세요. URL은 /blog/{id} 형식이고, PUT은 바꿀 내용을 json=으로 통째로 보내면 됩니다. Claude에게 "방금 만든 게시글의 id를 받아서 제목을 수정한 뒤 삭제까지 하는 스크립트를 작성해줘"라고 부탁해 보세요
  2. 이 API에는 /signup과 /login이 있습니다. 로그인에 성공하면 access_token이 내려옵니다. Claude에게 "회원가입 → 로그인 → 받은 토큰을 Authorization: Bearer ... 헤더에 실어 /blog를 호출하는 흐름을 짜줘"라고 요청해 직접 토큰 인증을 구현해 보세요
  3. PUT·DELETE는 인증을 깜빡한 API에서 가장 자주 치명적 취약점이 터지는 메서드입니다. 스캐너를 만든다면 OPTIONS로 서버가 허용한 메서드를 먼저 훑고, 인증 헤더 없이도 PUT/DELETE가 성공하는지 점검하는 로직을 넣으면 좋습니다. 이 점검 로직을 Claude와 함께 설계해 보세요

6.10 세션과 자주 쓰는 옵션

같은 서버에 여러 번 요청할 때 매번 headers=, cookies=를 넘기긴 번거롭습니다. Session은 로그인 쿠키·공통 헤더를 자동으로 유지해주고, TCP 커넥션도 재활용해서 속도까지 빨라집니다.

import requests

session = requests.Session()
session.headers.update({
    'User-Agent': 'BasecampSecurity/0.1',
    'Accept': 'application/json',
})

# 같은 Session으로 보낸 요청끼리는 헤더·쿠키가 공유됩니다
session.get('https://paullab.co.kr/stock.html', timeout=5)
session.get('https://dev.wenivops.co.kr/services/fastapi-crud/1/blog', timeout=5)

보안 도구가 자주 쓰는 옵션도 몇 개만 눈에 익혀두세요. 실제 코드는 뒤 장에서 스캐너를 만들면서 자연스럽게 등장합니다.

옵션용도
allow_redirects=False302 + Location 헤더를 직접 보고 싶을 때 (로그인 리다이렉트 분석)
proxies={'http': ...}Burp Suite·mitmproxy로 트래픽을 가로채고 싶을 때
timeout=(3, 5)(연결, 읽기) 초 단위 — 안 주면 응답 없는 서버에서 스캐너가 영원히 멈춥니다
verify=False자체 서명 인증서를 무시. 실습 환경에서만

바이브 코딩으로 직접 해보세요 — URL 목록을 받아 각 URL의 상태 코드·응답 시간·최종 URL(리다이렉트 추적)을 CSV로 정리하는 간단한 수집기를 Claude와 함께 작성해 보세요. 프롬프트는 7.2에서 예시로 제공합니다.

법·윤리 주의 — requests로 외부 서비스에 반복 요청을 보내는 순간, 이미 정보 수집 행위가 시작됩니다. 본인이 소유하거나 명시적 허가를 받은 자산, 혹은 이 책에서 안내한 실습용 엔드포인트(paullab.co.kr/stock.html, dev.wenivops.co.kr/.../fastapi-crud/..., dummyjson.com)에만 사용하세요. 자세한 법령·윤리는 5장에서 다룹니다.

6.11 응답과 요청의 전체 흐름

요청이 브라우저를 떠나 서버에 닿았다가 돌아오는 과정은 다음과 같습니다. 모든 계층에서 헤더가 붙고, 반대쪽에서 하나씩 벗겨집니다.

이 그림을 머릿속에 넣어두면, 앞으로 만들 포트 스캐너·배너 그래버·웹 퍼저가 어느 계층에서 무엇을 건드리는지 구분할 수 있게 됩니다.

7. Claude와 함께하는 바이브 코딩

이 책의 핵심은 "문법 암기"가 아니라 "AI와 함께 보안 도구를 설계·생성·검증하는 루프"를 익히는 것입니다. 이번 절에서는 좋은 프롬프트의 축과 예시만 짧게 보고, 실제 도구는 3장부터 본격적으로 만듭니다.

7.1 좋은 프롬프트의 네 축

  1. 역할 — "너는 파이썬 보안 도구 개발자야."
  2. 맥락 — OS, 파이썬 버전, 허용 라이브러리, 실행 환경.
  3. 요구사항 — 입력/출력, 에러 처리, 로그 포맷, 성능 기준.
  4. 출력 형식 — "파이썬 코드 블록 하나 + 3줄 이내 동작 요약."

7.2 프롬프트 예시 — HTTP 상태 코드 수집기

6.10 끝에서 언급한 수집기를 Claude에게 맡기고 싶다면, 대략 이런 틀로 요청합니다.

너는 파이썬 보안 도구 개발자야.
다음 요구사항으로 스크립트를 작성해줘.

요구사항:
- 파이썬 3.11, 외부 라이브러리는 requests만 허용
- 입력: urls.txt (한 줄에 URL 하나)
- 처리: 각 URL에 GET (timeout=5, allow_redirects=True)
- 출력: results.csv — url, status_code, elapsed_ms, final_url, title
- 실패(타임아웃, DNS, SSL)는 status_code 대신 에러 분류 문자열
- 동시 요청 수 10 (concurrent.futures.ThreadPoolExecutor)
- User-Agent: "BasecampSecurity/0.1"

출력 형식:
1) ```python 코드 블록
2) 이어서 3줄 이내로 동작 요약

생성된 코드를 그대로 받아들이지 않는 것이 이 책의 핵심 태도입니다. 타임아웃이 실제로 동작하는지, CSV가 Excel에서 깨지지 않는지, 에러 분류가 충분히 세분화되어 있는지를 직접 실행하며 검증하고, 부족한 부분은 에러 메시지 그대로 Claude에 되먹여 고쳐가세요. 이 루프가 바로 바이브 코딩입니다.

네트워크 프로그래밍 기초 - AI 네이티브 모의해킹 베이스캠프 | 위니버시티