보안관제 실습 - 로그에서 공격 흔적 찾기

changseop lee·2026년 9월 24일

리눅스 시스템 기초

목록 보기
249/250

리눅스 시스템 기초 · 입문편 — 본문의 "N편"은 입문 과정 번호다. 번호별 글과 전체 250편 구성은 통합 로드맵에서 확인할 수 있다.

리눅스 시스템 기초 50 / 50 · Part 5. 네트워크·보안·SOC
실습 환경: 21편 샘플 로그(secure + access_log) · 타임라인·집계는 모두 샘플 로그에 스크립트를 실행한 실제 출력
이전 글: 49. SSH Brute Force 로그 분석

1. 들어가며

시리즈의 마지막 글이다. 1편에서 보안관제의 흐름을 이렇게 정리했다.

Logs → Indicators → Root Cause → Response
로그     침해 지표      원인 분석      대응

이번 글은 이 흐름을 처음부터 끝까지 한 번 따라간다. 49편에서 SSH 로그만으로 "10.0.0.128이 devuser 계정을 탈취했다"는 것까지 확인했다. 이번에는 웹 로그(access_log)를 더해 같은 공격자가 그 뒤에 무엇을 했는지 재구성한다.

실제 관제에서 경보는 대개 하나의 로그 소스에서 시작하지만, 판단은 여러 소스를 하나의 시간축에 올려야 내릴 수 있다. 이번 글의 목표는 세 가지다.

  1. 두 로그를 한 IP 기준으로 병합 해 타임라인을 만든다.
  2. 각 행위를 공격 단계 에 대응시키고, 성공·실패를 근거와 함께 판정한다.
  3. 시리즈 전체에서 배운 명령어를 초동 대응 체크리스트 로 정리한다.

2. 핵심 개념

2-1. 타임라인 분석

원칙이유
시간대를 통일 한다syslog에는 연도·시간대가 없고, 웹 로그는 +0900이 있다(48편)
피벗(pivot) 값 으로 묶는다IP, 계정, PID, 세션처럼 여러 로그에 공통으로 나오는 값
반복은 요약, 특이점은 원문404 30줄은 "404 × 30", 200 응답은 한 줄씩
사실과 추정을 구분 한다"200, 1893 B"는 사실, "passwd가 노출됨"은 추정

2-2. 판정 등급

판정의미예
시도 (Attempt)공격 행위가 있었으나 결과 불명·실패404, SYN만 있음, Failed password
성공 (Success)목적 달성의 직접 근거Accepted, 200 + 의미 있는 응답 크기
영향 (Impact)성공으로 인해 생긴 피해계정 탈취, 파일 노출, 서비스 중단

3. 동작 원리

로그 두 개로 재구성한 공격 타임라인 — 10.0.0.128

3-1. 병합 스크립트

secure와 access_log를 한 IP 기준으로 시간순 병합하는 스크립트다(shellcheck 경고 없음). 21~28편의 grep, awk, sort, 파이프라인만 사용한다.

#!/usr/bin/env bash
# timeline.sh — secure + access_log 를 한 IP 기준으로 시간순 병합
# 사용법: ./timeline.sh <IP> <secure> <access_log> [연도]
IP=$1; SEC=$2; WEB=$3; Y=${4:-$(date +%Y)}
{
  # secure: 해당 IP 줄 + 그 IP로 성공한 세션 PID·계정의 줄 (실패·없는 계정은 아래에서 요약)
  pids=$(grep -F "from $IP " "$SEC" | grep Accepted | grep -oE 'sshd\[[0-9]+\]' | sort -u | paste -sd'|')
  users=$(grep -F "from $IP " "$SEC" | grep Accepted | awk '{print $9}' | sort -u | paste -sd'|')
  grep -E "from $IP |${pids:-NOPID}|${users:+\b($users)\b}" "$SEC" | grep -vE 'Failed password|Invalid user' |
    awk -v y="$Y" '{m=index("JanFebMarAprMayJunJulAugSepOctNovDec",$1); m=(m+2)/3;
      printf "%s-%02d-%02d %s  SSH  ", y, m, $2, $3; $1=$2=$3=$4=""; sub(/^ +/,""); print}'
  # 실패는 분 단위로 요약
  grep -F "from $IP " "$SEC" | grep 'Failed password' |
    awk -v y="$Y" '{m=index("JanFebMarAprMayJunJulAugSepOctNovDec",$1); m=(m+2)/3;
      k=sprintf("%s-%02d-%02d %s:00", y, m, $2, substr($3,1,5)); c[k]++}
      END{for(k in c) printf "%s  SSH  Failed password x%d (분 합계)\n", k, c[k]}'
  # access_log: 404 는 분 단위로 요약, 나머지(200·500 등)는 한 줄씩
  awk -v ip="$IP" '$1==ip {split($4,t,/[\[\/:]/);
      m=index("JanFebMarAprMayJunJulAugSepOctNovDec",t[3]); m=(m+2)/3;
      d=sprintf("%s-%02d-%02d", t[4], m, t[2]);
      if ($9==404) {k=d" "t[5]":"t[6]":00"; c[k]++; next}
      printf "%s %s:%s:%s  WEB  %s %s -> %s (%s B)\n", d, t[5], t[6], t[7], substr($6,2), $7, $9, $10}
      END{for(k in c) printf "%s  WEB  404 x%d (경로 탐색, 분 합계)\n", k, c[k]}' "$WEB"
} | sort

핵심 아이디어는 두 로그의 시각을 같은 형식(YYYY-MM-DD HH:MM:SS)으로 바꾼 뒤 sort 하는 것이다. 문자열 정렬이 곧 시간 정렬이 된다.

한계: 두 로그가 같은 시간대(KST)로 기록되었다고 가정한다. 서버마다 시간대가 다르면 먼저 UTC로 맞춰야 한다.

3-2. 실행 결과 (실제 출력)

$ ./timeline.sh 10.0.0.128 secure access_log 2026
2026-09-23 02:10:00  SSH  Failed password x10 (분 합계)
2026-09-23 02:11:00  SSH  Failed password x8 (분 합계)
2026-09-23 02:12:00  SSH  Failed password x10 (분 합계)
2026-09-23 02:13:00  SSH  Failed password x8 (분 합계)
2026-09-23 02:14:00  SSH  Failed password x10 (분 합계)
2026-09-23 02:15:00  SSH  Failed password x10 (분 합계)
2026-09-23 02:16:00  SSH  Failed password x11 (분 합계)
2026-09-23 02:17:00  SSH  Failed password x14 (분 합계)
2026-09-23 02:18:00  SSH  Failed password x10 (분 합계)
2026-09-23 02:19:00  SSH  Failed password x4 (분 합계)
2026-09-23 02:19:21  SSH  sshd[5224]: Accepted password for devuser from 10.0.0.128 port 51514 ssh2
2026-09-23 02:19:21  SSH  sshd[5224]: pam_unix(sshd:session): session opened for user devuser(uid=1001) by (uid=0)
2026-09-23 02:21:21  SSH  sudo[5233]: devuser : user NOT in sudoers ; TTY=pts/1 ; PWD=/home/devuser ; USER=root ; COMMAND=/bin/bash
2026-09-23 02:22:21  SSH  su[5270]: FAILED SU (to root) devuser on pts/1
2026-09-23 02:22:21  SSH  su[5270]: pam_unix(su-l:auth): authentication failure; logname=devuser uid=1001 euid=0 tty=pts/1 ruser=devuser rhost= user=root
2026-09-23 02:30:00  WEB  404 x30 (경로 탐색, 분 합계)
2026-09-23 02:30:53  WEB  GET /view.php?file=../../../etc/passwd -> 200 (1893 B)
2026-09-23 02:30:57  WEB  GET /view.php?file=..%2f..%2f..%2fetc%2fshadow -> 200 (0 B)
2026-09-23 02:31:00  WEB  GET /view.php?file=....//....//etc/hosts -> 200 (0 B)
2026-09-23 02:31:05  WEB  GET /board.php?id=1%27%20OR%20%271%27=%271 -> 500 (612 B)
2026-09-23 02:31:07  WEB  GET /board.php?id=1%20UNION%20SELECT%20user,pass%20FROM%20members-- -> 500 (612 B)
2026-09-23 02:31:21  SSH  sshd[5224]: pam_unix(sshd:session): session closed for user devuser

176줄 + 255줄의 로그가 22줄의 이야기 로 줄었다.


4. 실습

4-1. 단계별 분석 절차

# 0) 준비 — 원본은 건드리지 않고 복사본으로 (29편)
mkdir -p ~/case && cp -p secure access_log ~/case/ && cd ~/case
sha256sum secure access_log > SHA256SUMS

# 1) Logs — 규모 파악 (49편)
grep -c 'Failed password' secure
awk '{print $1}' access_log | sort | uniq -c | sort -rn | head -5

# 2) Indicators — 의심 IP의 웹 요청 상태 코드 (24·25편)
awk '$1=="10.0.0.128" {print $9}' access_log | sort | uniq -c

# 3) 공격 패턴 검색 (21편)
grep -Ei '\.\./|%2e%2e|%2f|union|select|%27|etc/passwd' access_log

# 4) 두 로그 병합 타임라인
./timeline.sh 10.0.0.128 secure access_log 2026 | tee timeline_10.0.0.128.txt

# 5) 다른 IP도 같은 패턴을 보였는가 (범위 확인)
grep -Ei '\.\./|%2e%2e|%2f|union|select|%27|etc/passwd' access_log | awk '{print $1}' | sort | uniq -c

2)의 실제 결과다.

     3 200
    30 404
     2 500

5)의 실제 결과 — 공격 패턴을 보인 IP는 10.0.0.128 하나다.

      5 10.0.0.128

4-2. 서버에서 추가로 확인할 것 (실제 서버)

로그 분석으로 "무엇을 시도했는가"를 알았다면, 서버에서 "무엇이 남았는가"를 확인한다.

# 세션 중 생긴 파일 (22편)
sudo find / -xdev -newermt '2026-09-23 02:19' ! -newermt '2026-09-23 02:32' -type f 2>/dev/null | grep -v '^/proc'
# 지속성 (36·38편, 45편)
sudo crontab -l -u devuser; ls -la ~devuser/.ssh/; systemctl list-units --type=service --state=running
# 남아 있는 프로세스·연결 (32·44편)
ps -u devuser -o pid,ppid,etime,args; sudo ss -tnp | grep -E '10\.0\.0\.128'
# 웹 취약 파일
sudo grep -n 'file' /var/www/html/view.php

5. 결과 분석

5-1. 행위별 판정

시각행위근거판정
02:10–02:19SSH 사전·무차별 대입실패 95회, 분당 8~14회시도
02:19:21devuser 로그인Accepted password성공
02:21–02:22root 권한 상승 시도NOT in sudoers, FAILED SU실패
02:30:01–51웹 경로 탐색404 × 30 (/admin, /.git/config, /backup.zip …)시도 (정보 수집)
02:30:53경로 조작 → /etc/passwd200, 1893 B성공 추정
02:30:57경로 조작 → /etc/shadow200, 0 B실패 추정 (권한 없음)
02:31:00경로 조작 → /etc/hosts200, 0 B실패 추정 (필터 우회 실패)
02:31:05–07SQL Injection500시도 — 오류 유발, 데이터 유출 여부 확인 필요

응답 크기를 읽는 법. 경로 조작 요청의 응답이 200이라도 크기가 0이면 파일을 읽지 못했을 가능성이 높다. 반면 1893바이트는 일반적인 /etc/passwd 크기와 비슷하다. 그래서 "passwd가 노출되었다"는 강한 추정 이지만, 최종 확인은 웹 서버에서 같은 요청을 재현하거나 view.php 코드를 보고 한다. /etc/shadow가 0바이트인 것은 웹 서버 계정(apache)이 읽을 권한이 없기 때문일 가능성이 높다(15·18편: shadow는 root만 읽기). 권한 설계가 피해를 막은 사례 다.

SQL Injection의 500. 서버 오류는 입력이 SQL 문에 그대로 들어가 문법 오류를 일으켰다 는 신호일 수 있다. 즉 취약점이 존재할 가능성 이 높다. 이번 요청으로 데이터가 나가지는 않았더라도 조치 대상이다.

5-2. 결론

[무엇이]   내부 호스트 10.0.0.128 이
[어떻게]   SSH 무차별 대입으로 devuser 계정을 탈취하고(02:19),
           root 권한 획득에는 실패한 뒤(02:21~22),
           웹 서버를 탐색해 view.php 경로 조작 취약점으로 /etc/passwd 를 읽은 것으로 보이며(02:30),
           board.php 에 SQL Injection 을 시도했다(02:31).
[영향]     devuser 계정 탈취, 시스템 계정 목록(/etc/passwd) 노출 추정,
           웹 취약점 2건(경로 조작 확인, SQL Injection 의심) 존재.
[미확인]   devuser 세션 12분 동안의 명령 (audit·쉘 기록 필요), 10.0.0.128 이 장악된 경위.

근본 원인(Root Cause) 은 하나가 아니다.

원인설명
비밀번호 인증 허용SSH PasswordAuthentication yes + 약한 devuser 비밀번호
SSH 접근 범위내부 전 대역에서 SSH 허용
웹 코드 취약점view.php 파일 경로 입력값 검증 없음, board.php 쿼리 조립
내부 거점10.0.0.128 자체가 이미 침해된 상태 (출발지가 내부 IP)

6. 보안 관점

6-1. 대응 (Response)

순서조치관련 글
1증거 보존 — 로그 사본·해시, 가능하면 메모리·프로세스 정보29·34·48편
2격리 — 10.0.0.128 네트워크 격리, 10.0.0.200의 SSH 접근 차단46편
3계정 — devuser 잠금·비밀번호 변경, authorized_keys·crontab 점검18·38·45편
4웹 — view.php·board.php 차단 후 수정 (입력값 검증, 준비된 쿼리)25편
5설정 강화 — PasswordAuthentication no, SSH 관리망 한정, fail2ban45·46·49편
6범위 확인 — 같은 IOC(10.0.0.128, 요청 패턴)를 다른 서버 로그에서 검색21·48편
7출발지 조사 — 10.0.0.128 호스트의 침해 경위전체
8탐지 개선 — "대입 후 성공", "웹 경로 조작 200" 규칙 추가49편

순서가 중요하다. 증거 보존 전에 격리·삭제부터 하면 원인을 밝힐 근거가 사라진다(34편 STOP → 증적 → KILL과 같은 원리).

6-2. 이 사건에서 배울 점

  • 한 로그만 보면 절반만 보인다. SSH 로그만 봤다면 "계정 탈취, 권한 상승 실패"로 끝났을 것이다. 웹 로그를 더하니 실제 정보 노출 이 드러났다.
  • 실패도 정보다. sudo·su 실패, shadow 0바이트는 공격자의 의도 와 방어가 작동한 지점 을 알려준다.
  • 내부 IP 출발 공격 은 그 자체로 또 다른 침해의 증거다.

7. SOC / 보안관제 활용

7-1. Linux 서버 초동 대응 체크리스트

시리즈에서 다룬 명령을 순서대로 정리했다. 증거가 휘발되는 순서(메모리·프로세스 → 네트워크 → 파일 → 로그)를 따른다.

단계확인 항목명령글
0. 기록 시작작업 기록, 시각script -a case.log, date; hostnamectl10편
1. 접속자현재 세션, 최근 로그인w, last -F -n 20, sudo lastb -n 2010·18편
2. 프로세스CPU 상위, 트리, 웹 계정 쉘ps -eo pid,ppid,user,etimes,args --forest, top -b -n131–35편
의심 프로세스 정보ls -l /proc/PID/exe, cat /proc/PID/cmdline \| tr '\0' ' '29편
동결kill -STOP PID34편
3. 네트워크열린 포트, 외부 연결sudo ss -tulnp, sudo ss -tnp state established44편
쉘이 쥔 소켓ls -l /proc/PID/fd \| grep socket43편
4. 지속성서비스·타이머systemctl list-unit-files --state=enabled, systemctl list-timers36·37편
cron/etc/crontab, /etc/cron.d, /var/spool/cron38편
SSH 키~/.ssh/authorized_keys 전 계정45편
5. 파일최근 변경, 숨김, SUIDfind / -xdev -newermt ..., find / -perm -400016·22편
무결성rpm -Va / debsums -s7·30편
6. 계정·권한UID 0, sudo 권한awk -F: '$3==0' /etc/passwd, /etc/sudoers.d/18–20편
7. 로그인증·서비스·커널·감사secure/auth.log, journalctl, ausearch48편
분석집계·타임라인21–28·49·50편
8. 방어 상태방화벽, SELinuxfirewall-cmd --list-all, getenforce, AVC46·47편
9. 자원디스크·메모리df -h; df -i, free -h, lsof +L139·40편
10. 보존사본·해시cp -p, sha256sum29편

주의: 침해가 의심되는 서버의 명령어(ps, ss, ls) 자체가 변조되었을 수 있다(7·9편). 결과가 이상하면 /proc을 직접 읽거나, 신뢰할 수 있는 정적 바이너리·외부 로그(SIEM)와 교차 확인한다.

7-2. 보고서 템플릿

1. 개요        사건명 / 탐지 경로 / 대상 서버 / 분석자 / 일시(시간대 명시)
2. 요약        3줄 이내: 무엇이 · 어떻게 · 영향
3. 타임라인    시각 | 출처(로그) | 행위 | 판정(시도/성공/영향)
4. 침해 지표   IP · 계정 · 파일 · 해시 · URL · 프로세스
5. 원인        기술적 원인(설정·취약점) / 관리적 원인
6. 조치        완료 / 진행 중 / 권고 (담당·기한)
7. 미확인 사항 추가 조사가 필요한 부분
8. 첨부        로그 사본 해시, 명령 기록(case.log)

8. 핵심 정리

  • 관제 흐름: Logs → Indicators → Root Cause → Response.
  • 여러 로그를 같은 시간 형식으로 바꾸고 피벗 값(IP·계정·PID)으로 묶어 정렬 하면 타임라인이 된다.
  • 반복은 요약, 특이점은 원문. 사실(200, 1893 B)과 추정(passwd 노출)을 구분 한다.
  • 이번 사건: SSH 대입 성공 → 권한 상승 실패 → 웹 탐색 → 경로 조작으로 파일 노출(추정) → SQL Injection 시도.
  • 대응은 증거 보존 → 격리 → 계정·취약점 조치 → 설정 강화 → 범위 확인 → 근본 원인 순서.
  • 초동 대응은 휘발성이 높은 것부터: 접속자 → 프로세스 → 네트워크 → 지속성 → 파일 → 로그.

9. 시리즈를 마치며

50편 동안 다룬 내용을 한 줄로 정리하면 다음과 같다.

Part글주제관제에서의 역할
11–10Linux 기본 구조 (커널·쉘·System Call·파일 시스템·부팅·환경변수)시스템이 어떻게 동작하는지 이해
211–20파일·명령어·권한 (inode·권한·SUID·사용자·sudo·최소 권한)누가 무엇을 할 수 있는지 판단
321–30명령어 활용 (grep·find·awk·sed·파이프·스크립트·보안 점검)로그와 파일에서 증거를 뽑아내는 기술
431–40프로세스·서비스·리소스 (ps·top·signal·systemd·cron·자원)지금 무엇이 실행 중인지, 지속성 탐지
541–50네트워크·보안·SOC (TCP·소켓·ss·SSH·방화벽·SELinux·로그·실습)어디와 통신했고 무엇이 남았는지 분석

각 Part는 앞 Part를 전제로 한다. 권한(2)을 알아야 로그의 NOT in sudoers를 해석할 수 있고, 명령어(3)를 알아야 로그를 집계할 수 있으며, 프로세스(4)와 네트워크(5)를 알아야 "이 연결은 누가 만들었는가"에 답할 수 있다.

보안관제는 도구를 많이 아는 것보다 "이 기록이 왜 여기에 남았는가"를 설명할 수 있는 것 이 중요하다고 생각한다. 이 시리즈가 그 설명의 기초가 되었으면 한다. 읽어 주셔서 감사합니다.


참고 자료

profile
코드에 숨겨진 위협을 읽고 AI로 보안의 미래를 설계합니다. 프론트엔드 개발 경험을 자산 삼아 더 견고하고 지능적인 보안 운영 시스템을 구축해 나가는 과정을 기록합니다

0개의 댓글