리눅스 시스템 기초 · 입문편 — 본문의 "N편"은 입문 과정 번호다. 번호별 글과 전체 250편 구성은 통합 로드맵에서 확인할 수 있다.
리눅스 시스템 기초 22 / 50 · Part 3. Linux 명령어 활용
실습 환경: Rocky Linux 9 (10.0.0.200) · 실습용 파일 구조~/lab/fs(아래 생성 스크립트)
이전 글: 21. grep으로 로그와 문자열 검색
🔗 심화 시리즈 — 「파일 · 권한 · 사용자 관리」
이 주제를 더 깊게 다룬 실무형 보안 시리즈: 46. World-Writable 파일 점검 · 47. SUID/SGID 파일 보안 점검
21편의 grep이 파일 안의 내용 을 검색한다면, find는 파일 시스템에서 파일 자체 를 찾는다. 이름뿐 아니라 종류·크기·권한·소유자·시각을 조건으로 조합할 수 있어서, 침해사고 분석에서 "공격자가 남긴 파일"을 찾는 헌팅 도구 로 쓰인다.
이미 여러 글에서 find를 썼다.
| 편 | 사용한 조건 | 목적 |
|---|---|---|
| 7편 | -mtime -1 | 최근 변경된 /etc 파일 |
| 13편 | -type s, -type p | 임시 경로의 소켓·파이프 |
| 14편 | -newerct | 사고 시간대 타임라인 |
| 16편 | -perm -4000 | SUID 파일 |
| 17편 | -nouser | 소유자 없는 파일 |
이번 글은 이 조건들을 체계적으로 정리하고 여러 조건을 조합하는 방법 을 다룬다.
실제 서버를 건드리지 않도록, 홈 디렉터리 아래에 침해 흔적을 흉내 낸 파일 구조를 만든다. 웹셸처럼 보이는 파일은 문자열만 들어 있는 텍스트 이고 실행하지 않는다.
#!/bin/bash
# make_lab_fs.sh — find 실습용 파일 구조 생성 (~/lab/fs 아래에만 생성)
set -e
R=~/lab/fs; rm -rf "$R"; mkdir -p "$R"/{www/html/upload,tmp/.cache,etc/cron.d,home/devuser,var/log}
cd "$R"
# 정상 웹 파일 (과거 시각)
for f in index.php about.php view.php; do echo "<?php // page ?>" > www/html/$f; done
touch -d '2026-08-01 10:00' www/html/*.php
cp /usr/bin/true www/html/upload/logo.png.tmp 2>/dev/null; mv www/html/upload/logo.png.tmp www/html/upload/thumb.jpg
echo '<?php /* 실습용 표식: 웹셸 의심 파일 */ ?>' > www/html/upload/.cache.php # 웹셸 흉내 (기능 없음)
# 임시 경로의 숨김 실행 파일과 큰 파일
cp /usr/bin/true tmp/.cache/kw && chmod 755 tmp/.cache/kw
head -c 60M /dev/zero > tmp/.cache/dump.bin
# SUID가 붙은 쉘 사본 흉내 (실행 권한 비트만 실습용으로 설정)
cp /usr/bin/true tmp/.cache/bash && chmod 4755 tmp/.cache/bash
# 지속성 흉내: cron 파일
echo '*/5 * * * * root /tmp/.cache/kw' > etc/cron.d/sys-update
# 누구나 쓸 수 있는 스크립트
echo 'tar czf /backup/home.tgz /home' > etc/backup.sh && chmod 777 etc/backup.sh
# 시각 위조 흉내: mtime만 과거로
echo 'x' > www/html/config.php && touch -d '2019-06-12 08:00' www/html/config.php
# 로그 파일
seq 1 1000 > var/log/app.log; : > var/log/empty.log
echo "[완료] $R"; find "$R" | wc -l
find [시작 경로...] [범위 옵션] [조건...] [동작]
find /var/www -xdev -type f -name '*.php' -mmin -60 -ls
| 구분 | 예 | 의미 |
|---|---|---|
| 시작 경로 | /, /var/www, . | 어디서부터 |
| 범위 | -xdev, -maxdepth 2 | 다른 파일 시스템 제외, 깊이 제한 |
| 조건 | -type f, -name | 무엇을 |
| 동작 | -print(기본), -ls, -exec, -print0 | 찾으면 무엇을 |
| 분류 | 조건 | 의미 |
|---|---|---|
| 이름 | -name '*.php', -iname | 이름 (대소문자 무시) |
| 경로 | -path '*upload*' | 전체 경로 패턴 |
| 종류 | -type f/d/l/s/p | 파일 종류 (13편) |
| 크기 | -size +50M, -empty | 크기, 빈 파일 |
| 권한 | -perm 644(정확히), -perm -4000(해당 비트 모두 포함), -perm /6000(하나라도) | |
| 소유 | -user apache, -group, -nouser, -nogroup | |
| 시각 (일 단위) | -mtime -1, -ctime +30, -atime | 24시간 단위 (-1 = 1일 이내) |
| 시각 (분 단위) | -mmin -60, -cmin | 60분 이내 |
| 시각 (기준) | -newer 파일, -newermt '날짜', -newerct | 기준보다 최근 |
| 표기 | 의미 |
|---|---|
| 나란히 쓰기 | AND |
-o | OR |
! 또는 -not | NOT |
\( ... \) | 묶기 (쉘이 해석하지 않도록 \) |
-o는 AND보다 우선순위가 낮다. 그래서 find . -type f -perm -4000 -o -perm -2000은 의도와 다르게 "(파일이면서 SUID) 또는 (SGID인 모든 것)"이 된다. OR를 쓸 때는 괄호로 묶는다.
find / -xdev -type f \( -perm -4000 -o -perm -2000 \)

find는 시작 경로부터 디렉터리를 재귀적으로 내려가며, 각 항목에 대해 조건을 왼쪽부터 평가 한다. AND로 연결된 조건은 앞 조건이 거짓이면 뒤 조건을 평가하지 않는다. 그래서 가벼운 조건(-type, -name)을 앞에, 무거운 조건(-exec)을 뒤에 두는 것이 빠르다.
각 파일마다 inode 정보(stat)를 읽어 조건을 비교하므로, / 전체를 검색하면 시간이 오래 걸리고 디스크 I/O가 발생한다. 운영 중인 서버에서는 시작 경로를 좁히고 -xdev로 범위를 제한 하는 것이 기본이다.
cd ~/lab/fs
# 1) 오늘 생성·변경된 파일
find . -type f -newermt "$(date +%F)" | sort
# 2) SUID/SGID 파일 (괄호 주의)
find . -type f \( -perm -4000 -o -perm -2000 \) -ls
# 3) 숨김 파일·디렉터리
find . -name '.*' ! -name '.'
# 4) 임시·웹 경로의 실행 파일 (확장자 무관, 13편)
find tmp www -type f -exec file {} + | grep -i elf
# 5) 50MB 넘는 파일
find . -type f -size +50M -exec ls -lh {} +
# 6) 누구나 쓸 수 있는 파일
find . -type f -perm -0002
# 7) 업로드 폴더의 스크립트
find www -path '*upload*' -type f \( -name '*.php' -o -name '*.jsp' \)
# 8) 시각 위조 의심 — mtime은 오래됐는데 ctime은 오늘
find www -type f ! -newermt '2026-01-01' -newerct "$(date +%F)"
stat -c '%n mtime=%y ctime=%z' www/html/config.php
# 9) 결과를 안전하게 다음 명령으로 (공백 포함 이름 대비)
find tmp -type f -print0 | xargs -0 sha256sum
아래는 ~/lab/fs에 실행한 실제 결과다.
① 오늘 생성·변경된 파일
./etc/backup.sh
./etc/cron.d/sys-update
./tmp/.cache/bash
./tmp/.cache/dump.bin
./tmp/.cache/kw
./var/log/app.log
./var/log/empty.log
./www/html/upload/.cache.php
./www/html/upload/thumb.jpg
웹 루트의 정상 페이지(index.php 등)는 8월 시각이라 목록에 없다. 변경 목록이 곧 조사 목록 이 된다.
② ~ ⑧ 헌팅 결과 요약
| 쿼리 | 결과 | 해석 |
|---|---|---|
| SUID | ./tmp/.cache/bash (-rwsr-xr-x root) | 임시 경로의 root SUID 쉘 사본 → 지속성 백도어 패턴 (16편) |
| 숨김 | ./tmp/.cache, ./www/html/upload/.cache.php | 숨김 디렉터리 + 업로드 폴더의 숨김 PHP |
| ELF | tmp/.cache/kw, tmp/.cache/bash, upload/thumb.jpg | 이미지 이름의 실행 파일 포함 (13편) |
| 큰 파일 | 60M ./tmp/.cache/dump.bin | 데이터 수집·유출 준비 가능성 |
| world-writable | ./etc/backup.sh | 누구나 수정 가능한 스크립트 (15편) |
| 업로드 스크립트 | www/html/upload/.cache.php | 웹셸 의심 |
| 시각 위조 | www/html/config.php | mtime 2019-06-12 08:00:00.000000000, ctime 오늘 (14편) |
그리고 etc/cron.d/sys-update의 내용을 보면 /tmp/.cache/kw를 5분마다 root로 실행하도록 되어 있다. find 결과끼리 연결 하면 "임시 경로의 실행 파일 + 그것을 주기 실행하는 cron"이라는 지속성 구조가 드러난다.
| 주의점 | 이유 | 방법 |
|---|---|---|
-delete, -exec rm 사용 금지 | 증거 훼손 | 분석 단계에서는 목록·해시만 |
| 공백·특수문자 파일명 | `find | xargs`가 이름을 잘라 오작동 |
/proc, 네트워크 마운트 검색 | 느리고 무의미한 결과, 원격 저장소 부하 | -xdev, -path /proc -prune |
-mtime 단위 오해 | -mtime -1은 "24시간 이내", +1은 "48시간 이전" | 정확한 시각은 -newermt |
| mtime만 기준 | 공격자가 touch로 위조 가능 | ctime(-newerct) 함께 사용 |
| 권한 오류 출력 | 결과가 오류 메시지에 묻힘 | 2>/dev/null (27편 리다이렉션) |
T='2026-09-23 02:00' # 사고 추정 시작 시각
# 사고 이후 변경된 시스템 파일 (ctime 기준)
sudo find /etc /usr/bin /usr/sbin /var/spool/cron -xdev -newerct "$T" -ls 2>/dev/null
# 임시 경로의 실행 파일
sudo find /tmp /var/tmp /dev/shm -type f -perm /111 -ls 2>/dev/null
# 웹 루트의 최근 스크립트
sudo find /var/www -type f \( -name '*.php' -o -name '*.jsp' \) -newerct "$T" -ls
# 새 SUID (16편 기준값과 비교)
sudo find / -xdev -type f -perm -4000 2>/dev/null | sort | comm -13 /root/baseline_suid.txt -
# 홈 디렉터리의 SSH 키 변경
sudo find /root /home -name authorized_keys -newerct "$T" -ls 2>/dev/null
-perm /111은 "소유자·그룹·기타 중 하나라도 실행 권한이 있는" 파일이다.
[Event] 21편: 02:19 devuser 로그인 성공 (무차별 대입 IP)
↓
[Hunting] find -newerct '02:19' → cron.d/sys-update, /tmp/.cache/{kw,bash}, upload/.cache.php
↓
[연결] cron 이 /tmp/.cache/kw 를 root로 주기 실행 / bash 사본에 SUID
↓
[IOC] 파일 경로, sha256 해시, 생성 시각(btime·ctime)
↓
[Response] 파일 격리·해시 보존 → cron 제거 → SUID 사본 제거 → 같은 해시를 다른 서버에서 검색
-o(OR)는 우선순위가 낮으므로 \( \)로 묶는다.-newermt/-newerct(시각), -perm -4000(SUID), -perm -0002(world-writable), -name '.*'(숨김), -size, -nouser.-newerct) 도 함께 본다.-delete를 쓰지 않고, -print0 | xargs -0, -xdev, 2>/dev/null로 안전하게 검색한다.다음 글 「23. head와 tail」 에서는 로그의 앞·뒤를 보는 가장 단순한 명령어를 다룬다. 특히 실시간 관찰에 쓰는 tail -f와 tail -F의 차이, 그리고 로그 로테이션 중에 모니터링이 끊기는 문제 를 inode 관점에서 설명한다.