리눅스 서버 침해 대응 기록: 의심 계정 조사부터 악성 프로세스 제거까지

2026. 5. 22. 12:59개발/ubuntu,linux,wsl

반응형

1. 배경

운영 중이던 리눅스 서버에서 계정 비밀번호가 의도치 않게 변경되거나, 알 수 없는 계정 접근 흔적이 보이는 문제가 발생했다. 처음에는 단순한 계정 관리 이슈로 보였지만, 확인 과정에서 SSH 무차별 대입 공격, 관리자급 계정 악용, SSH 백도어 키, 악성 프로세스, crontab 기반 자동 재실행 흔적까지 확인되었다.

최종적으로는 서버가 이미 침해된 것으로 판단했고, 새 서버 이전 전까지 임시 운영을 위해 다음 목표로 대응했다.

1. 추가 접속 차단
2. 관리자급 계정 무력화
3. 악성 프로세스 종료
4. 자동 재실행 루프 제거
5. 새 서버 이전 시 위험 요소 배제

2. 최초 의심 증상

처음 의심한 증상은 다음과 같았다.

- 서버 계정 비밀번호가 자꾸 바뀌는 것처럼 보임
- 알 수 없는 관리자급 계정 존재
- 오래 유지된 SSH 세션 존재
- 서버 load average가 비정상적으로 높음

먼저 현재 접속자를 확인했다.

who
w

확인 결과 정상 사용자 외에도 오래 유지된 세션이 있었고, 서버 부하가 높게 나타났다. 이후 계정 목록과 특정 계정의 권한을 확인했다.

getent passwd <계정명>
id <계정명>
sudo passwd -S <계정명>

그 결과, 의심 계정이 단순 일반 사용자가 아니라 sudo, docker, lxd 등 관리자급 권한을 가진 계정임을 확인했다.


3. 계정 접근 이력 확인

다음으로 로그인 이력을 확인했다.

last -a <계정명> | head -30
lastlog -u <계정명>

여러 IP에서 해당 계정으로 로그인한 기록이 확인되었다. 일부는 내부망 또는 정상 작업자의 접근으로 보였지만, 일부 외부 IP는 정상 접근으로 보기 어려웠다.

SSH 실패 시도 상위 IP를 확인하기 위해 auth 로그를 분석했다.

sudo zgrep -hE "Failed password|Invalid user|Connection closed by invalid user" /var/log/auth.log* 2>/dev/null \
| awk '{for (i=1;i<=NF;i++) if ($i=="from") print $(i+1)}' \
| sort | uniq -c | sort -nr | head -30

이 명령으로 SSH 무차별 대입 공격이 상당히 많이 발생했음을 확인했다.

이후 공격 시도가 많았던 IP들이 실제 로그인에 성공했는지 확인했다.

sudo zgrep -hE "Accepted .* from (<의심 IP 목록>)" /var/log/auth.log* 2>/dev/null

그 결과, 공격 시도가 많았던 외부 IP 중 하나가 관리자급 계정으로 로그인에 성공한 기록이 확인되었다.


4. SSH 백도어 키 확인

의심 계정의 SSH 키를 확인했다.

sudo ls -la /home/<계정명>/.ssh
sudo cat /home/<계정명>/.ssh/authorized_keys

authorized_keys에 알 수 없는 공개키가 들어 있었다. 더 이상한 점은 해당 파일이 일반적인 방법으로 비워지지 않았다는 것이다.

sudo sh -c '> /home/<계정명>/.ssh/authorized_keys'

위 명령이 실패했고, 파일 속성을 확인했다.

sudo lsattr /home/<계정명>/.ssh/authorized_keys

결과적으로 immutable과 append-only 속성이 걸려 있었다. 이는 파일을 삭제하거나 덮어쓰지 못하게 하는 속성이다.

조치는 다음과 같이 했다.

sudo chattr -ia /home/<계정명>/.ssh/authorized_keys
sudo sh -c '> /home/<계정명>/.ssh/authorized_keys'
sudo wc -c /home/<계정명>/.ssh/authorized_keys

결과적으로 authorized_keys를 0바이트로 비웠다.


5. 의심 계정 잠금 및 권한 제거

의심 계정은 관리자급 권한을 가지고 있었기 때문에, 우선 로그인 자체를 막았다.

sudo passwd -l <계정명>
sudo usermod -s /usr/sbin/nologin <계정명>
sudo pkill -KILL -u <계정명>

그 다음 관리자 권한 그룹에서 제거했다.

sudo gpasswd -d <계정명> sudo
sudo gpasswd -d <계정명> docker
sudo gpasswd -d <계정명> lxd

확인은 다음 명령으로 했다.

sudo passwd -S <계정명>
getent passwd <계정명>
id <계정명>

기대 상태는 다음과 같다.

- 비밀번호 잠김
- 로그인 shell이 nologin
- sudo/docker/lxd 권한 제거

6. 오래된 정상 사용자 세션 종료

조사 중 오래 유지된 정상 사용자 세션도 확인되었다. 해당 사용자가 회사 내부 사용자임은 확인되었지만, 현재 작업 중인 세션은 아니었으므로 종료했다.

who | grep <정상 사용자>
sudo pkill -KILL -u <정상 사용자>
who
w

이 조치는 계정 잠금이 아니라, 오래 남아 있던 세션만 정리한 것이다.


7. 시스템 권한 이상 확인

방화벽 상태를 확인하던 중 다음 경고가 나왔다.

WARN: /usr is world writable!

이는 /usr 디렉터리가 모든 사용자에게 쓰기 가능한 상태였다는 의미다. 매우 위험한 상태이므로 즉시 권한을 복구했다.

sudo chown root:root /usr
sudo chmod 755 /usr
ls -ld /usr

정상 상태는 다음과 같다.

drwxr-xr-x root root /usr

8. 높은 CPU 사용 프로세스 조사

서버 load average가 비정상적으로 높았기 때문에 CPU 사용량 상위 프로세스를 확인했다.

ps aux --sort=-%cpu | head -30

여기서 정상적인 서비스나 모델 학습 프로세스가 아니라, root 권한으로 실행 중인 수상한 프로세스를 발견했다.

특징은 다음과 같았다.

- 프로세스명은 bash처럼 보임
- 실제 실행 파일은 /root 하위 숨김 캐시 디렉터리
- CPU 사용률이 매우 높음
- 외부 IP와 연결 중

정상적인 bash라면 /bin/bash 또는 /usr/bin/bash여야 한다. 하지만 실제 실행 파일은 숨김 디렉터리 아래에 있었다.

프로세스 정보를 확인했다.

sudo readlink -f /proc/<PID>/exe
sudo ls -l /proc/<PID>/exe
sudo tr '\0' ' ' < /proc/<PID>/cmdline; echo
sudo pwdx <PID>
sudo pstree -asp <PID>

네트워크 연결도 확인했다.

sudo lsof -p <PID> | head -100
sudo ss -tpn | grep <PID>

외부 IP와 연결 중인 것이 확인되었다.


9. 모델 학습이 아닌 악성 프로세스로 판단한 이유

처음에는 서버가 AI/ML 작업에도 사용되고 있었기 때문에 모델 학습 프로세스인지 확인이 필요했다.

하지만 정상적인 모델 학습이라면 보통 다음과 같은 형태가 나타난다.

python train.py
torchrun
deepspeed
accelerate
uvicorn
vllm
프로젝트 디렉터리 하위 실행
GPU 사용 흔적

반면 발견된 프로세스는 다음과 같은 특징을 가지고 있었다.

/root 하위 숨김 디렉터리에서 실행
bash로 위장
root 권한 실행
CPU 과다 사용
외부 IP와 지속 연결
crontab 기반 자동 재실행

따라서 이는 모델 학습 프로세스가 아니라, 채굴 또는 서버 리소스 탈취 목적의 악성 프로세스로 보는 것이 합리적이라고 판단했다.


10. 악성 프로세스 종료

먼저 프로세스를 종료했다.

sudo kill -TERM <PID>
sleep 2
ps -p <PID> -o pid,ppid,user,%cpu,%mem,stat,lstart,cmd

종료되지 않을 경우 강제 종료했다.

sudo kill -KILL <PID>

이후 다시 CPU 사용량과 외부 연결을 확인했다.

ps aux --sort=-%cpu | head -20
sudo ss -tpn | grep <외부 IP>

처음에는 프로세스가 사라진 듯 보였지만, 잠시 후 다시 생성되었다. 즉 단순 프로세스 종료만으로는 해결되지 않았다.


11. 자동 재실행 원인 확인

악성 프로세스가 다시 살아났기 때문에 crontab, systemd, profile 계열을 확인했다.

sudo grep -R "root/.cache\|<외부 IP>\|bash.pid\|h64\|config.json\|/root/.cache/run\|/root/.cache/upd" \
/etc/cron* /var/spool/cron* /etc/systemd/system /root/.bashrc /root/.profile 2>/dev/null

root crontab에서 다음과 같은 항목이 확인되었다.

* * * * * /root/.cache/upd >/dev/null 2>&1

즉, 매분 악성 실행파일을 다시 실행하는 구조였다.


12. 악성 crontab 제거

삭제 전 root crontab을 백업했다.

sudo mkdir -p /root/incident-backup
sudo cp -a /var/spool/cron/crontabs/root /root/incident-backup/root.crontab.$(date +%F-%H%M)

그 다음 악성 줄을 제거했다.

sudo crontab -l | grep -v '/root/.cache/upd' | sudo crontab -

확인했다.

sudo crontab -l

아무것도 나오지 않으면 root crontab이 비어 있는 상태다.


13. 악성 파일 무력화

숨김 캐시 디렉터리 전체를 이동하려 했지만, 내부에 정상 캐시 파일도 많아 오래 걸렸다. 그래서 우선 악성으로 보이는 실행파일의 실행 권한을 제거했다.

sudo chmod 000 /root/.cache/upd /root/.cache/run /root/.cache/h64 /root/.cache/a /root/.cache/c /root/.cache/x /root/.cache/z 2>/dev/null
sudo chown root:root /root/.cache/upd /root/.cache/run /root/.cache/h64 /root/.cache/a /root/.cache/c /root/.cache/x /root/.cache/z 2>/dev/null

이후 1분 이상 기다려 crontab 재실행 주기를 확인했다.

sleep 70
ps aux | grep -Ei "/root/.cache|<외부 IP>|xmrig|miner|kinsing" | grep -v grep
sudo ss -tpn | grep <외부 IP>
sudo crontab -l

결과적으로 악성 프로세스가 다시 뜨지 않았고, 외부 연결도 사라졌으며, root crontab도 비어 있었다.


14. 최종 조치 결과

이번 대응에서 수행한 조치는 다음과 같다.

1. 현재 접속자 확인
2. 의심 계정 존재 및 권한 확인
3. 로그인 성공/실패 이력 분석
4. SSH brute-force 공격 IP 확인
5. 의심 계정 SSH authorized_keys 확인
6. immutable/append-only 속성 제거
7. SSH 백도어 키 제거
8. 의심 계정 잠금
9. sudo/docker/lxd 권한 제거
10. 오래된 세션 종료
11. /usr 권한 복구
12. 고CPU 프로세스 확인
13. 악성 프로세스 실행 경로 확인
14. 외부 연결 확인
15. 악성 프로세스 종료
16. crontab 기반 자동 재실행 원인 확인
17. root crontab 악성 항목 제거
18. 악성 실행파일 권한 제거
19. 재실행 여부 확인

현재 기준으로는 다음 상태다.

- 의심 계정 로그인 차단 완료
- SSH 백도어 키 제거 완료
- 관리자급 권한 제거 완료
- 악성 프로세스 종료 완료
- 자동 재실행 crontab 제거 완료
- 외부 연결 사라짐
- /usr 권한 복구 완료

15. 침해 목적에 대한 판단

정확한 악성코드 종류는 바이너리 분석을 하지 않았기 때문에 단정할 수 없다. 하지만 현재까지 확인된 정황상, 가장 합리적인 판단은 채굴 또는 서버 리소스 탈취 목적이다.

근거는 다음과 같다.

- CPU 사용률이 비정상적으로 높음
- 정상 학습 프로세스가 아님
- bash로 위장한 실행파일
- root 권한으로 실행
- 외부 IP와 연결
- crontab으로 매분 자동 재실행
- 숨김 디렉터리에 여러 실행파일 존재

따라서 이번 사고는 단순 침입 시도가 아니라, 실제 서버 자원을 사용하기 위한 침해 사고로 판단했다.


16. 남은 위험

긴급 대응으로 악성 프로세스와 자동 재실행 루프는 차단했지만, 서버는 이미 root 권한 수준으로 침해된 것으로 봐야 한다.

따라서 다음 위험은 남아 있다.

- 시스템 파일 변조 가능성
- Docker 컨테이너 내부 감염 가능성
- 추가 crontab/systemd 백도어 가능성
- 서비스 환경변수 유출 가능성
- DB 계정, API 키, deploy key 유출 가능성
- 로그 조작 가능성

이 때문에 해당 서버는 완전히 복구해서 계속 사용하는 것보다, 새 서버를 구축하고 필요한 데이터만 선별 이전하는 것이 안전하다.


17. 새 서버 이전 시 주의사항

새 서버에는 전체 OS 이미지나 홈 디렉터리를 그대로 옮기면 안 된다.

옮기지 말아야 할 것:

/root
/root/.cache
/home/<의심 계정>/.ssh
/etc/cron*
/var/spool/cron*
/etc/systemd
/tmp
/var/tmp
/dev/shm
기존 OS 이미지 전체

옮길 수 있는 것:

검토된 프로젝트 소스
DB dump
업로드 데이터
필요한 설정 파일

단, 다음 값은 새로 발급하거나 변경해야 한다.

DB 비밀번호
API 키
서비스 토큰
Git deploy key
Docker registry token
클라우드 access key
SSH 개인키
서버 계정 비밀번호

18. 마무리

이번 사고에서 가장 중요한 포인트는 “프로세스 하나를 죽이는 것”이 아니라, 어떻게 다시 살아나는지 찾는 것이었다.

처음에는 계정 문제처럼 보였지만, 순서대로 확인하면서 다음 흐름을 파악할 수 있었다.

SSH 공격 시도
→ 관리자급 계정 접근
→ SSH 백도어 키 확인
→ 고CPU 악성 프로세스 발견
→ 외부 연결 확인
→ 프로세스 재생성 확인
→ root crontab 기반 자동 실행 발견
→ crontab 제거 및 실행파일 무력화

결론적으로, 침해된 서버에서는 단순히 비밀번호만 바꾸거나 프로세스만 종료해서는 충분하지 않다. 계정, SSH 키, 권한, 프로세스, 네트워크 연결, crontab, systemd, 숨김 디렉터리까지 함께 확인해야 한다.

이번 조치로 임시 운영에 필요한 급한 불은 껐지만, 최종 대응은 새 서버 구축 후 데이터 선별 이전 및 기존 서버 포맷이 적절하다.

 

 

반응형