"바이브 코딩 (Vibe Coding)"이 어떻게 실수로 내 EC2 인스턴스를 암호화폐 채굴기로 만들었나
요약
개발자가 '바이브 코딩' 과정에서 의도치 않게 포함된 악성 코드로 인해 AWS EC2 인스턴스가 암호화폐 채굴기로 변질된 사례를 다룹니다. 외부 침입 흔적 없이 package.json의 코드 한 줄이 어떻게 시스템 권한을 탈취하고 네트워크 스캔을 유발했는지 분석합니다.
핵심 포인트
- package.json 내 악성 코드 삽입을 통한 공급망 공격 위험성
- SSH 침입 흔적이 없어도 시스템이 탈취될 수 있음
- EC2 인스턴스의 비정상적인 아웃바운드 트래픽 모니터링 필요성
- root 권한을 가진 악성 프로세스(xmrig 등) 탐지 및 대응
며칠 전, 저는 예상치 못한 이메일을 받았습니다. AWS Trust & Safety로부터 제 개인 AWS 계정의 EC2 인스턴스가 인터넷상의 다른 호스트들을 스캔하다 적발되었다는 남용 보고서(abuse report)였습니다.
제 첫 번째 생각은 당연하게도 누군가 제 서버를 해킹했다는 것이었습니다.
하지만 실제로 제가 발견한 것은 더 흥미롭고 (솔직히 말해서, 좀 창피한) 것이었습니다. package.json 파일에 적힌 단 한 줄의 코드가 모든 일의 원인이었습니다.
이 일이 어떻게 발생했는지, 어떻게 추적했는지, 그리고 실제로 무엇이 문제를 해결했는지에 대한 전체 이야기를 들려드리겠습니다.
남용 보고서 (The abuse report)
이메일에는 제3자 네트워크로부터 전달된 보고서가 포함되어 있었습니다. 그들은 제 인스턴스의 퍼블릭 IP로부터 9200(일반적으로 Elasticsearch 사용), 443, 80과 같은 포트로 여러 개의 완료된 TCP 핸드셰이크 (TCP handshakes)를 기록했습니다.
"완료된 핸드셰이크 (completed handshake)"라는 세부 사항이 중요한 이유는 IP 스푸핑 (IP spoofing) 가능성을 배제하기 때문입니다. 핸드셰이크가 완료되려면 양측 모두 응답해야 하므로, 이는 누군가 제 IP를 위조한 것이 아니었습니다. 제 인스턴스가 실제로 다른 호스트들에 접속하여 탐색(probing)을 수행하고 있었던 것입니다.
이제 왜 이런 일이 일어났는지 알아낼 차례입니다.
첫 번째 가설: 누군가 침입했다
외부로 나가는 스캔 트래픽은 "해킹된 서버"를 강력하게 시사하므로, 저는 누구나 할 법한 질문부터 시작했습니다. 어떻게 들어왔을까?
SSH 무차별 대입 공격 (SSH brute force)?
아니었습니다. SSH는 키 기반 인증만 사용하고 있었고, auth.log의 모든 항목은 제가 알고 있는 IP로부터의 "Accepted publickey"였습니다. 비밀번호 시도도, 낯선 소스도 없었습니다.
Accepted publickey for ubuntu from <trusted-ip> port 57322 ssh2: RSA SHA256:...
노출된 서비스가 있는가?
그 외의 모든 것은 인터넷에서 완전히 접근할 수 없는 상태였습니다.
보안 그룹 (Security group)이 그 모든 것의 앞단에서 막고 있었습니다. 막다른 길이었죠.
결론: 무차별 대입 공격도 없었고, 노출된 서비스도 없었습니다. 이것이 무엇이든, 정문(front door)을 통해 들어온 것은 아니었습니다.
악성코드 찾기
find / -mtime -2 (최근 2일 이내에 수정된 파일) 명령어를 빠르게 실행해 보니, 제 프론트엔드 앱 디렉토리에 아주 끔찍한 것들이 들어있었습니다:
scanner_linux
xmrig.tar.gz
scanner_deployed.log
...
xmrig는 합법적이지만 (심하게 악용되는) Monero 채굴 도구입니다. scanner_linux는 제가 한 번도 본 적 없는 바이너리였으며, ps aux 명령어를 통해 root 권한으로 실행되어 CPU를 엄청나게 점유하고 있음을 확인했습니다:
root 285679 27.7% CPU ./scanner_linux -t 1000
바로 그것이었습니다. 해당 프로세스가 AWS가 탐지한 트래픽을 생성한 것이 거의 확실했습니다.
두 파일 모두 root 소유였으며, 제가 진행한 어떤 SSH 세션과도 일치하지 않는 시간에 약 1분 간격으로 생성되었습니다. 이 파일들을 떨어뜨린(drop) 무언가는 root 수준의 파일 시스템 접근 권한을 가지고 있었지만, 이를 얻기 위해 SSH를 건드리지는 않았습니다.
외부로 향하는 흔적 추적
외부에서 들어온 것이 없다면, 내부의 무언가가 외부로 연결을 시도했을 것입니다. 보안 그룹(Security Group)의 아웃바운드 규칙을 확인했습니다:
Outbound: All traffic → 0.0.0.0/0
실제 근본 원인
프론트엔드 앱의 package.json을 파헤치던 중, 한 줄이 즉시 눈에 띄었습니다:
"dependencies": {
"zod": "latest",
"child_process": "latest",
...
child_process는 실제 npm 패키지가 아닙니다. 이는 별도의 설치가 전혀 필요 없는 Node.js의 내장 코어 모듈 (built-in core module)입니다.
이것이 외부 의존성(external dependency)으로 나타날 정당한 이유는 전혀 없습니다.
하지만 누군가가 공용 npm 레지스트리에 정확히 그 이름으로 패키지를 게시해 두었습니다. 이는 알려진 공격 패턴입니다. 신뢰할 수 있는 표준 라이브러리 모듈처럼 보이는 이름을 선점(squat)한 뒤, 누군가(코드 스니펫을 복사하여 붙여넣는 개발자, 혹은 요즘 같은 시대에는 의존성을 환각(hallucinating)하는 AI 코딩 어시스턴트)가 실수로 이를 설치하기를 기다리는 방식입니다.
이러한 패키지들은 거의 항상 postinstall 스크립트를 포함하고 있으며, npm은 npm install이 완료되는 즉시 설치 프로세스가 가진 권한을 사용하여 조용히 이를 자동으로 실행합니다. 저의 경우, 그것은 호스트 디렉토리가 바인드 마운트(bind-mounted)된 컨테이너 내부의 root 권한이었습니다. 해당 컨테이너를 재빌드할 때마다 이 스크립트가 조용히 다시 실행되고 있었습니다.
해결 방법: package.json에서 child_process 라인을 제거하고, node_modules와 lockfile을 삭제한 뒤, docker compose up -d --build 명령어로 처음부터 다시 빌드합니다. 여기서 --build 플래그가 중요한데, 단순히 재시작만 하면 이미 감염된 이미지가 그대로 다시 실행되기 때문입니다.
이번 사례를 통해 얻은 교훈
-
보안 그룹(Security Group)을 엄격하게 설정하는 것은 외부 공격자만 차단할 뿐입니다. 악성 코드가 이미 사용자의 권한으로 로컬에서 실행되고 있다면 보안 그룹은 아무런 역할을 하지 못합니다.
-
광범위하게 허용된 아웃바운드(Outbound) 규칙의 위험성은 과소평가되어 있습니다. 0.0.0.0/0 아웃바운드 설정은 흔한 기본값이지만, 바로 이 설정 때문에 아주 작은 postinstall 스크립트가 암호화폐 채굴기(Cryptominer)를 가져올 수 있었습니다. 이그레스(Egress, 나가는 트래픽)를 제한했다면 악성 패키지가 설치된 후라도 이 상황을 완전히 막을 수 있었을 것입니다.
-
Node.js 내장 모듈과 동일한 이름을 가진 모든 의존성(Dependency)은 즉각적인 위험 신호(Red flag)입니다. child_process, fs, http 등은 의존성 목록에 절대 나타나서는 안 됩니다.
-
버전을 "latest"로 고정하는 것을 멈추세요. 제 의존성 목록의 절반은 버전이 고정되어 있지 않았는데, 이로 인해 새롭고 원치 않는 요소가 몰래 침투했을 때 이를 발견하기가 훨씬 더 어려워집니다.
-
npm audit 실행과 package.json을 수동으로 훑어보는 작업은 사고 발생 후의 활동이 아니라 일상적인 루틴이 되어야 합니다.
-
쉘 히스토리(Shell history)에 기록된 것이라면, 그것이 실제로 사용되었는지 증명할 수 있는지 여부와 상관없이 모두 교체(Rotate)하세요. 이번 사례와는 무관하지만, 악성코드 조사 과정에서 .bash_history에 평문(Plaintext)으로 저장된 API 키를 발견했는데, 이 또한 수정하는 것이 똑같이 중요합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기