# 개발자를 해킹하는 LinkedIn 채용 과제 사기

> Clean Markdown view of GeekNews topic #31514. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=31514](https://news.hada.io/topic?id=31514)
- GeekNews Markdown: [https://news.hada.io/topic/31514.md](https://news.hada.io/topic/31514.md)
- Type: GN+
- Author: [xguru](https://news.hada.io/@xguru)
- Published: 2026-07-17T09:48:13+09:00
- Updated: 2026-07-17T09:48:13+09:00
- Original source: [aisafe.io](https://aisafe.io/blog/inside-a-malicious-take-home-interview)
- Points: 1
- Comments: 0

## Topic Body

- 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](https://www.linkedin.com/in/wayan-adrian-7a596b20a/)은 [shrapnel.com](https://www.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 난독화 방식**을 사용함
  1. 중요한 문자열을 큰 배열에 저장함
  2. 체크섬이 맞을 때까지 배열을 회전함
  3. 문자열 접근을 디코더 함수 뒤에 숨김
  4. 명확한 이름을 숫자 인덱스와 래퍼 호출로 교체함
- 두 디코더 중 하나는 Base64 형태로 문자열을 복구하고, 다른 하나는 문자열별 키와 **RC4 유사 스트림 암호**를 적용했으며, 디코더 인덱스가 맞기 전에 문자열 배열을 회전시킴
- 악성코드를 실행하지 않고도 다음 순서로 난독화를 해제할 수 있었음
  1. 문자열 배열을 추출함
  2. 예상 체크섬에 도달하도록 배열 회전을 재현함
  3. 디코더 함수를 다시 구현함
  4. `b(0x214)`, `c(0x2f1, "key")` 같은 호출을 실제 문자열로 치환함
  5. 래퍼 객체와 도우미 함수를 단순화해 실행 의도를 드러냄

### 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` 네트워킹
  - 공유 폴더 비활성화
- 여러 관점에서 악성코드 동작을 수집하도록 도구를 설치함
  - [Wireshark](https://www.wireshark.org/): 패킷과 C2·유출 트래픽 수집
  - [Sysinternals Sysmon](https://learn.microsoft.com/en-us/sysinternals/downloads/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
- 복호화된 `pack`도 다시 난독화돼 있었지만 첫 단계와 같은 기법을 사용해 동일한 절차로 해제할 수 있었음

### 네 개 프로그램으로 구성된 2단계 페이로드
- `pack`은 단일 스크립트가 아니라 다음 네 프로그램을 실행하는 **작은 프로세스 트리**였음
  1. `scdata` 명령·제어 백도어를 임시 디렉터리에 기록하고 실행함
  2. `ldata` 브라우저·지갑 탈취기를 임시 디렉터리에 기록하고 실행함
  3. 재귀 파일 스캐너를 `node -e`로 메모리에서 직접 실행함
  4. 클립보드 감시기를 `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/process/59e605dd78fb2aafccd1b622f06a00ca`
  - 로그 전송: `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 Data`
  - `Login Data For Account`
  - `Web Data`
- 이후 `Local Extension Settings/&lt;extension-id&gt;`를 탐색하며, 하드코딩된 지갑 확장 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`임
  - 몇 초 기다린 뒤 반복해서 클립보드를 읽음
  - 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 블롭을 **완전히 제거하는 것**임

## Comments



_No public comments on this page._
