리눅스 시스템 기초 · 입문편 — 본문의 "N편"은 입문 과정 번호다. 번호별 글과 전체 250편 구성은 통합 로드맵에서 확인할 수 있다.
리눅스 시스템 기초 50 / 50 · Part 5. 네트워크·보안·SOC
실습 환경: 21편 샘플 로그(secure+access_log) · 타임라인·집계는 모두 샘플 로그에 스크립트를 실행한 실제 출력
이전 글: 49. SSH Brute Force 로그 분석
시리즈의 마지막 글이다. 1편에서 보안관제의 흐름을 이렇게 정리했다.
Logs → Indicators → Root Cause → Response
로그 침해 지표 원인 분석 대응
이번 글은 이 흐름을 처음부터 끝까지 한 번 따라간다. 49편에서 SSH 로그만으로 "10.0.0.128이 devuser 계정을 탈취했다"는 것까지 확인했다. 이번에는 웹 로그(access_log)를 더해 같은 공격자가 그 뒤에 무엇을 했는지 재구성한다.
실제 관제에서 경보는 대개 하나의 로그 소스에서 시작하지만, 판단은 여러 소스를 하나의 시간축에 올려야 내릴 수 있다. 이번 글의 목표는 세 가지다.
| 원칙 | 이유 |
|---|---|
| 시간대를 통일 한다 | syslog에는 연도·시간대가 없고, 웹 로그는 +0900이 있다(48편) |
| 피벗(pivot) 값 으로 묶는다 | IP, 계정, PID, 세션처럼 여러 로그에 공통으로 나오는 값 |
| 반복은 요약, 특이점은 원문 | 404 30줄은 "404 × 30", 200 응답은 한 줄씩 |
| 사실과 추정을 구분 한다 | "200, 1893 B"는 사실, "passwd가 노출됨"은 추정 |
| 판정 | 의미 | 예 |
|---|---|---|
| 시도 (Attempt) | 공격 행위가 있었으나 결과 불명·실패 | 404, SYN만 있음, Failed password |
| 성공 (Success) | 목적 달성의 직접 근거 | Accepted, 200 + 의미 있는 응답 크기 |
| 영향 (Impact) | 성공으로 인해 생긴 피해 | 계정 탈취, 파일 노출, 서비스 중단 |

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로 맞춰야 한다.
$ ./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줄의 이야기 로 줄었다.
# 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
로그 분석으로 "무엇을 시도했는가"를 알았다면, 서버에서 "무엇이 남았는가"를 확인한다.
# 세션 중 생긴 파일 (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
| 시각 | 행위 | 근거 | 판정 |
|---|---|---|---|
| 02:10–02:19 | SSH 사전·무차별 대입 | 실패 95회, 분당 8~14회 | 시도 |
| 02:19:21 | devuser 로그인 | Accepted password | 성공 |
| 02:21–02:22 | root 권한 상승 시도 | NOT in sudoers, FAILED SU | 실패 |
| 02:30:01–51 | 웹 경로 탐색 | 404 × 30 (/admin, /.git/config, /backup.zip …) | 시도 (정보 수집) |
| 02:30:53 | 경로 조작 → /etc/passwd | 200, 1893 B | 성공 추정 |
| 02:30:57 | 경로 조작 → /etc/shadow | 200, 0 B | 실패 추정 (권한 없음) |
| 02:31:00 | 경로 조작 → /etc/hosts | 200, 0 B | 실패 추정 (필터 우회 실패) |
| 02:31:05–07 | SQL Injection | 500 | 시도 — 오류 유발, 데이터 유출 여부 확인 필요 |
응답 크기를 읽는 법. 경로 조작 요청의 응답이 200이라도 크기가 0이면 파일을 읽지 못했을 가능성이 높다. 반면 1893바이트는 일반적인 /etc/passwd 크기와 비슷하다. 그래서 "passwd가 노출되었다"는 강한 추정 이지만, 최종 확인은 웹 서버에서 같은 요청을 재현하거나 view.php 코드를 보고 한다. /etc/shadow가 0바이트인 것은 웹 서버 계정(apache)이 읽을 권한이 없기 때문일 가능성이 높다(15·18편: shadow는 root만 읽기). 권한 설계가 피해를 막은 사례 다.
SQL Injection의 500. 서버 오류는 입력이 SQL 문에 그대로 들어가 문법 오류를 일으켰다 는 신호일 수 있다. 즉 취약점이 존재할 가능성 이 높다. 이번 요청으로 데이터가 나가지는 않았더라도 조치 대상이다.
[무엇이] 내부 호스트 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) |
| 순서 | 조치 | 관련 글 |
|---|---|---|
| 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 관리망 한정, fail2ban | 45·46·49편 |
| 6 | 범위 확인 — 같은 IOC(10.0.0.128, 요청 패턴)를 다른 서버 로그에서 검색 | 21·48편 |
| 7 | 출발지 조사 — 10.0.0.128 호스트의 침해 경위 | 전체 |
| 8 | 탐지 개선 — "대입 후 성공", "웹 경로 조작 200" 규칙 추가 | 49편 |
순서가 중요하다. 증거 보존 전에 격리·삭제부터 하면 원인을 밝힐 근거가 사라진다(34편 STOP → 증적 → KILL과 같은 원리).
시리즈에서 다룬 명령을 순서대로 정리했다. 증거가 휘발되는 순서(메모리·프로세스 → 네트워크 → 파일 → 로그)를 따른다.
| 단계 | 확인 항목 | 명령 | 글 |
|---|---|---|---|
| 0. 기록 시작 | 작업 기록, 시각 | script -a case.log, date; hostnamectl | 10편 |
| 1. 접속자 | 현재 세션, 최근 로그인 | w, last -F -n 20, sudo lastb -n 20 | 10·18편 |
| 2. 프로세스 | CPU 상위, 트리, 웹 계정 쉘 | ps -eo pid,ppid,user,etimes,args --forest, top -b -n1 | 31–35편 |
| 의심 프로세스 정보 | ls -l /proc/PID/exe, cat /proc/PID/cmdline \| tr '\0' ' ' | 29편 | |
| 동결 | kill -STOP PID | 34편 | |
| 3. 네트워크 | 열린 포트, 외부 연결 | sudo ss -tulnp, sudo ss -tnp state established | 44편 |
| 쉘이 쥔 소켓 | ls -l /proc/PID/fd \| grep socket | 43편 | |
| 4. 지속성 | 서비스·타이머 | systemctl list-unit-files --state=enabled, systemctl list-timers | 36·37편 |
| cron | /etc/crontab, /etc/cron.d, /var/spool/cron | 38편 | |
| SSH 키 | ~/.ssh/authorized_keys 전 계정 | 45편 | |
| 5. 파일 | 최근 변경, 숨김, SUID | find / -xdev -newermt ..., find / -perm -4000 | 16·22편 |
| 무결성 | rpm -Va / debsums -s | 7·30편 | |
| 6. 계정·권한 | UID 0, sudo 권한 | awk -F: '$3==0' /etc/passwd, /etc/sudoers.d/ | 18–20편 |
| 7. 로그 | 인증·서비스·커널·감사 | secure/auth.log, journalctl, ausearch | 48편 |
| 분석 | 집계·타임라인 | 21–28·49·50편 | |
| 8. 방어 상태 | 방화벽, SELinux | firewall-cmd --list-all, getenforce, AVC | 46·47편 |
| 9. 자원 | 디스크·메모리 | df -h; df -i, free -h, lsof +L1 | 39·40편 |
| 10. 보존 | 사본·해시 | cp -p, sha256sum | 29편 |
주의: 침해가 의심되는 서버의 명령어(
ps,ss,ls) 자체가 변조되었을 수 있다(7·9편). 결과가 이상하면/proc을 직접 읽거나, 신뢰할 수 있는 정적 바이너리·외부 로그(SIEM)와 교차 확인한다.
1. 개요 사건명 / 탐지 경로 / 대상 서버 / 분석자 / 일시(시간대 명시)
2. 요약 3줄 이내: 무엇이 · 어떻게 · 영향
3. 타임라인 시각 | 출처(로그) | 행위 | 판정(시도/성공/영향)
4. 침해 지표 IP · 계정 · 파일 · 해시 · URL · 프로세스
5. 원인 기술적 원인(설정·취약점) / 관리적 원인
6. 조치 완료 / 진행 중 / 권고 (담당·기한)
7. 미확인 사항 추가 조사가 필요한 부분
8. 첨부 로그 사본 해시, 명령 기록(case.log)
50편 동안 다룬 내용을 한 줄로 정리하면 다음과 같다.
| Part | 글 | 주제 | 관제에서의 역할 |
|---|---|---|---|
| 1 | 1–10 | Linux 기본 구조 (커널·쉘·System Call·파일 시스템·부팅·환경변수) | 시스템이 어떻게 동작하는지 이해 |
| 2 | 11–20 | 파일·명령어·권한 (inode·권한·SUID·사용자·sudo·최소 권한) | 누가 무엇을 할 수 있는지 판단 |
| 3 | 21–30 | 명령어 활용 (grep·find·awk·sed·파이프·스크립트·보안 점검) | 로그와 파일에서 증거를 뽑아내는 기술 |
| 4 | 31–40 | 프로세스·서비스·리소스 (ps·top·signal·systemd·cron·자원) | 지금 무엇이 실행 중인지, 지속성 탐지 |
| 5 | 41–50 | 네트워크·보안·SOC (TCP·소켓·ss·SSH·방화벽·SELinux·로그·실습) | 어디와 통신했고 무엇이 남았는지 분석 |
각 Part는 앞 Part를 전제로 한다. 권한(2)을 알아야 로그의 NOT in sudoers를 해석할 수 있고, 명령어(3)를 알아야 로그를 집계할 수 있으며, 프로세스(4)와 네트워크(5)를 알아야 "이 연결은 누가 만들었는가"에 답할 수 있다.
보안관제는 도구를 많이 아는 것보다 "이 기록이 왜 여기에 남았는가"를 설명할 수 있는 것 이 중요하다고 생각한다. 이 시리즈가 그 설명의 기초가 되었으면 한다. 읽어 주셔서 감사합니다.