📚 네트워크 · 패킷 분석 › 02. 포트 · 프로토콜 분석 — 63편
이전 글: 62. HTTPS와 TLS Handshake — 암호화된 통신에서 볼 수 있는 것 · 다음 글: 64. SFTP
참고(리눅스 시스템 기초): Firewalld 이해 — 서비스 단위 포트 허용
FTP(File Transfer Protocol)는 다른 서비스와 달리 연결이 두 개입니다. 명령을 주고받는 제어 연결과 실제 파일이 오가는 데이터 연결이 분리되어 있고, 모드에 따라 데이터 연결을 여는 방향이 반대입니다. 이 구조를 모르면 다음과 같은 상황에서 판단이 흔들립니다.
또한 FTP는 평문 프로토콜이라 계정 정보가 노출되고, 외부에 열린 FTP는 무차별 대입과 익명 접속 점검 대상이 됩니다. 관제에서 FTP는 "정보 유출 경로"와 "인증 공격 대상" 두 관점으로 봅니다.
| 연결 | 포트 | 내용 | 수명 |
|---|---|---|---|
| 제어 연결(Control) | 서버 TCP 21 | USER, PASS, LIST, RETR 등 명령과 응답 코드 | 세션 동안 유지 |
| 데이터 연결(Data) | Active: 서버 20 → 클라이언트 임의 포트 / Passive: 클라이언트 → 서버 임의 포트 | 파일 내용, 디렉터리 목록 | 전송 1건마다 새로 열고 닫음 |
| 구분 | Active 모드 | Passive 모드 |
|---|---|---|
| 데이터 연결을 여는 쪽 | 서버 → 클라이언트 | 클라이언트 → 서버 |
| 핵심 명령 | PORT (IPv6 겸용: EPRT) | PASV (IPv6 겸용: EPSV) |
| 서버 출발 포트 | 20 | 서버가 알려준 임의 포트 |
| 클라이언트 방화벽·NAT 영향 | 서버의 인바운드 접속이 막히기 쉬움 | 영향 적음 (오늘날 기본) |
| 서버 방화벽 | 20번 아웃바운드 허용 | Passive 포트 범위 인바운드 허용 필요 |
| 명령 | 의미 | 응답 코드 | 의미 | |
|---|---|---|---|---|
| USER | 계정명 전송 | 220 | 서비스 준비됨(접속 배너) | |
| PASS | 비밀번호 전송(평문) | 331 | 계정 확인, 비밀번호 필요 | |
| PWD / CWD | 현재 경로 / 경로 이동 | 230 | 로그인 성공 | |
| LIST | 디렉터리 목록 | 530 | 로그인 실패(인증 거부) | |
| RETR | 파일 다운로드 | 150 | 데이터 연결 열기 시작 | |
| STOR | 파일 업로드 | 226 | 전송 완료, 데이터 연결 닫음 | |
| DELE | 파일 삭제 | 227 / 229 | Passive / Extended Passive 진입 | |
| QUIT | 종료 | 550 | 파일 없음·권한 없음 |
| 방식 | 포트 | 특징 |
|---|---|---|
| FTPS (명시적) | 21 | 접속 후 AUTH TLS로 TLS 전환 |
| FTPS (암묵적) | 990 | 접속 즉시 TLS |
| SFTP | 22 | FTP가 아니라 SSH 위에서 동작하는 별도 프로토콜 |

그림 1. 제어 연결(21)과 데이터 연결의 방향 비교
클라이언트 192.168.10.20 서버 192.168.10.10
│ (임의 포트 50000) ── 제어 연결 ──────────────────→ :21
│ ←── 220 / USER / 331 / PASS / 230 ──────────────→ │
│ │
│ ── PORT 192,168,10,20,195,81 ───────────────────→ │ "내 50001 포트로 접속해"
│ (195×256 + 81 = 50001)
│ ←── 200 PORT command successful ──────────────── │
│ ── LIST ────────────────────────────────────────→ │
│ │
│ :50001 ←═════ 데이터 연결 (서버 :20 → 클라이언트) ═══ │ ← 서버가 먼저 접속
│ ←═════ 디렉터리 목록 ═══════════════════════════════ │
│ ←── 226 Transfer complete ─────────────────────── │
클라이언트 192.168.10.20 서버 192.168.10.10
│ ── 제어 연결 ───────────────────────────────────→ :21
│ ── PASV ────────────────────────────────────────→ │
│ ←── 227 Entering Passive Mode (192,168,10,10,156,65) │
│ (156×256 + 65 = 40001)
│ ── LIST ────────────────────────────────────────→ │
│ ═══════ 데이터 연결 (클라이언트 → 서버 :40001) ══════→ │ ← 클라이언트가 접속
│ ←═════ 디렉터리 목록 ═══════════════════════════════ │
│ ←── 226 Transfer complete ─────────────────────── │
PORT/227 응답의 마지막 두 숫자는 상위 바이트, 하위 바이트이므로 p1 × 256 + p2로 포트를 계산합니다.
데이터 포트가 매번 바뀌기 때문에, 상태 기반 방화벽은 제어 연결의 PORT/PASV 응답을 읽어 데이터 연결을 "관련(RELATED) 연결"로 허용합니다. 리눅스에서는 nf_conntrack_ftp 헬퍼가 이 역할을 하며, firewalld의 ftp 서비스 정의는 이 헬퍼를 함께 사용합니다. 단, 제어 연결이 FTPS로 암호화되면 방화벽이 내용을 읽을 수 없으므로 Passive 포트 범위를 고정해서 직접 허용해야 합니다.
실습 환경: 본인 소유 Linux VM (Rocky Linux 또는 Ubuntu), 인터페이스는 ens33을 예시로 사용합니다. 접속은 루프백(127.0.0.1)에서만 하고, 실습 전용 계정을 사용합니다.
# Rocky Linux
sudo dnf install -y vsftpd ftp tcpdump
# Ubuntu
sudo apt install -y vsftpd ftp tcpdump
설정 파일 위치가 배포판마다 다릅니다.
| 항목 | Rocky Linux | Ubuntu |
|---|---|---|
| 설정 파일 | /etc/vsftpd/vsftpd.conf | /etc/vsftpd.conf |
| 서비스 이름 | vsftpd | vsftpd |
| 인증 로그 | /var/log/secure | /var/log/auth.log |
설정 파일을 백업한 뒤 아래 항목을 확인·수정합니다(이미 있는 항목은 값만 변경).
sudo cp /etc/vsftpd/vsftpd.conf /etc/vsftpd/vsftpd.conf.bak # Ubuntu는 /etc/vsftpd.conf
anonymous_enable=NO # 익명 접속 금지
local_enable=YES # 로컬 계정 로그인 허용
write_enable=YES
chroot_local_user=YES # 홈 디렉터리 밖 이동 제한
allow_writeable_chroot=YES # 쓰기 가능한 홈에서 chroot 허용 (실습용)
pasv_enable=YES
pasv_min_port=40000 # Passive 포트 범위 고정
pasv_max_port=40010
xferlog_enable=YES # 전송 로그 활성화
xferlog_std_format=YES # /var/log/xferlog (표준 형식)
dual_log_enable=YES # vsftpd 자체 로그도 함께 기록
vsftpd_log_file=/var/log/vsftpd.log
참고로 log_ftp_protocol=YES를 쓰면 명령·응답까지 기록되지만, vsftpd 매뉴얼상 xferlog_std_format이 꺼져 있을 때만 적용되므로 이 실습에서는 사용하지 않습니다.
sudo useradd -m ftptest && sudo passwd ftptest
echo "sample data" | sudo tee /home/ftptest/sample.txt
sudo systemctl restart vsftpd
sudo ss -tlnp | grep ':21 '
Rocky Linux에서 SELinux가 로컬 계정 홈 접근을 막으면 sudo setsebool -P ftpd_full_access on이 필요할 수 있습니다(실습 후 off로 되돌림).
# 터미널 1
sudo tcpdump -i lo -nn -A 'tcp port 21 or portrange 40000-40010 or tcp port 20'
# 터미널 2: ftp 클라이언트 접속
ftp 127.0.0.1
# 로그인 후 ftp> 프롬프트에서:
# passive ← Passive 모드 켜기/끄기 토글 (현재 상태가 출력됨)
# ls
# get sample.txt
# bye
passive를 한 번 더 입력해 모드를 바꾼 뒤 ls를 다시 하면, 캡처에서 PASV/227 대신 PORT/200이 보입니다(클라이언트 구현에 따라 EPSV/EPRT가 먼저 시도될 수 있음).
📷 [실습 화면 삽입] tcpdump -A에서
USER ftptest,PASS ...(가림 처리),227 Entering Passive Mode (...)가 보이는 부분
sudo tail -n 20 /var/log/vsftpd.log
sudo tail -n 5 /var/log/xferlog
📷 [실습 화면 삽입] vsftpd.log의
OK LOGIN,OK DOWNLOAD줄과 xferlog의 전송 기록
sudo systemctl disable --now vsftpd
sudo userdel -r ftptest
vsftpd.log (형식 예시(값은 환경마다 다름))
Sat Sep 26 11:02:10 2026 [pid 3101] CONNECT: Client "::ffff:127.0.0.1"
Sat Sep 26 11:02:14 2026 [pid 3100] [ftptest] OK LOGIN: Client "::ffff:127.0.0.1"
Sat Sep 26 11:02:30 2026 [pid 3102] [ftptest] OK DOWNLOAD: Client "::ffff:127.0.0.1", "/sample.txt", 12 bytes, 3.10Kbyte/sec
Sat Sep 26 11:05:41 2026 [pid 3110] [baduser] FAIL LOGIN: Client "::ffff:127.0.0.1"
xferlog 표준 형식 (형식 예시(값은 환경마다 다름))
Sat Sep 26 11:02:30 2026 1 ::ffff:127.0.0.1 12 /sample.txt b _ o r ftptest ftp 0 * c
| xferlog 필드 | 예시 값 | 의미 |
|---|---|---|
| 전송 시간(초) | 1 | 전송에 걸린 시간 |
| 원격 호스트 | ::ffff:127.0.0.1 | 클라이언트 주소 |
| 파일 크기 | 12 | 바이트 |
| 파일 이름 | /sample.txt | chroot 기준 경로 |
| 방향 | o | o = 다운로드(outgoing), i = 업로드(incoming) |
| 접근 모드 | r | r = 로컬 계정, a = 익명 |
| 완료 상태 | c | c = 완료, i = 미완료 |
점검 체크리스트
227 응답의 포트를 p1×256+p2로 계산해 실제 데이터 포트와 비교했다USER/PASS가 평문으로 캡처되는 것을 확인했다OK LOGIN/FAIL LOGIN/OK DOWNLOAD를 확인했다| 관찰 | 정상일 수 있는 경우 | 의심해야 하는 경우 |
|---|---|---|
| 서버 :20 → 클라이언트 접속 | Active 모드 정상 동작 | FTP 제어 세션 없이 20번 출발 연결만 존재 |
FAIL LOGIN 반복 | 사용자 비밀번호 오입력 몇 회 | 짧은 시간 다수 계정·다수 시도 (무차별 대입) |
익명(anonymous) 로그인 성공 | 공개 배포용 익명 FTP로 문서화됨 | 익명 비허용 정책인데 성공 |
대량 o(다운로드) 전송 | 정기 백업·배포 작업 시간대 | 업무 외 시간, 평소 없던 계정, 대용량 |
i(업로드) 후 웹 경로 파일 생성 | 콘텐츠 배포 | FTP 루트가 웹 루트와 같고 스크립트 파일 업로드 |
PORT에 다른 IP 지정 | NAT 환경 설정 문제 | 클라이언트와 다른 제3의 IP 지정 (FTP Bounce 원리) |
| 내부 → 외부 FTP 업로드 | 협력사 파일 교환 승인 건 | 미승인 목적지로 대용량 업로드 (유출 의심) |
FTP Bounce는 PORT 명령에 클라이언트가 아닌 다른 주소를 넣어 서버가 제3자에게 접속하도록 만드는 오래된 기법입니다. 현대 FTP 서버는 대부분 기본적으로 이를 거부하므로, 로그에 PORT 관련 거부가 보이면 시도 흔적으로 봅니다.
# 로그인 실패 출발지별 집계
sudo grep "FAIL LOGIN" /var/log/vsftpd.log | grep -o 'Client "[^"]*"' | sort | uniq -c | sort -rn
# 다운로드/업로드 파일 목록
sudo grep -E "OK (DOWNLOAD|UPLOAD)" /var/log/vsftpd.log
# 현재 FTP 제어·데이터 연결
sudo ss -tnp '( sport = :21 or sport = :20 or ( sport >= :40000 and sport <= :40010 ) )'
흔적이 남는 곳
| 위치 | 남는 정보 | 비고 |
|---|---|---|
| 방화벽 | 21번 세션 + 데이터 세션(RELATED) | 두 세션을 연결해서 봐야 전체 행위가 보임 |
| FTP 서버 로그 | 로그인 성공/실패, 명령, 전송 파일·크기 | 가장 구체적인 증거 |
| IDS/IPS | 평문 명령 기반 탐지(익명 로그인, 특정 파일명) | FTPS면 내용 탐지 불가 (06 방화벽·IDS 시리즈에서 다룸) |
| DLP·프록시 | 외부 FTP 업로드 차단/기록 | 조직 정책에 따라 다름 |
관제 시 핵심 포인트
한계와 오탐 주의
p1×256+p2로 계산합니다.