LiteLLM 1.107.0 배포본에 자격증명 탈취기와 백도어가 숨어 있었다
PyPI에 올라온 LiteLLM 1.107.0이 정상적인 LLM 프록시 패키지가 아니라 자격증명 탈취기와 원격 코드 실행 백도어를 함께 실어 나른 것으로 드러났다. 리눅스·macOS·윈도우용까지 일곱 개 휠 전부에 악성 파일이 들어 있었고, 해당 릴리스는 현재 PyPI에서 내려간 상태다. 더 고약한 부분은 이 패키지가 설치된 호스트에서 파이썬 프로세스가 시작될 때마다 악성 코드가 같이 딸려 실행된다는 점이다. litellm을 임포트조차 하지 않는 프로세스도 예외가 아니다.
악성 파일은 site-packages에 세 개로 떨어진다. 12바이트짜리 cook.pth는 `import cook` 한 줄로 cook.py를 끌어오고, cook.py는 약 61KB 분량의 파이썬 코드를 zlib과 base64로 감싼 실행문 형태로 품고 있다. sitecustomize.py는 파이썬이 기동할 때 자동으로 임포트하는 파일이라 별도의 트리거 없이 로더 겸 백도어로 동작한다. 세 파일 모두 휠의 RECORD에 정상 항목으로 등재돼 있어 빌드 이후 손댄 흔적은 없다.
탈취 범위가 넓다. cook.py는 16개 모듈로 SSH 키, 클라우드 자격증명, CI/CD 시크릿, 브라우저 저장 비밀번호, 셸 히스토리, 컨테이너 자격증명, GPG 키와 TLS 개인키까지 훑는다. AI 개발도구 쪽도 표적이다. Claude Code와 Claude Desktop, Cursor, Windsurf, Copilot, VS Code의 mcp.json, Gemini CLI, Codex의 auth.json, Continue, Aider, Goose, opencode, Qwen, Crush, Kiro, Zed, Hugging Face 토큰이 목록에 이름을 올렸다. LiteLLM 프록시 자체도 예외가 아니다. DATABASE_URL을 읽어 LiteLLM_CredentialsTable과 LiteLLM_ProxyModelTable을 덤프해 LLM 제공자 API 키를 빼낸다. AWS 인스턴스 메타데이터(IMDSv2 우선, 실패 시 v1)와 /proc/<pid>/environ을 통한 타 프로세스 환경변수 수집도 포함된다. 결과는 AES-256-CBC로 암호화하고 그 키를 내장 RSA-4096 공개키로 감싼 뒤 tar.gz로 묶어 www.litellm[.]dev/healthcheck로 POST한다. 성공하면 /tmp/.pc_once_… 마커를 남겨 호스트당 한 번만 수집한다.
sitecustomize.py 쪽은 상주형 백도어에 가깝다. /tmp/remote_code_loader.lock에 배타 락을 걸어 호스트당 데몬 하나만 뜨게 하고, fork와 setsid로 부모 프로세스에서 분리한 뒤 표준 입출력을 /dev/null로 돌린다. 이후 공격자의 GitHub 저장소에 있는 domain.txt에서 주소를 읽어 매시간 탈취기를 다시 돌리고, 그 서버에서 내려받은 코드를 실행한다. 이 사본에는 호스트당 1회 제한이 걸려 있지 않다.
침투 경로는 소스 배포판(sdist)의 pyproject.toml 한 줄이다. maturin 빌드 섹션에 `data = "cook"`이 끼어 있었고, 이 설정은 cook/ 폴더를 휠의 litellm-1.107.0.data/purelib/로 복사하게 만든다. pip이 그 폴더를 site-packages에 그대로 풀어놓는 구조다. sdist에는 cook/ 폴더 자체가 빠져 있어 이 한 줄이 눈에 띄는 유일한 변조 흔적이며, BerriAI/litellm의 공개 커밋 어디에도 해당 설정이나 폴더는 존재하지 않는다.
빌드 정황은 정상 릴리스와 놀랄 만큼 닮았다. 태그 v1.107.0이 가리키는 커밋의 pyproject.toml은 버전이 1.108.0으로 적혀 있었는데, 배포된 sdist는 이를 1.107.0으로 되돌리고 litellm-proxy-extras==0.4.110, litellm-enterprise==0.1.78로 rc2와 동일한 핀을 걸었다. 덕분에 rc2와 의존성을 비교해도 이상한 점이 잡히지 않는다. SafeDep이 sdist의 모든 파일을 blob 해시로 git 트리와 대조한 결과, 나머지 변조 파일 네 개는 Cargo.toml에 readme 한 줄을 추가한 것으로 정상 릴리스에도 있던 변경이었다. WHEEL 파일은 maturin 1.9.4로 정상 빌드와 같고, zip 타임스탬프는 macOS 11:20, 리눅스 11:21, 윈도우 11:37 UTC로 네이티브 빌드가 따로 돌았음을 보여준다. 윈도우 휠은 2,804개 .py 파일 전부가 CRLF 줄바꿈인 반면 리눅스 휠에는 하나도 없다.
공개된 GitHub Actions 실행 기록은 없다. SafeDep이 09:30부터 12:30 UTC 사이의 실행 539건을 전수 확인했지만 이 릴리스를 빌드하거나 배포한 작업은 없었다. PyPI 업로드 약 10분 뒤인 11:50 UTC에 litellm-dispatcher[bot]이 GitHub 릴리스 v1.107.0을 게시했고, 1.106.2에서도 같은 지연이 관찰됐다. 1.107.0과 1.106.2, rc2 모두 PyPI provenance attestation이 붙어 있지 않다.
SafeDep은 중간에서 높은 수준의 신뢰도로, BerriAI의 사설 릴리스 파이프라인이 공격자가 변조한 git 소스를 받아 이 휠들을 빌드했다고 본다. 침투 경로로는 탈취된 메인테이너나 봇 자격증명, 사설 브랜치나 미러, 파이프라인이 참조하는 소스 지정 중 하나를 꼽는다. 공격자가 자기 장비에서 훔친 PyPI 토큰으로 직접 올린 시나리오는 증거와 덜 맞아떨어진다.
실무에서 할 일은 명확하다. 잠금 파일과 설치 이력을 뒤져 litellm==1.107.0이 들어간 적이 있는지 확인해야 한다. 1.106.2와 1.107.0rc1, 1.107.0rc2는 깨끗하고, 1.107.0이 고정한 litellm-proxy-extras==0.4.110과 litellm-enterprise==0.1.78도 마찬가지다. 문제는 한 번이라도 실행됐다면 site-packages에서 세 파일을 지우는 것으로 끝나지 않는다는 점이다. SSH 키, 클라우드 자격증명, CI/CD 시크릿, AI 개발도구 토큰, 프록시 DB에 있던 LLM 제공자 키까지 전부 교체 대상으로 놓고 회전해야 한다.
다만 원인 귀속은 아직 확정이 아니다. 파이프라인 내부에서 무슨 일이 있었는지는 BerriAI의 릴리스 로그만이 밝힐 수 있고, 위 평가는 SafeDep의 조사 결과다.