Pentest Playbook

침해사고분석 - auth.log

Linux 기반 환경에서 침해사고가 발생했을 때, 가장 먼저 확인하는 필수 로그에 해당됩니다. 외부에서 공격자가 침투했을 때 ssh를 이용하거나 명령어를 입력하면 인증/권한 관련하여 요청이 발생하고 요청을 처리할 때의 기록이 auth.log 파일에 기록되게 됩니다.

리눅스 환경에서 로그는 서로 다른 위치에 기록됩니다.

  1. /var/log/auth.log: Authentication(인증)과 Authorization(인가)에 해당되는 파일로 누가 접속했고, 어떤 권한을 사용하려는지 기록하는 파일에 해당
  2. /var/log/syslog: 시스템에서 발생하는 모든 로그가 저장되는 위치(kernel, systemd, apache2, sshd 등)
  3. /access/access.log or ~/error.log: 웹 서비스 요청에 의해 발생하는 로그 기록(XSS, SQL 등 웹 취약점)
  4. ~/.bash_history: Shell을 이용해서 사용자가 명령어를 입력한 내용을 기록한 곳(공격자가 쉘 획득 후, 입력한 명령어 저장)

MITRE ATT&CK에서의 공격 단계에 따라서 아래와 같은 기록이 남을 수 있습니다.

1. auth.log 분석 한계/주의점

로그 파일의 경우 공격자가 침투 후에 공격 흔적을 최소화하기 위해서 로그 파일을 삭제하거나 흔적으로 남는 라인만 삭제할 수 있습니다. 실무에서는 auth.log이외에 확인할 수 있는 다른 로그 정보가 필요할 수 있습니다.

sudo가 아닌 kernel취약점을 이용한다면, auth.log에는 입력한 명령어(COMMAND) 기록이 남지 않을 수 있습니다.

2. 실습환경

디렉토리 구조는 다음과 같으며 실습을 위해서 폴더/파일 생성 필요

.
├── app
│   └── app.py
├── docker-compose.yml
├── Dockerfile
└── logs
    ├── auth.log
    └── syslog

  1. docker-compose.yml
# docker-compose.yml
version: '3.8'

services:
  forensic-lab:
    build: .
    container_name: auth_analysis_lab
    ports:
      - "8082:5000"   # Flask
      - "2222:22"     # SSH (실습: ssh -p 2222 lab@localhost, 비밀번호 lab)
    volumes:
      - ./app:/app
      - ./logs/auth.log:/var/log/auth.log   # SSH/PAM 등 → 호스트 ./logs/auth.log
      - ./logs/syslog:/var/log/syslog       # rsyslog 일반 로그 → 호스트 ./logs/syslog
    privileged: true           # rsyslog 동작을 위한 권한
  1. Dockerfile
# Dockerfile
FROM ubuntu:22.04

# 필요 패키지 설치 (rsyslog, sudo, python3, openssh-server)
RUN apt-get update && apt-get install -y \
    rsyslog \
    sudo \
    python3 \
    python3-pip \
    vim \
    openssh-server \
    && rm -rf /var/lib/apt/lists/*

# Flask 설치
RUN pip3 install flask

# rsyslog 설정: 컨테이너에서 로그가 파일로 남도록 설정
RUN sed -i '/imklog/s/^/#/' /etc/rsyslog.conf

# SSH 실습: 비밀번호 로그인 계정 → pam_unix / sshd 메시지가 auth.log에 기록됨 (기본 auth,authpriv.* → /var/log/auth.log)
RUN useradd -m -s /bin/bash lab && echo 'lab:lab' | chpasswd
RUN printf '%s\n' \
    'PasswordAuthentication yes' \
    'PermitRootLogin no' \
    'UsePAM yes' \
    > /etc/ssh/sshd_config.d/99-forensics-lab.conf
RUN ssh-keygen -A

# 작업 디렉토리 설정
WORKDIR /app

# 로그 파일 (바인드 마운트 시 rsyslog가 동일 경로에 기록)
RUN touch /var/log/auth.log /var/log/syslog && chmod 666 /var/log/auth.log /var/log/syslog

# 시작 스크립트: rsyslog → sshd → Flask
RUN printf '%s\n' \
    '#!/bin/bash' \
    'set -e' \
    'rsyslogd || true' \
    'mkdir -p /run/sshd' \
    'ssh-keygen -A 2>/dev/null || true' \
    '/usr/sbin/sshd || true' \
    'exec python3 app.py' \
    > /start.sh && chmod +x /start.sh

CMD ["/bin/bash", "/start.sh"]
  1. app.py
# app/app.py
from html import escape
from flask import Flask, request
import os

app = Flask(__name__)

AUTH_LOG = "/var/log/auth.log"
SYSLOG = "/var/log/syslog"


def _tail_file(path: str, max_lines: int, max_bytes: int = 512_000) -> str:
    if not os.path.isfile(path):
        return f"(파일 없음: {path})"
    try:
        with open(path, "rb") as f:
            data = f.read(max_bytes)
        text = data.decode("utf-8", errors="replace")
        lines = text.splitlines()
        if len(lines) > max_lines:
            lines = lines[-max_lines:]
        return "\n".join(lines) if lines else "(비어 있음)"
    except OSError as e:
        return f"(읽기 오류: {e})"


@app.route("/")
def index():
    return """<!DOCTYPE html>
<html lang="ko"><head><meta charset="utf-8"><title>Forensics Lab</title></head>
<body>
  <p>Forensics Lab — Try to exploit me!</p>
  <p>SSH: <code>ssh -p 2222 lab@127.0.0.1</code> (password: <code>lab</code>) → <code>./logs/auth.log</code></p>
  <p>로그 보기: <a href="/logs">/logs</a> (auth.log + syslog tail)</p>
</body></html>"""


@app.route("/logs")
def logs_page():
    """auth.log / syslog 마지막 줄들을 브라우저에서 확인 (실습용)."""
    try:
        n = int(request.args.get("n", "200"))
    except ValueError:
        n = 200
    n = max(1, min(n, 2000))

    auth_tail = _tail_file(AUTH_LOG, n)
    syslog_tail = _tail_file(SYSLOG, n)
    return f"""<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>auth.log / syslog</title>
  <style>
    body {{ font-family: ui-monospace, Consolas, monospace; margin: 16px; background: #1a1a1a; color: #e8e8e8; }}
    h2 {{ font-size: 1rem; margin: 1.2em 0 0.4em; color: #9cf; }}
    pre {{ white-space: pre-wrap; word-break: break-all; background: #0d0d0d; padding: 12px; border: 1px solid #333; border-radius: 6px; }}
    a {{ color: #8cf; }}
    .hint {{ color: #888; font-size: 0.85rem; margin-bottom: 1em; }}
  </style>
</head>
<body>
  <p class="hint">쿼리: <code>?n=줄수</code> (기본 200, 최대 2000) · <a href="/">홈</a></p>
  <h2>auth.log (tail)</h2>
  <pre>{escape(auth_tail)}</pre>
  <h2>syslog (tail)</h2>
  <pre>{escape(syslog_tail)}</pre>
</body>
</html>"""

@app.route('/cmd')
def run_cmd():
    # 실제 침해 사고 상황을 시연하기 위한 취약한 엔드포인트
    cmd = request.args.get('c')
    if cmd:
        os.system(cmd)
        return f"Executed: {cmd}"
    return "No command provided."

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
ESC

💡 검색 팁

  • #T1572 - 태그로 검색
  • persistence - 키워드로 검색