📚 네트워크 · 패킷 분석 › 02. 포트 · 프로토콜 분석 — 61편
이전 글: 60. TCP Connection과 Port · 다음 글: 62. HTTPS와 TLS Handshake — 암호화된 통신에서 볼 수 있는 것
참고(01. TCP/IP 구조 이해): 50. 웹 요청 하나의 전체 경로 — 01 시리즈 종합 — DNS·TCP·HTTP가 이어지는 전체 흐름
보안관제에서 가장 많이 보는 로그 중 하나가 웹 접근 로그와 웹 방화벽(WAF)·IDS의 HTTP 이벤트입니다. SQL Injection, 경로 조작, 웹셸 접근 같은 웹 공격은 대부분 HTTP 요청 안에 흔적을 남깁니다.
그런데 로그는 HTTP 메시지 전체가 아니라 일부 필드만 골라 기록한 결과입니다. HTTP 메시지 구조를 알아야 다음 질문에 답할 수 있습니다.
"GET /index.html HTTP/1.1"은 메시지의 어느 부분인가X-Forwarded-For의 IP를 믿어도 되는가이번 글은 HTTP/1.1 메시지를 기준으로 구조를 정리하고, 그 구조가 로그의 어느 필드로 남는지 연결합니다. 메서드·상태코드별 이상징후는 다음 글에서 다룹니다.

그림 1. 요청 줄·상태 줄 → 헤더 → 빈 줄 → 본문 순서
| 부분 | 요청(Request) | 응답(Response) |
|---|---|---|
| 시작줄 | 요청줄: 메서드 경로 버전 (예: GET /login HTTP/1.1) | 상태줄: 버전 상태코드 설명 (예: HTTP/1.1 200 OK) |
| 헤더 | 이름: 값 형식 여러 줄 | 이름: 값 형식 여러 줄 |
| 빈 줄 | 헤더의 끝 (\r\n) | 헤더의 끝 |
| 본문 | POST·PUT 등의 전송 데이터 (없을 수 있음) | HTML, JSON, 파일 등 |
HTTP/1.1은 사람이 읽을 수 있는 텍스트 형식이며, 줄바꿈은 CR LF(\r\n)입니다.
| 헤더 | 의미 | 관제 포인트 |
|---|---|---|
Host | 요청 대상 도메인 (HTTP/1.1 필수) | 한 IP에 여러 사이트가 있을 때 어느 사이트인지 구분. IP로 직접 접근한 요청은 자동화 도구일 가능성 |
User-Agent | 클라이언트 프로그램 정보 | 스캐너·스크립트 식별 단서. 클라이언트가 임의로 바꿀 수 있음 |
Referer | 이전 페이지 주소 | 정상 사용자 흐름 여부 판단 |
Cookie | 세션 정보 | 세션 탈취 조사 시 중요, 로그에는 보통 남지 않음 |
Content-Type / Content-Length | 본문 형식·길이 | 파일 업로드(multipart/form-data) 식별 |
X-Forwarded-For | 프록시·로드밸런서가 붙이는 원래 클라이언트 IP | 신뢰할 수 있는 프록시가 붙인 값만 의미 있음. 클라이언트가 직접 넣을 수도 있음 |
| 헤더 | 의미 | 관제 포인트 |
|---|---|---|
Server | 서버 소프트웨어 정보 | 버전 노출은 정보 수집에 이용될 수 있음 |
Set-Cookie | 세션 발급 | 로그인 성공 여부 판단 단서 |
Location | 리다이렉트 대상 | 3xx 응답과 함께 이동 경로 확인 |
Content-Length | 응답 크기 | 로그의 응답 바이트와 연결, 비정상적으로 큰 응답 확인 |
| 버전 | 형식 | 전송 계층 | 관제 시 의미 |
|---|---|---|---|
| HTTP/1.1 | 텍스트 | TCP | 평문이면 패킷에서 그대로 읽힘 |
| HTTP/2 | 바이너리 프레임 | TCP (실제로는 대부분 TLS 위) | 패킷에서 텍스트로 보이지 않음 |
| HTTP/3 | 바이너리 프레임 | UDP 기반 QUIC | 443/UDP로 나타남 |
버전이 달라도 메서드·경로·헤더·상태코드라는 의미 구조는 같습니다. 웹 서버 로그에는 버전과 상관없이 같은 형식으로 기록됩니다.
요청 하나가 로그 한 줄이 되기까지의 흐름입니다.
[브라우저]
GET /board/list?page=2 HTTP/1.1 ← 요청줄
Host: www.example.com ← 헤더
User-Agent: Mozilla/5.0 ...
Referer: http://www.example.com/
(빈 줄)
↓ TCP 연결(80) 위로 전송
[웹 서버: nginx]
요청 처리 → 응답 생성
HTTP/1.1 200 OK ← 상태줄
Content-Type: text/html
Content-Length: 5120
(빈 줄)
<html>... ← 본문
↓ 응답 전송 후 로그 기록
[access.log 한 줄]
클라이언트IP - - [시각] "요청줄" 상태코드 응답바이트 "Referer" "User-Agent"
nginx의 기본 combined 로그 형식은 다음 변수로 구성됩니다.
$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"
여기서 $request가 요청줄 전체이고, 요청 본문은 포함되지 않습니다. Apache의 combined 형식(%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i")도 같은 구조입니다. 단, Rocky Linux 패키지의 nginx.conf는 combined 뒤에 "$http_x_forwarded_for"를 덧붙인 main 형식을 기본으로 쓰므로, 로그 끝에 필드가 하나 더 있을 수 있습니다(/etc/nginx/nginx.conf의 log_format에서 확인).
실습 환경: 본인 소유 Linux VM (Rocky Linux 또는 Ubuntu). 인터페이스 이름은 ens33을 예시로 사용합니다.
# Rocky Linux
sudo dnf install -y nginx curl tcpdump
sudo systemctl enable --now nginx
sudo firewall-cmd --add-service=http --permanent && sudo firewall-cmd --reload
# Ubuntu (설치 시 자동 시작)
sudo apt install -y nginx curl tcpdump
| 항목 | nginx (Rocky·Ubuntu 공통) | Apache 참고 (Rocky: httpd / Ubuntu: apache2) |
|---|---|---|
| 접근 로그 | /var/log/nginx/access.log | /var/log/httpd/access_log / /var/log/apache2/access.log |
| 에러 로그 | /var/log/nginx/error.log | /var/log/httpd/error_log / /var/log/apache2/error.log |
curl -v http://127.0.0.1/ -o /dev/null
-v 출력에서 >로 시작하는 줄이 요청, <로 시작하는 줄이 응답 헤더입니다.
curl -s -o /dev/null -A "lab-test-agent" -e "http://ref.example/" "http://127.0.0.1/?q=hello"
curl -s -o /dev/null -X POST -d "user=test&memo=secret" http://127.0.0.1/
sudo tail -n 2 /var/log/nginx/access.log
📷 [실습 화면 삽입]
curl -v의 요청(>)·응답(<) 헤더 출력 화면
다른 PC(또는 호스트)에서 VM의 IP로 접속하면서 VM에서 캡처합니다.
sudo tcpdump -ni ens33 -A 'tcp port 80'
패킷 필드 단위 해석은 03. Wireshark 패킷 분석 시리즈에서 다룹니다.
형식 예시(값은 환경마다 다름):
$ curl -v http://127.0.0.1/ -o /dev/null
> GET / HTTP/1.1
> Host: 127.0.0.1
> User-Agent: curl/8.x.x
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.x.x
< Content-Type: text/html
< Content-Length: 615
nginx 접근 로그 형식 예시(값은 환경마다 다름):
127.0.0.1 - - [26/Sep/2026:14:02:11 +0900] "GET /?q=hello HTTP/1.1" 200 615 "http://ref.example/" "lab-test-agent"
127.0.0.1 - - [26/Sep/2026:14:02:15 +0900] "POST / HTTP/1.1" 405 157 "-" "curl/8.x.x"
| 로그 필드 | HTTP 메시지에서의 출처 |
|---|---|
127.0.0.1 | HTTP가 아니라 TCP 연결의 상대 IP |
"GET /?q=hello HTTP/1.1" | 요청줄 (쿼리 스트링 포함) |
200, 405 | 응답 상태줄의 상태코드 (정적 페이지에 POST 시 nginx는 405 응답) |
615 | 응답 본문 바이트 수 |
"http://ref.example/", "lab-test-agent" | 요청 헤더 Referer, User-Agent |
| (없음) | POST 본문 user=test&memo=secret → 접근 로그에 기록되지 않음 |
-A, -e로 바꾼 헤더가 로그에 그대로 남는 것을 확인했다-A로 읽히는 것을 확인했다📷 [실습 화면 삽입]
access.log에 조작한 User-Agent·Referer가 기록된 화면
HTTP 구조 관점에서 로그를 볼 때의 판단 기준입니다.
| 관찰 | 정상일 수 있는 경우 | 의심해야 하는 경우 |
|---|---|---|
Host가 IP 주소이거나 비어 있음 | 모니터링 시스템의 헬스체크 | 외부에서 IP 직접 접근 + 다양한 경로 요청 (자동화 탐색) |
User-Agent가 curl, python-requests 등 | 사내 스크립트, API 연동 | 로그인·관리자 경로에 반복 요청 |
User-Agent에 SQL 구문·스크립트 문자열 | — | 헤더를 통한 주입 시도 |
요청줄에 ../, %2e%2e, ', <script> | 드물게 정상 검색어 | 경로 조작·SQLi·XSS 시도 (응답 코드와 함께 판단) |
X-Forwarded-For 값이 로그마다 다른 사설 IP | 사내 프록시 경유 | 프록시가 없는 구간인데 존재 → 클라이언트가 임의로 넣은 값 |
요청줄이 HTTP 형식이 아님 (로그에 400과 깨진 문자열) | 다른 프로토콜 클라이언트의 오접속 | 80 포트에 TLS·기타 프로토콜을 보내는 탐색 |
로그에서 빠르게 확인하는 명령 예시입니다.
# User-Agent 상위 10개 (combined 형식에서 따옴표 기준 6번째 필드)
sudo awk -F'"' '{print $6}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
# 요청줄에 상위 경로 이동 문자열이 포함된 요청
sudo grep -E '\.\./|%2e%2e' /var/log/nginx/access.log
| 로그·장비 | 볼 수 있는 HTTP 정보 | 한계 |
|---|---|---|
| 웹 서버 접근 로그 | 요청줄, 상태코드, 크기, 일부 헤더 | 본문·쿠키는 기본 미기록 |
| WAF | 요청 본문까지 검사, 차단 룰 | 설정된 룰 범위만 탐지 (06. 방화벽 · IDS/IPS 분석 시리즈에서 다룸) |
| 네트워크 IDS | 평문 HTTP 전체 | HTTPS는 복호화 없이는 내용 확인 불가 (06편에서 다룸) |
| 프록시 로그 | 내부 사용자의 외부 접속 URL | 프록시를 거치지 않는 통신은 누락 |
한계와 오탐 주의
User-Agent, Referer, X-Forwarded-For는 클라이언트가 조작할 수 있는 값입니다. 판단 근거로 쓰되 단독 근거로 쓰지 않습니다.$remote_addr가 프록시 IP로 남습니다. 원래 클라이언트 IP가 어느 필드에 남는지 구조를 먼저 확인합니다."GET /... HTTP/1.1"은 요청줄이고, 상태코드·크기는 응답에서, Referer·User-Agent는 요청 헤더에서 옵니다.User-Agent, X-Forwarded-For 등 헤더 값은 조작 가능하므로 단독 판단 근거로 쓰지 않습니다.