콘텐츠로 이동

2.5강 — 웹 서버 로그: 형식, 읽기, 공격의 흔적

이번 회차의 범위

  • 웹 서버 로그가 조사에서 특별한 이유: HTTPS라도 서버 로그는 평문이다
  • 자주 만나는 형식: NCSA Common, Common+VHost, Combined, Apache mod_log_forensic, IIS의 형식들
  • 필드 하나씩 읽기, 그리고 직접 돌려 본 Apache 2.4.58로 확인한 함정들: 시각은 받은 시각이라 줄 순서가 뒤바뀐다, 크기 필드는 배포판마다 뜻이 다르다, 가상 호스트 필드는 요청한 이름이 아니다, 실패한 로그인도 사용자 이름이 남는다
  • [정정] "W3C Extended"와 "Combined"는 다른 형식이다, 출력할 수 없는 글자는 +가 아니라 \xhh로 남는다
  • 셸로 분석하기: 교재의 명령 네 가지를 실제 로그에 돌려 보기
  • 공격의 흔적: 정찰, SQL 주입 시도, 원격 관리 도구

실습 환경과 표기

**(직접 실행 결과)**는 이 작업 환경에 설치한 **Apache 2.4.58(Ubuntu 패키지)**로 얻었습니다. 연습용 가상 호스트(www.lab.example, 별칭 portal.lab.example)를 8080번 포트에 만들고, 같은 요청을 여러 로그 형식으로 동시에 남기도록 설정했습니다. 검색 기능을 흉내 내는 CGI는 따옴표가 들어오면 일부러 500을 돌려주도록 만든 연습용 스크립트이고, 실제 데이터베이스는 없습니다. 요청은 모두 같은 컴퓨터의 curl로 보냈기 때문에 출발지가 127.0.0.1로 찍혀 있습니다. IIS는 이 환경에서 돌릴 수 없어 문서로만 확인했습니다.

1. 웹 서버 로그가 특별한 이유

웹 서버 로그는 웹 서버만큼 오래됐습니다. 원래 목적은 지금도 같아요. 사이트 사용량 보고(얼마나 전송했나, 몇 페이지를 보여 줬나), 그리고 서버와 내용의 오류를 고치기 위한 기록입니다. 그런데 이 로그가 웹 서버가 얽힌 사건에서는 가장 중요한 증거가 되곤 합니다. 자동·수동 정찰부터 공격 자체, 장악의 순간(SQL 주입 같은), 장악 이후의 작업까지 한 줄씩 남기 때문입니다. 원격 관리 도구(RAT)를 쓰는 공격이라면, 공격자의 모든 명령이 요청 한 줄씩으로 남는 경우도 있어요.

1.3강의 프록시 로그와 비교하면, 프록시 로그는 우리 직원이 바깥으로 나간 기록이고, 웹 서버 로그는 바깥 사람이 우리 서버로 들어온 기록입니다. 같은 HTTP를 반대편에서 본 것이죠.

그리고 웹 서버 로그에는 큰 장점이 하나 있습니다. HTTPS 요청도 평문으로 남는다는 것입니다. 웹 서버는 TLS의 끝점이라, 요청을 복호화한 뒤에 로그를 쓰고, 응답을 암호화하기 전에 결과를 압니다(1.12강의 "연결의 한쪽 끝"). 패킷 센서는 암호화된 바이트만 봤더라도, 서버 로그에는 메서드, 경로, 쿼리, 상태 코드가 그대로 있어요. 조사자가 가진 웹 활동의 유일한 창이 이런 로그일 때가 많습니다. 웹 서버, 캐시 프록시, 그리고 2.8강의 Zeek 같은 네트워크 보안 모니터링 도구가 남긴 로그요. 그 로그들은 모두 네트워크 통신에서 만들어진 것이니, 2.1~2.4강에서 패킷으로 익힌 것이 로그 분석에도 그대로 쓰입니다.

2. 자주 만나는 형식

웹 서버 소프트웨어는 많아도 로그 형식은 몇 가지로 모입니다. 대부분의 서버가 표준 형식을 선택지로 주고, 형식을 직접 정하는 기능(템플릿)도 있어요.

서버 주요 형식
Apache NCSA Common, Common+VHost, Combined, 별도의 Referer·User-Agent 로그, 형식 문자열로 직접 정의, mod_log_forensic
IIS W3C 확장 형식(기본값), IIS 형식, NCSA Common, 중앙 집중식 이진 로그(CBL), ODBC(데이터베이스)
nginx combined(기본으로 정의된 형식), log_format으로 직접 정의

nginx는 교재에 없지만 지금 아주 많이 쓰여서 함께 적었습니다. nginx의 기본 combined 형식은 Apache의 Combined와 같은 순서의 필드를 씁니다.

3. NCSA Common: 기본 일곱 필드

가장 기본이 되는 형식은 웹 기술의 많은 부분을 만든 NCSA(미국 국립 슈퍼컴퓨팅 응용 센터)가 정한 Common 형식입니다. Apache의 형식 문자열로는 %h %l %u %t "%r" %>s %b예요. 실습 서버가 남긴 줄을 봅니다.

(직접 실행 결과)

127.0.0.1 - jdoe [05/Nov/2026:07:03:23 +0000] "GET /admin/ HTTP/1.1" 200 11
필드 형식 문자열 이 줄의 값 뜻
1 %h 127.0.0.1 요청한 쪽의 주소(설정에 따라 호스트 이름)
2 %l - 원격 사용자 이름(identd). 지금은 거의 항상 -
3 %u jdoe HTTP 인증(Basic 등)으로 들어온 사용자 이름. 폼 로그인은 해당 없음
4 %t [05/Nov/2026:07:03:23 +0000] 요청을 받은 시각과 시간대
5 "%r" "GET /admin/ HTTP/1.1" 요청 줄 그대로: 메서드, 요청 대상(쿼리 포함, 호스트 이름 없음), 버전
6 %>s 200 최종 상태 코드(내부 리디렉션 뒤의 값)
7 %b 11 응답 본문의 바이트 수(헤더 제외). 0이면 -

없는 값은 -로 채웁니다. 1.3강의 Squid 로그처럼 칸 위치로 읽을 수 있어서 awk로 다루기 좋아요. 이제 이 일곱 필드에 숨은 함정을 직접 확인합니다.

3.1 시각은 "받은" 시각, 줄은 "끝난" 순서

실습에서 3초 걸리는 요청(/cgi-bin/slow.cgi)을 먼저 보내고, 그 사이에 다른 요청들을 보냈습니다.

(직접 실행 결과, 앞뒤 줄만)

127.0.0.1 - - [05/Nov/2026:07:03:22 +0000] "GET / HTTP/1.1" 200 29
127.0.0.1 - - [05/Nov/2026:07:03:23 +0000] "GET /index.html HTTP/1.1" 200 29
...
127.0.0.1 - - [05/Nov/2026:07:03:23 +0000] "GET /.git/config HTTP/1.1" 404 277
127.0.0.1 - - [05/Nov/2026:07:03:22 +0000] "GET /cgi-bin/slow.cgi HTTP/1.1" 200 17

slow.cgi의 줄이 맨 마지막에 있는데, 시각은 07:03:22로 그 앞의 줄들보다 이릅니다. %t는 요청을 받은 시각이지만, 로그 줄은 요청이 끝난 뒤에 쓰기 때문이에요. 교재의 경고 그대로입니다. 1.3강의 Squid는 반대로 끝난 시각을 적었다는 걸 기억하세요(시작 = 끝 − 걸린 시간). 같은 "웹 로그"라도 시각의 뜻이 다릅니다.

조사에서의 의미: 로그를 줄 순서대로 읽어 타임라인을 만들면 틀립니다. 시각 필드로 다시 정렬해야 하고, 오래 걸린 요청(큰 다운로드, 느린 업로드, 오래 붙어 있는 연결)일수록 줄 위치와 시각의 차이가 큽니다. 걸린 시간을 남기는 %D(마이크로초)를 형식에 넣어 두면 시작과 끝을 모두 계산할 수 있어요.

3.2 크기 필드: %b와 %O, 그리고 배포판의 기본값

같은 요청을 %b와 %O를 함께 남기는 형식으로도 기록했습니다.

(직접 실행 결과, 필드 일부)

"GET / HTTP/1.1"            200  29      256
"GET /dataset.bin HTTP/1.1" 206  50000   50304
"GET /dataset.bin HTTP/1.1" 200  200000  200249
  • %b(가운데 열)는 응답 본문의 크기입니다. 29바이트짜리 페이지는 29입니다.
  • %O(오른쪽 열)는 mod_logio가 세는 헤더를 포함해 실제로 보낸 바이트입니다. 같은 페이지가 256이에요.
  • 206(부분 응답): curl -r 150000-로 200,000바이트 파일의 15만 바이트 이후만 요청하자 %b는 50,000이었습니다. 교재 말처럼 이어 받기를 하면 이 값은 "파일 크기"가 아니라 그 요청으로 보낸 부분의 크기예요. 디스크의 파일 크기와 맞춰 볼 때 조심해야 합니다.

[보충] Ubuntu의 combined는 %b가 아니라 %O를 쓴다

Apache의 원래 문서에 있는 Combined 형식은 크기 필드로 %b를 쓰지만, 이 작업 환경의 Ubuntu 패키지가 미리 정의해 둔 combined는 "%h %l %u %t \"%r\" %>s %O ..."로 %O를 씁니다(/etc/apache2/apache2.conf에서 직접 확인). 그래서 같은 이름의 "combined" 로그라도 배포판에 따라 크기가 본문만(29)이기도, 헤더 포함(256)이기도 합니다. 로그를 받으면 숫자를 해석하기 전에 그 서버의 설정 파일에서 LogFormat 정의부터 확인하세요. 1.2강에서 "설정 파일이 증거"라고 한 이유가 여기서도 통합니다.

3.3 [정정] 출력할 수 없는 글자는 \xhh로 남는다

교재는 대부분의 경우 출력할 수 없는 글자가 +로 바뀐다고 설명합니다. Apache 2.4에서 직접 시험해 보니 그렇지 않았습니다. 탭, 따옴표, 한글이 든 User-Agent와, 제어 문자(\x01)가 든 요청 줄을 보냈어요.

(직접 실행 결과)

"GET /index.html HTTP/1.1" 200 256 "-" "tab\there \"quoted\" \xed\x95\x9c\xea\xb8\x80"
"GET /a\x01b HTTP/1.1" 400 489 "-" "-"

탭은 \t, 따옴표는 \", 한글 "한글"의 UTF-8 바이트는 \xed\x95\x9c\xea\xb8\x80, 제어 문자는 \x01로 이스케이프됐습니다. Apache 문서도 요청 줄과 헤더 값의 출력할 수 없는 글자를 \xhh로 바꿔 쓴다고 적고 있어요. 공격자가 로그에 가짜 줄을 끼워 넣지 못하게 하려는 장치입니다. 조사에서는 두 가지를 기억합니다. ① 로그에서 키워드를 찾을 때 이스케이프된 모양으로도 찾아야 합니다(한글 User-Agent는 \xed...로만 보입니다). ② 제어 문자가 든 요청 줄은 그 자체로 정상 브라우저가 아니라는 신호입니다. 이 요청은 400을 받았어요.

4. Common+VHost와 Combined

4.1 [주의] 가상 호스트 필드는 "요청한 이름"이 아니다

Common+VHost는 각 줄 앞에 가상 호스트 이름을 붙입니다. 한 IP에서 여러 사이트를 운영하는 환경을 위한 형식이에요(2.1강 6.1절). Ubuntu의 vhost_combined는 %v:%p로 시작합니다. 그런데 실습에서 Host: portal.lab.example로 보낸 요청이 이렇게 남았습니다.

(직접 실행 결과)

www.lab.example:80 127.0.0.1 - - [05/Nov/2026:07:03:23 +0000] "GET /index.html HTTP/1.1" 200 256 "https://search.example/" "..."

요청한 이름은 portal.lab.example이었는데, 로그에는 www.lab.example:80이 찍혔습니다. %v는 그 요청을 처리한 가상 호스트의 대표 이름(ServerName)이고, %p도 설정상의 대표 포트라 실제로 받은 8080이 아니었어요. 한 가상 호스트에 별칭(ServerAlias)이 여러 개라면, 어느 이름으로 들어왔는지는 이 필드로 알 수 없습니다. 클라이언트가 실제로 보낸 이름이 필요하다면 형식에 %{Host}i(요청 헤더 Host의 값)를 넣어야 합니다. 바로 아래 5절의 mod_log_forensic에는 Host:portal.lab.example이 그대로 남아 있었습니다.

4.2 Combined: Referer와 User-Agent

처음에는 Referer와 User-Agent를 별도 파일에 남겼습니다. Common 형식에 의존하는 프로그램들을 깨지 않으려고요. 그 뒤 둘을 줄 끝에 붙인 Combined 형식이 표준처럼 쓰이게 됐습니다.

(직접 실행 결과)

127.0.0.1 - - [05/Nov/2026:07:03:23 +0000] "GET /index.html HTTP/1.1" 200 256 "https://search.example/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"

조사에서 두 값은 이렇게 씁니다.

  • 우리 서버를 볼 때: Referer는 그 클릭이 어디서 왔는지(수상한 게시판에 링크가 올라왔다든가), User-Agent는 사이트를 긁어 간 도구(피싱 페이지를 그럴듯하게 만들려고 내용을 베껴 가는 정찰 등)를 알려 줍니다.
  • 우리 직원을 볼 때(프록시 쪽): Referer는 한 브라우저 탭 안의 이동을 잇고, User-Agent는 조직 안에서 실제로 쓰이는 소프트웨어의 목록을 줍니다(2.4강).

교재는 로그에 남는 헤더가 mod_headers 같은 기능으로 서버 안에서 바뀐 뒤의 값이라는 점도 경고합니다. 서버 설정이 헤더를 고친다면 로그는 클라이언트가 보낸 원래 값이 아닐 수 있어요. 그리고 두 값을 로그에서 빼 버리면 조사에 쓸 귀한 자료를 잃습니다.

[정정] \"W3C Extended\"와 \"Combined\"는 같은 형식이 아니다

교재는 이 형식을 "W3C Extended/Combined"라고 함께 부르고, W3C가 정한 형식이라고 설명합니다. 하지만 둘은 다릅니다. Combined는 NCSA Common에 Referer와 User-Agent를 붙인 NCSA 계열 형식(Apache, nginx의 기본)이고, W3C 확장 로그 형식(Extended Log File Format)은 1.5강에서 본 것처럼 #Fields: 줄로 열 이름을 선언하는 다른 형식(IIS의 기본, 일부 상용 프록시)입니다. 둘을 같은 해석기로 읽으면 필드가 어긋나니, 파일 앞부분에 #Fields:가 있는지부터 봅니다.

4.3 [주의] 실패한 로그인에도 사용자 이름이 남는다

Basic 인증이 걸린 /admin/에 올바른 계정(jdoe)과 없는 계정(admin:admin)으로 한 번씩 요청했습니다.

(직접 실행 결과)

127.0.0.1 - jdoe [05/Nov/2026:07:03:23 +0000] "GET /admin/ HTTP/1.1" 200 11
127.0.0.1 - admin [05/Nov/2026:07:03:23 +0000] "GET /admin/ HTTP/1.1" 401 462

두 번째 줄의 %u는 admin인데 상태 코드는 401입니다. 즉 %u는 "인증에 성공한 사용자"가 아니라 클라이언트가 보낸 사용자 이름이고, 실패해도 남습니다. 오류 로그에는 같은 시각에 AH01618: user admin not found: /admin/이 남았어요**(직접 실행 결과)**. 그러니 "%u에 admin이 있으니 관리자가 접속했다"고 읽으면 안 되고, 반드시 상태 코드와 함께 읽습니다. 반대로 401이 쌓인 %u 목록은 무차별 대입에 쓰인 계정 이름 목록이 됩니다.

5. Apache mod_log_forensic: 모든 헤더를 남긴다

mod_log_forensic은 이름과 달리 디지털 포렌식을 위해 만든 건 아니지만, 요청마다 모든 요청 헤더를 남겨 줍니다. 서버가 TLS의 끝점이니 HTTPS 요청도 복호화된 상태로 남아요. 직접 켜서 본 결과입니다.

(직접 실행 결과, 한 요청의 시작 줄)

+auwqO8D2TYfXTSURFaXOvAAAAAc|GET /index.html HTTP/1.1|Host:portal.lab.example|User-Agent:Mozilla/5.0 (...)|Accept:*/*|Referer:https%3a//search.example/
  • 줄은 +고유 ID|요청 줄|헤더:값|헤더:값|... 모양입니다. 고유 ID는 mod_unique_id가 요청마다 붙이는 값이에요.
  • 요청 처리가 끝나면 -고유 ID 줄이 따로 남습니다. 이번 실습에서는 + 17줄, - 17줄이었습니다**(직접 실행 결과)**. 처리 도중 서버가 죽거나 끊긴 요청은 +만 있고 -가 없어, 서버를 쓰러뜨린 요청을 찾는 데 쓸 수 있습니다.
  • 4.1절에서 본 것처럼 실제로 요청한 Host(portal.lab.example)가 남아 있습니다.
  • 헤더 값 안의 특별한 글자는 퍼센트 인코딩으로 바뀝니다(Referer:https%3a//...의 %3a).

[주의] 포렌식 로그는 비밀번호까지 남긴다

/admin/ 요청의 줄에는 Authorization:Basic amRvZTpTcHJpbmcyMDI2IQ==가 그대로 남았습니다**(직접 실행 결과)**. 2.1강에서 본 대로 Base64는 바로 풀리니, 이 로그에는 평문 비밀번호(jdoe:Spring2026!)가 들어 있는 셈입니다. 세션 쿠키도 마찬가지고요. 조사에는 귀한 자료지만, 이 로그 자체가 공격자에게는 보물 창고입니다. 켤 때는 필요한 가상 호스트나 경로로 범위를 좁히고(교재가 권하듯 성능과 디스크 부담도 줄어듭니다), 접근 권한과 보관 기간을 엄격히 정합니다.

6. IIS의 로그

IIS는 이 환경에서 돌릴 수 없어 문서로 확인한 내용입니다.

  • W3C 확장 형식이 기본입니다. #Fields: 줄로 남길 필드를 고를 수 있고(date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) sc-status sc-bytes cs-bytes time-taken 등), Microsoft의 IIS 6 시절 문서에 따르면 시각을 UTC로 적습니다. 1.5강의 ELFF와 같은 방식으로 #Fields:를 읽어 열 이름으로 다룹니다.
  • IIS 형식은 필드가 고정된 쉼표 구분 형식으로, 교재가 정리한 순서는 요청자 주소, 인증 사용자, 날짜(두 자리 연도), 시각, 서비스 이름과 인스턴스(W3SVC2 등), 서버 이름, 서버 IP, 처리 시간(밀리초), 받은 바이트, 보낸 바이트, 상태 코드, Windows 오류 코드, 메서드, 요청 자원, 쿼리입니다. 줄 끝에도 쉼표가 붙어요. 이 형식은 지역 시각을 쓴다고 IIS 6 문서에 적혀 있으니, W3C 형식 로그와 섞어 볼 때 시간대를 꼭 맞춥니다.
  • 받은 바이트(cs-bytes)와 처리 시간을 기본으로 남길 수 있다는 점이 Apache의 기본 형식과 다릅니다. 1.4강에서 "올린 양"이 로그에 없어 아쉬웠던 그 값이에요.
  • 부하가 큰 서버를 위한 **중앙 집중식 이진 로그(CBL)**와 ODBC(SQL Server 같은 데이터베이스에 기록)도 있습니다. 둘 다 바로 읽을 수 없어 질의 도구가 필요하고, Microsoft의 오래된 무료 도구 Log Parser(SQL 비슷한 질의로 여러 로그 형식을 읽음)와 그 화면 도구 Log Parser Studio가 흔히 쓰였습니다. 문서에 따르면 ODBC 기록은 커널 모드 캐시를 끄게 되어 성능 부담이 크다는 경고도 있어요.

7. 셸로 분석하기

글자 파일인 로그는 셸 도구로 바로, 그리고 대용량에도 다룰 수 있습니다. 교재가 예로 드는 네 가지 명령을 실습 로그(Ubuntu의 combined)에 맞춰 돌렸습니다. 필드 번호는 공백으로 나눈 위치이니, Combined 형식에서 $1은 주소, $4는 시각, $7은 요청 대상, $9는 상태 코드입니다.

① 특정 자원을 요청한 주소를 빈도순으로

grep '"GET /cgi-bin/search.cgi' lab_combined.log | awk '{print $1}' | sort | uniq -c | sort -nr

(직접 실행 결과) 5 127.0.0.1

② 한 주소가 요청한 자원을 빈도순으로

grep '^127.0.0.1 ' lab_combined.log | awk '{print $7}' | sort | uniq -c | sort -nr | head -5

grep '^127.0.0.1 '의 끝 공백은 127.0.0.10처럼 앞부분이 같은 주소를 빼기 위한 것입니다. 정규식에서 .은 아무 글자이니 엄밀히는 '^127\.0\.0\.1 '이 맞아요.

③ 시간대별로 묶기

awk '{split($4,t,":"); print substr(t[1],2)" "t[2]":00"}' lab_combined.log | sort | uniq -c

(직접 실행 결과) 17 05/Nov/2026 07:00

$4는 [05/Nov/2026:07:03:23이고, :로 나누면 t[1]이 [05/Nov/2026, t[2]가 07입니다. substr(t[1],2)로 앞의 [를 뗐어요. 교재는 같은 일을 sed 정규식으로 하는데, 필드를 나눠 쓰는 쪽이 읽기 쉽습니다. 이 방법은 로그에 적힌 시간대(+0000) 기준이라는 점을 잊지 마세요.

④ 서버 오류를 일으킨 주소와 요청

awk '{print $9, $1, $7}' lab_combined.log | grep -E '^5[0-9]{2}' | sort | uniq -c | sort -nr

(직접 실행 결과)

      1 500 127.0.0.1 /cgi-bin/search.cgi?q=orchid%27%20OR%201=1--
      1 500 127.0.0.1 /cgi-bin/search.cgi?q=orchid%27
      1 500 127.0.0.1 /cgi-bin/search.cgi?q=1%27%20UNION%20SELECT%20user,pass%20FROM%20users--

이런 명령은 끝이 없습니다. awk와 sed만 익혀도 큰 로그에서 원하는 것을 빠르게 뽑고, 스프레드시트나 보고서에 넣을 표까지 만들 수 있어요. 데이터베이스(CBL·ODBC)에 있는 로그라면 SQL 질의를 모아 반복 가능한 질의 목록을 만들 수 있고, 2.13강의 SOF-ELK처럼 NCSA와 Combined 형식을 바로 읽는 도구도 있습니다. 조직이 업무 목적으로 오래 보관해 온 로그가 있다면 몇 년 전의 기록까지 볼 수 있다는 점도 기억하세요.

[주의] $9가 늘 상태 코드는 아니다

공백으로 필드를 나누는 방식은 요청 줄 안에 공백이 없다는 가정에 기댑니다. 공격자가 보낸 엉터리 요청 줄(공백이 여러 개이거나 따옴표가 깨진)이 있으면 그 줄만 필드 번호가 밀립니다. 3.3절처럼 Apache가 따옴표를 이스케이프하긴 하지만, 중요한 결론을 내리기 전에는 awk -F'"'처럼 따옴표로 나눠 다시 확인하거나, 형식을 아는 해석기를 씁니다.

8. 로그에 남은 공격의 흔적

교재는 로그 증거가 특히 쓸모 있는 경우를 이렇게 정리합니다. 실습 로그로 보면,

정찰. 검색 엔진으로 취약한 웹 앱을 찾거나, 알려진 취약 소프트웨어의 경로를 무작정 찍어 봅니다.

(직접 실행 결과, 일부)

"GET /phpmyadmin/ HTTP/1.1" 404 ... "Mozilla/5.0 zgrab/0.x"
"GET /phpMyAdmin-2.8.0/scripts/setup.php HTTP/1.1" 404 ... "Mozilla/5.0 zgrab/0.x"
"GET /wp-login.php HTTP/1.1" 404 ...
"GET /.env HTTP/1.1" 404 ...
"GET /.git/config HTTP/1.1" 404 ...

교재의 예처럼 특정 소프트웨어의 여러 버전 경로(phpMyAdmin-2.8.0, -2.8.1, …)를 몇 초 안에 차례로 찍는 모양이 전형적입니다. 2.2강 3.3절의 "404 폭우"예요.

SQL 주입 시도와 성공. 상태 코드와 User-Agent를 함께 세면 모양이 보입니다.

awk -F'"' '{split($3,a," "); print a[1], $6}' lab_combined.log | sort | uniq -c | sort -nr

-F'"'로 따옴표를 기준으로 나누면 $3은 요청 줄 뒤의 200 256(상태와 크기), $6은 User-Agent입니다.

(직접 실행 결과)

      5 404 Mozilla/5.0 zgrab/0.x
      5 200 Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/141.0.0.0 Safari/537.36
      3 500 sqlmap/1.8.4#stable (https://sqlmap.org)
      2 200 sqlmap/1.8.4#stable (https://sqlmap.org)
      1 401 Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/141.0.0.0 Safari/537.36
      1 206 Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/141.0.0.0 Safari/537.36

sqlmap은 널리 알려진 SQL 주입 도구의 기본 User-Agent입니다(물론 바꿀 수 있어요). 따옴표(%27)가 든 요청 셋이 500을 받았습니다(7절의 ④). 실제 사건이라면 여기서 볼 것은 ① 500 뒤에 같은 출발지의 200이 있는지, ② 그 200 응답의 크기가 평소 검색 결과보다 유난히 큰지(데이터가 쏟아져 나왔을 수 있음), ③ 그 뒤에 POST나 낯선 경로 요청이 이어지는지(장악 이후의 작업), ④ 오류 로그의 데이터베이스 오류 메시지입니다. 이 실습의 200 두 건은 따옴표가 없는 정상 검색이라 주입 성공과는 관계가 없어요.

알려진 나쁜 주소가 무엇에 접근했나. 위협 정보로 주소를 알게 되면, 7절의 ② 명령 하나로 그 주소의 정찰, 공격, 장악 이후 활동을 모두 뽑아 볼 수 있습니다. 민감한 파일이 실수로 공개됐던 사고라면, 그 파일을 누가, 언제 받아 갔는지도 로그가 바로 알려 줘요.

원격 관리 도구(RAT)와 웹 셸. 공격자가 서버에 웹 셸을 심어 두고 요청으로 명령을 보내는데, 로그를 지우지 않았다면(또는 로그가 다른 서버에 모여 있어 지울 수 없었다면, 2.12강) 공격자의 명령이 요청 줄마다 남습니다. 교재는 SQL 주입과 일련의 echo 명령으로 서버에 떨어뜨린 악성 실행 파일을, 로그만으로 다시 조립해 낸 조사 사례도 소개합니다. 쿼리 문자열에 실린 데이터가 그만큼 고스란히 남는다는 뜻이에요.

9. 실제 조사에서는 이렇게 씁니다

  1. 로그와 함께 설정 파일을 확보한다. LogFormat, CustomLog, IIS의 로그 설정. 크기 필드와 시각의 뜻은 설정을 봐야 압니다.
  2. 시각으로 다시 정렬한다. 줄 순서는 끝난 순서입니다. 시간대도 확인합니다(IIS 형식은 지역 시각, W3C 형식은 UTC).
  3. %u는 상태 코드와 함께 읽는다. 401과 함께 있으면 시도한 이름일 뿐입니다.
  4. 이스케이프된 모양으로도 찾는다. \xhh, \", 퍼센트 인코딩.
  5. 요청한 이름이 필요하면 %{Host}i를 본다. %v는 대표 이름입니다.
  6. 포렌식 로그는 민감 정보로 다룬다. 인증 헤더와 쿠키를 가린 사본으로 분석을 나눕니다.
  7. 흐름으로 본다. 정찰(404), 시도(500), 성공(200과 큰 크기), 이후 작업(낯선 경로, POST)의 순서를 한 출발지 기준으로 엮습니다.

10. 정리

  • 웹 서버 로그는 바깥에서 우리 서버로 들어온 기록이고, 서버가 TLS 끝점이라 HTTPS 요청도 평문으로 남습니다.
  • 형식은 NCSA Common, Common+VHost, Combined, mod_log_forensic, IIS의 W3C 확장(기본)·IIS·CBL·ODBC, nginx의 combined입니다.
  • Common의 일곱 필드: 주소, identd, 인증 사용자, 받은 시각, 요청 줄, 최종 상태, 본문 크기.
  • 직접 확인한 함정: 느린 요청은 이른 시각으로 맨 뒤에 남고(줄 순서 ≠ 시간 순서), 206의 %b는 보낸 부분(50,000)이며, Ubuntu의 combined는 헤더 포함 %O(256 대 29)이고, %v는 요청한 이름이 아니라 대표 이름이며(portal 요청이 www로), 실패한 로그인(401)에도 %u가 남습니다.
  • [정정] "W3C Extended"(#Fields:)와 "Combined"(NCSA 계열)는 다른 형식이고, 출력할 수 없는 글자는 +가 아니라 \xhh, \t, \"로 남습니다.
  • mod_log_forensic은 모든 헤더와 요청의 시작(+)·끝(-)을 남기지만 비밀번호와 쿠키까지 남깁니다.
  • 셸 명령 넷으로 자원별·주소별·시간대별·오류별 집계를 했고, 상태 코드와 User-Agent를 함께 세서 정찰(404)과 주입 시도(500)를 갈랐습니다.

복습 문제

문제 1. (필드 읽기) 다음 Common 줄의 각 필드를 설명하세요. 이 요청은 서버에서 무엇을 했을 가능성이 크나요?

192.0.2.77 - - [03/Nov/2026:02:14:09 +0900] "GET /backup/db_2026-10.sql.gz HTTP/1.1" 200 48211907
정답과 해설

주소 192.0.2.77, identd -, 인증 사용자 없음(-), 받은 시각 2026-11-03 02:14:09(+09:00, 즉 UTC로는 11월 2일 17:14:09), 요청 줄 GET /backup/db_2026-10.sql.gz HTTP/1.1, 최종 상태 200, 본문 48,211,907바이트(약 46MB). 데이터베이스 백업으로 보이는 파일이 인증 없이 통째로 내려갔을 가능성이 큽니다. 크기가 디스크의 파일 크기와 같은지(206 이어 받기가 아닌지) 확인하고, 같은 주소의 앞뒤 요청(어떻게 이 경로를 알았나: 디렉터리 목록, 정찰), 같은 파일을 받은 다른 주소를 찾습니다. 새벽 시각과 지역 시간대(+0900)도 보고서에 함께 적어요.

문제 2. (시각) 웹 서버 로그를 줄 순서대로 읽어 "10:00:05에 로그인, 10:00:07에 큰 파일 다운로드 시작"이라는 타임라인을 만들었는데, 다운로드 줄의 %t가 09:58:40이었습니다. 무엇이 일어난 걸까요?

정답과 해설

Apache의 %t는 요청을 받은 시각이고, 로그 줄은 요청이 끝난 뒤에 씁니다(3.1절). 그러니 이 다운로드는 09:58:40에 시작해서 로그인 줄이 쓰인 뒤에야 끝난, 오래 걸린 요청입니다. 즉 실제로는 다운로드가 로그인보다 먼저 시작됐어요. 다운로드가 로그인과 무관하게 시작됐다면(인증 없이 받을 수 있었다면) 사건의 의미가 크게 바뀝니다. 타임라인은 줄 순서가 아니라 %t로 다시 정렬하고, 형식에 %D(걸린 시간)가 있다면 끝난 시각까지 계산해 적습니다.

문제 3. (변경된 조건) 한 서버의 "combined" 로그에서 같은 29바이트 페이지가 256으로 찍혀 있어, 동료가 "로그가 틀렸다"고 합니다. 무엇을 확인하겠습니까?

정답과 해설

그 서버의 LogFormat 정의를 확인합니다. Ubuntu·Debian의 Apache 패키지는 combined를 %O(헤더 포함해 보낸 바이트)로 정의해 두었고, 원래 Apache 문서의 Combined는 %b(본문만)를 씁니다(3.2절에서 직접 확인: 29 대 256). 로그가 틀린 게 아니라 같은 이름, 다른 뜻인 거예요. 설정 파일을 증거와 함께 확보하고, 보고서에는 어느 정의인지 적습니다. 설정을 못 구했다면 응답 헤더 크기만큼의 일정한 차이가 있는지로 추정하되, 추정이라고 밝힙니다.

문제 4. (흔한 오해) "vhost_combined 로그의 첫 필드가 모두 shop.example.com이니, 공격자는 그 이름으로만 접속했다." 무엇이 틀렸을 수 있나요?

정답과 해설

%v는 요청을 처리한 가상 호스트의 대표 이름(ServerName)입니다. 4.1절의 실험에서 portal.lab.example로 보낸 요청이 www.lab.example:80으로 남았어요. 그 가상 호스트에 별칭이 있거나, 어떤 이름에도 맞지 않는 요청이 기본 가상 호스트로 처리됐다면, 공격자가 실제로 쓴 이름(IP 주소 그대로, 다른 별칭, 엉뚱한 이름)은 이 필드로 알 수 없습니다. 실제 Host 헤더가 필요하면 %{Host}i가 든 로그나 mod_log_forensic, 패킷(평문이라면), 앞단 장비의 로그를 찾습니다.

문제 5. (해석) 다음 두 줄이 1초 간격으로 있습니다. 무엇을 알 수 있고, 무엇은 알 수 없나요?

198.51.100.9 - administrator [04/Nov/2026:11:20:01 +0000] "GET /manage/ HTTP/1.1" 401 381
198.51.100.9 - administrator [04/Nov/2026:11:20:02 +0000] "GET /manage/ HTTP/1.1" 200 5120
정답과 해설

알 수 있는 것: 198.51.100.9가 administrator라는 이름으로 인증을 시도해 한 번 실패(401)하고 1초 뒤 성공(200)했다는 것. 4.3절처럼 %u는 보낸 이름이라 401 줄의 administrator는 시도한 이름일 뿐이고, 200 줄에서 비로소 그 이름으로 인증에 성공했다고 볼 수 있습니다. 알 수 없는 것: 첫 시도의 비밀번호가 틀렸는지, 아니면 브라우저가 처음에 인증 없이 요청해 401을 받은 뒤 계정을 붙여 다시 보낸 정상 흐름인지. Basic 인증에서는 브라우저가 401을 받고 로그인 창을 띄우는 게 흔한 모양이라, 401 한 번 뒤의 200은 정상일 수 있어요. 앞에 401이 수십 번 쌓였다면 무차별 대입입니다. 출발지가 그 계정의 평소 접속지인지도 확인합니다.

문제 6. (정정된 내용) User-Agent에 한글이 들어간 요청을 찾으려고 로그에서 grep '한국'을 했더니 하나도 나오지 않았습니다. 왜 그럴 수 있고, 어떻게 찾아야 하나요?

정답과 해설

Apache 2.4는 헤더 값의 ASCII가 아닌 바이트를 \xhh로 이스케이프해서 씁니다(3.3절: "한글"이 \xed\x95\x9c\xea\xb8\x80으로 남음). 그래서 한글 그대로는 로그에 없어요. "한국"의 UTF-8 바이트(\xed\x95\x9c\xea\xb5\xad)를 이스케이프된 글자 그대로 찾아야 합니다. 셸에서는 역슬래시를 조심해 grep -F '\xed\x95\x9c\xea\xb5\xad'처럼 고정 문자열로 찾는 것이 안전합니다. 넓게는 grep -F '\x'로 이스케이프가 든 줄을 모두 뽑아 디코딩해 보는 방법도 있습니다. 교재처럼 +로 바뀐다고 생각하고 찾으면 놓칩니다.

문제 7. (적용) 웹 서버 침해 조사를 위해 로그 형식을 바꿀 수 있다면, Combined에 무엇을 더 넣겠습니까? 각각의 이유도 말해 보세요.

정답과 해설

예를 들어 ① %{Host}i: 실제로 요청한 이름(4.1절). ② %D: 걸린 시간(마이크로초). 시작과 끝을 모두 계산하고, 느린 요청(큰 유출, 시간 기반 블라인드 SQL 주입)을 찾습니다(3.1절). ③ %I(받은 바이트, mod_logio): 업로드 양. 1.4강과 1.13강에서 아쉬웠던 값이에요. ④ %O와 %b를 함께: 헤더 포함·제외 크기를 모두. ⑤ 시간대가 분명한 시각(%{%Y-%m-%dT%H:%M:%S}t와 %{%z}t, 또는 UTC로 통일): 다른 로그와 맞추기 쉽습니다. ⑥ %{UNIQUE_ID}e: mod_log_forensic이나 앱 로그와 같은 요청을 잇는 키. ⑦ 앞단에 프록시가 있다면 %a와 %{X-Forwarded-For}i를 함께(2.1강 6.6절). 무엇을 넣든 성능과 디스크, 개인정보(쿠키나 인증 헤더는 넣지 않음)를 함께 따집니다.

문제 8. (해석) 7절의 ④처럼 500 오류를 세었더니 한 주소에서 따옴표가 든 요청 40건이 500을 받았고, 그 직후 같은 주소의 같은 경로 요청 한 건이 200, 크기 1,843,022바이트였습니다. 평소 그 검색 결과는 2KB 안팎입니다. 어떤 가설을 세우고, 무엇으로 확인하겠습니까?

정답과 해설

가설: SQL 주입이 여러 번 실패(500)하다가 마침내 성공했고, 그 응답에 데이터베이스의 많은 내용(평소의 900배 가까운 크기)이 실려 나갔다. 교재가 말하는 "500 뒤의 200" 모양입니다(2.2강 3.3절). 확인할 것: ① 그 200 요청의 쿼리 문자열(UNION SELECT 같은 구문이 있나), ② 오류 로그의 데이터베이스 오류 메시지와 같은 시각의 데이터베이스 로그, ③ 그 응답 내용을 다시 얻을 방법(패킷, 앞단 장비의 기록, 웹 앱 로그), ④ 같은 주소의 이후 요청(관리 화면, 업로드, 웹 셸로 보이는 경로), ⑤ 같은 기법을 쓴 다른 주소. 피해 평가(1.11강 1절)를 위해 어떤 테이블이 나갔는지 데이터베이스 담당자와 함께 봅니다.

문제 9. (흔한 오해) "mod_log_forensic은 포렌식용 기능이니 모든 서버에 항상 켜 두는 게 좋다." 이 주장의 문제를 설명하세요.

정답과 해설

이름과 달리 이 모듈은 디버깅용으로 만든 것이고, 모든 요청 헤더를 남기므로 ① 디스크 사용량과 성능 부담이 크고, ② 무엇보다 5절에서 본 것처럼 Basic 인증의 비밀번호, 세션 쿠키, 토큰까지 남깁니다. 이 로그가 유출되면 그 자체가 대규모 계정 유출이 돼요. 그래서 교재도 필요한 가상 호스트나 경로로 범위를 좁혀 켜라고 권합니다. 꼭 켜야 한다면 접근 권한을 엄격히 하고, 보관 기간을 짧게 정하고, 분석용으로는 민감한 헤더를 가린 사본을 쓰는 운영 절차를 함께 둡니다.

참고 자료

  • 작업 환경에서 쓴 도구: Apache 2.4.58(Ubuntu 패키지, mod_log_forensic, mod_unique_id, mod_cgid, mod_logio), curl 8.5.0, GNU awk·grep·sort·uniq. 실행 결과는 2026-11-05에 얻었습니다.
  • Apache, mod_log_config 문서 (%h %l %u %t %r %>s %b %O %D %v %p %{Host}i, 출력할 수 없는 글자의 \xhh 이스케이프, Common·Combined 형식 예).
  • Apache, mod_log_forensic 문서와 mod_logio 문서(%I, %O).
  • Microsoft, IIS Log File Formats (IIS 6 문서, 2026-11-05 검색 결과로 확인). W3C 확장 형식이 기본이고 UTC, 다른 형식은 지역 시각, ODBC 기록의 성능 경고.
  • nginx, ngx_http_log_module 문서 (기본 combined 형식).
  • W3C, Extended Log File Format (#Fields: 지시어).

이 페이지에 대하여

여러 학습 자료를 바탕으로 직접 다시 쓴 개인 학습 노트입니다. 연습용 서버 설정과 요청은 직접 만들었고, 사례는 가상입니다.