개발자를 해킹하는 LinkedIn 채용 과제 사기
11 hours ago
4
- 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 Collectible NFT를 생성함
- 실제 과제 범위는 단순한 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() })로 실행함
- 자체 기능을 수행하기보다 다른 악성 소프트웨어를 내려받아 실행하는 멀웨어 드로퍼임
- 다운로드한 코드에는 서명 검증, 해시 허용 목록, 출처 고정, 경로 제한, 실행 방지 가드가 없어서 원격 엔드포인트가 Tailwind 설정을 불러온 운영체제 계정의 권한으로 코드를 실행할 수 있음
격리 VM에서 수행한 동적 분석
- 호스트 시스템을 노출하지 않도록 UTM으로 일회용 Windows 11 ARM VM을 구성함
- 메모리 4096 MiB
- 디스크 64 GiB
- shared/NAT 네트워킹
- 공유 폴더 비활성화
- 여러 관점에서 악성코드 동작을 수집하도록 도구를 설치함
- 악성코드의 수집 대상을 확인하기 위해 미끼 데이터를 배치함
- 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
- 복호화된 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로 실행되며, 장시간 동작하는 자식 프로세스로 남음
- 실행 후 자신을 정상적인 프로세스처럼 보이도록 구성함
- 원격 제어 기능에 필요한 추가 패키지도 설치함
- 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 Data
- Login Data For Account
- Web 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로 전송함
- 미끼 환경에서는 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임
- 형식을 해석하지 않아도 사용자가 복사하는 비밀번호, 복구 문구, 개인 키, 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 블롭을 완전히 제거하는 것임
-
Homepage
-
Tech blog
- 개발자를 해킹하는 LinkedIn 채용 과제 사기