2026년 6월, GitLab은 PyPI를 대상으로 한 Shai-Hulud 악성코드 모방 공급망 공격을 발견했다. 공격자는 Flask·Requests·NumPy의 오타 패키지와 정상 프로젝트를 변조한 패키지에 설치 시 자동 실행되는 자격 증명 탈취 웜을 삽입했다. 이 웜은 CI/CD와 클라우드 자격 증명을 훔칠 뿐 아니라 GitHub 저장소와 패키지 레지스트리에 악성 코드를 퍼뜨려 추가 감염을 시도한다.
## 공격 대상 패키지와 위장 방식
- 공격 패키지 5개는 모두 PyPI 계정 `elitexp`에서 배포됐다.
- 오타 패키지:
- `rlask`, `tlask`: Flask 사칭
- `rsquests`: Requests 사칭
- `nhmpy`: NumPy 사칭
- `mflux-streamlit`은 원래 정상적으로 사용되던 프로젝트였지만, 공격자가 버전 `0.0.3`, `0.0.4`를 악성 버전으로 변조했다.
- 공격자는 먼저 실제 최신 버전과 동일한 “탐색용” 정상 패키지를 등록한 뒤, 악성 페이로드가 포함된 후속 버전을 배포했다.
- 따라서 단순 오타 입력뿐 아니라, 기존 프로젝트의 정상적인 의존성 업데이트도 감염 경로가 될 수 있다.
## `.pth` 파일을 이용한 설치 시 자동 실행
- npm 기반 Shai-Hulud 변종이 `preinstall` 스크립트를 사용한 것과 달리, 이번 공격은 Python의 `.pth` 메커니즘을 악용했다.
- Python은 시작 시 패키지에 포함된 `.pth` 파일을 자동 처리하므로, 사용자가 악성 모듈을 직접 import하거나 함수를 호출하지 않아도 코드가 실행될 수 있다.
- 예시 파일은 `rlask-setup.pth`이며, 임시 디렉터리의 `.bun_ran` 마커 파일을 확인한 뒤 다음 작업을 수행한다.
- GitHub에서 Bun JavaScript 런타임 다운로드
- 패키지에 포함된 약 5MB 크기의 난독화 JavaScript 실행
- 마커 파일을 이용해 반복 실행 방지
- 초기 `rlask` 버전에는 Python 시작 시 자동 import되는 `sitecustomize.py`도 포함되어 `_index.js`를 실행하는 보조 경로가 있었다.
- 이후 버전에서는 `.pth` 실행 방식만 남겨 공격 구조를 단순화했다.
## 다층 난독화와 페이로드 구성
- JavaScript 페이로드는 세 단계로 난독화됐다.
- 정수 배열에 ROT-N 문자 치환 적용
- AES-128-GCM으로 두 개의 데이터 블록 암호화
- `_0x` 접두사의 변수명 난독화
- 패키지마다 ROT 값이 달랐다.
- `rlask`: ROT-13
- `rsquests`: ROT-17
- `tlask`: ROT-25
- 분석 결과:
- 첫 번째 블록 약 907바이트: Bun 런타임 다운로드 코드
- 두 번째 블록 약 772KB: Shai-Hulud 자격 증명 탈취 웜 전체
- 두 번째 페이로드에는 2,538개의 하드코딩된 문자열이 포함돼 있었다.
- GitLab 연구팀은 실제 실행 없이 정적 분석만으로 페이로드를 복호화했다.
## 광범위한 자격 증명 탈취
웜은 주요 클라우드, CI/CD, 패키지 저장소와 개발 환경을 폭넓게 탐색한다.
- GitHub:
- `GITHUB_TOKEN`, 개인·Fine-grained 토큰
- OIDC 토큰, 조직·저장소 시크릿
- Actions 아티팩트와 러너 프로세스 메모리
- AWS:
- IAM 액세스 키와 시크릿 키
- 세션 토큰, STS 토큰
- IMDS 주소 `169.254.169.254`의 인스턴스 자격 증명
- Secrets Manager와 SSM 파라미터
- Azure 및 GCP:
- 클라이언트 시크릿, 관리형 ID 토큰, Key Vault 시크릿
- 서비스 계정 키와 Application Default Credentials
- HashiCorp Vault:
- `/var/run/secrets/vault-token`, `/etc/vault/token`, `/root/.vault-token` 등 알려진 경로
- API 및 Kubernetes 인증 정보
- 패키지 저장소:
- npm, JFrog/Artifactory, PyPI, RubyGems 토큰
- OIDC 토큰 교환 정보
- 기타:
- SSH 개인 키
- Kubernetes 서비스 계정 토큰과 kubeconfig
- Sigstore OIDC 토큰 및 Fulcio 서명 인증서
- MongoDB, MySQL, PostgreSQL, Redis 접속 문자열과 비밀번호
## 자격 증명 탈취를 넘어선 자기 전파
- 이 악성코드는 단순한 정보 탈취기가 아니라 훔친 권한을 이용해 다른 환경으로 확산하는 웜이다.
- 접근 가능한 GitHub 저장소에 다음 파일을 커밋한다.
- `.github/setup.js`
- GitHub Actions 워크플로 파일
- 이를 통해 다른 CI 파이프라인에서 악성 코드가 다시 실행되도록 만든다.
- `.github/copilot-instructions.md`를 삽입해 AI 코딩 도구의 동작을 오염시키려 한다.
- 훔친 레지스트리 토큰으로 PyPI, npm, RubyGems에 추가 악성 패키지를 배포한다.
- 자체 호스팅 CI 러너에서는 `sudoers` 규칙을 삽입해 권한 상승을 시도한다.
- StepSecurity의 `harden-runner`가 존재하는지 확인하고 탐지되면 동작을 조정한다.
## 공격자와 정상 프로젝트 변조
- PyPI 계정 `elitexp`는 2024년 11월 생성됐으며, 원래 정상 패키지 `mflux-streamlit`을 보유하고 있었다.
- 연결된 GitHub 계정은 13년 이상 된 계정으로, 대학 과제와 Laravel 프로젝트 등 다수의 공개 저장소가 있어 신뢰성을 높이는 데 활용됐을 가능성이 있다.
- 모든 패키지의 업로드에는 `Bun/1.3.14` 사용자 에이전트가 사용됐다.
- 공격자는 악성코드 실행 과정에서도 동일한 Bun 런타임을 다운로드한다.
- `mflux-streamlit`의 `0.0.1`, `0.0.2`는 정상 버전이지만, 이후 `0.0.3`, `0.0.4`에는 동일한 `.pth` 드로퍼와 난독화 페이로드가 포함됐다.
- 정상 프로젝트의 기존 사용자까지 일반적인 업데이트 과정에서 감염될 수 있다는 점이 전형적인 typosquatting보다 위험하다.
## 실용적인 대응 권고
- `rlask`, `tlask`, `rsquests`, `nhmpy`, `mflux-streamlit`의 악성 버전 설치 여부를 확인하고 즉시 제거한다.
- 해당 패키지가 설치된 환경에서는 CI/CD, 클라우드, 패키지 저장소, SSH 관련 자격 증명을 모두 폐기하고 재발급한다.
- GitHub 저장소의 워크플로와 `.github/setup.js`, `.github/copilot-instructions.md` 변경 이력을 점검한다.
- Python 패키지 설치 시 패키지명뿐 아니라 유지보수자, 배포 이력, 버전 변화, 해시를 검증한다.
- CI 환경에서는 최소 권한 토큰, 짧은 수명의 자격 증명, 네트워크 제한, 패키지 허용 목록을 적용하는 것이 안전하다.