본문 바로가기
보안2026년 10월 21일19분 읽기

LiteLLM 1.107.0, 릴리스 파이프라인 탈취로 PyPI·도커에 악성 배포 — 설치만 해도 키 털고 원격 백도어 띄운다

YS
김영삼
조회 25
LiteLLM 1.107.0, 릴리스 파이프라인 탈취로 PyPI·도커에 악성 배포 — 설치만 해도 키 털고 원격 백도어 띄운다

LLM 게이트웨이 라이브러리 LiteLLM의 1.107.0 버전이 10월 21일(UTC) 릴리스 파이프라인 탈취로 악성 코드가 심긴 채 PyPI와 도커 레지스트리에 올라왔고, 이 버전은 설치만 해도 파이썬이 시작될 때마다 자격 증명을 훔치고 원격 코드를 내려받는 백도어를 띄운다.

공급망 보안 업체 SafeDep, JFrog, Wiz가 잇따라 분석을 내놨고 LiteLLM 운영사 BerriAI도 공식 사고 보고서에서 침해를 인정했다. PyPI 패키지는 약 1시간 44분, 도커 이미지는 약 2시간 34분 동안 노출됐고, Wiz는 ClickPy 통계를 근거로 그 사이 악성 버전 다운로드가 약 4만 9,000건이라고 추정했다. 지난 3월 1.82.7·1.82.8 사건에 이어 올해 두 번째 침해다.

3줄 요약
  • 공격자는 개발자 3명의 GitHub CLI 토큰을 손에 넣어 브랜치 보호를 무력화하고, 공식 릴리스 워크플로가 이름이 비슷한 가짜 저장소 Berri-AI/litellm에서 빌드하도록 바꿨다. GitHub의 v1.107.0 소스 태그 자체는 깨끗했다.
  • 휠 7종 전부에 cook.pth·cook.py·sitecustomize.py가 들어가 있었다. litellm을 import하지 않아도 그 환경에서 파이썬만 실행되면 클라우드 키·SSH 키·AI 도구 토큰·LiteLLM 프록시 DB의 LLM API 키까지 쓸어 담아 외부로 보낸다.
  • LiteLLM은 1.106.2로 고정하고, 환경을 새로 만들고, 그 호스트가 닿을 수 있던 모든 비밀을 교체하라고 권고했다. 파이썬을 띄우는 것 자체가 페이로드를 다시 실행하므로 감염 환경에서 pip install부터 치지 말라는 경고도 붙었다.

무슨 일이공식 파이프라인이 가짜 저장소를 빌드했다

LiteLLM은 OpenAI, Anthropic, 베드록, 버텍스 등 수십 개 LLM API를 하나의 인터페이스로 묶어 주는 파이썬 SDK이자 프록시 서버다. 사내 AI 게이트웨이로 깔아 두고 각 팀의 프로바이더 키를 한곳에 모아 관리하는 용도로 많이 쓴다. 바로 그 점 때문에 이번 사건이 무겁다. 이 패키지가 도는 서버에는 보통 회사의 LLM 키가 전부 모여 있다.

LiteLLM이 docs.litellm.ai에 올린 사고 보고서에 따르면 공격은 10월 21일 10시 8분(UTC)에 시작됐다. GitHub 브랜치 보호가 승인되지 않은 워크플로 변경을 한 번 거부했는데, 5초 뒤 권한 있는 개발자의 자격 증명으로 보호 설정을 무시하고 변경을 밀어 넣었다. 이 계정은 GitHub 조직 소유자(owner)였다. 10시 46분 공격자 저장소를 원본으로 한 스테이징 빌드가 돌았고, 11시 11분에는 두 번째 개발자의 자격 증명이 프로덕션 승인 단계를 통과시켰다.

결과물은 공식 파이프라인이 직접 빌드하고 서명해 배포했다. 11시 39분 51초부터 40분 13초 사이에 휠 7개가 PyPI에 올라갔고, 11시 46분에는 도커 이미지가 나갔다. 공격자는 11시 53분께 세 번째 개발자의 자격 증명으로 프로덕션·스테이징 워크플로 실행 기록을 지웠다. LiteLLM은 이 때문에 조사에 쓸 로그가 줄었다고 밝혔다.

외부에서 처음 이상을 공개적으로 짚은 건 12시 6분(UTC)에 BerriAI/litellm 저장소에 올라온 보안 이슈(#47826)다. 작성자는 휠 7종의 SHA-256 값을 표로 정리하고 "페이로드는 v1.107.0 git 태그에는 없고, 휠을 빌드하기 전에 빌드 트리에 주입됐다"고 적었다. PyPI는 13시 23분 1.107.0을 내렸고, 도커 허브 태그는 14시 20분에야 모두 삭제되거나 깨끗한 이미지로 다시 연결됐다.

토큰은 어디서 샜나

LiteLLM 보고서가 지목한 유출 경로는 꽤 아프다. 인터넷에 노출된 사내 LiteLLM 배포본에 정체를 알 수 없는 평범한 이름의 사용자 계정이 하나 있었고, 공격자가 그 배포본의 로그에서 토큰을 얻은 것으로 본다고 했다. 보고서는 Claude Code가 응답 본문에 사용자의 GitHub CLI OAuth 토큰을 담을 수 있고, 응답을 로깅하면 그 토큰이 LiteLLM 로그에 남을 수 있다고 설명했다. 공격자가 그 배포본에 어떻게 들어갔는지는 아직 확인되지 않았고, 팀은 "최근 패치된 취약점"을 의심한다고만 밝혔다.

운도 나빴다. 사건 직전 월요일에 엔지니어 두 명이 버그 수정 백포트를 배포할 수 있도록 릴리스 권한을 임시로 넓혀 둔 상태였다. 3월 사고 이후 릴리스를 비공개 저장소로 옮기고 Trusted Publisher, 변경 불가능한 도커 태그, 이미지 서명까지 도입했지만, 서명은 공식 파이프라인이 했으니 서명 검증만으로는 걸러지지 않았다. 보고서 표현대로 공격자는 "여러 독립적인 보안 통제를 모두 뚫어야" 했고, 실제로 뚫었다.

LiteLLM 보고서·SafeDep·Wiz 종합. 출처마다 몇 분씩 차이가 있고, LiteLLM 보고서는 공격 시각을 "태평양 시간 새벽 4시"(UTC 12시)로도 적어 자체 업로드 기록(11:39)과 어긋난다.
시각 (UTC, 10월 21일)사건
10:08브랜치 보호 우회, 릴리스 워크플로 변경
10:46~11:11가짜 저장소 Berri-AI/litellm 기반 빌드 시작, 두 번째 자격 증명으로 프로덕션 승인 통과
11:39~11:46악성 휠 7종 PyPI 업로드, 이어 도커 이미지 게시 (v1.107.0·latest·main-latest·main-stable)
12:06GitHub 보안 이슈 #47826 공개 제보
13:20~13:23PyPI 격리·삭제 (Wiz는 13:20 격리, LiteLLM은 13:23 삭제로 기록)
13:51~14:20GHCR 태그 접근 불가, 도커 허브 태그 삭제·재지정 완료
16:20~17:01탈취된 AWS 인스턴스 역할로 GetCallerIdentity·베드록 모델 목록 호출 (Wiz 관찰)

악성 코드가 하는 일파이썬이 켜질 때마다 두 개의 페이로드가 돈다

SafeDep 분석을 보면 휠 안의 pyproject.toml maturin 설정에 data = "cook" 한 줄이 추가돼 cook/ 폴더가 휠에 복사되도록 돼 있었다. 소스 배포본(sdist)에는 이 폴더가 없고, 공개된 업스트림 커밋 어디에도 이 변경은 없다. 코드 리뷰나 소스 감사만으로는 찾을 수 없는 구조다.

1) 자격 증명 탈취기 (cook.pth → cook.py)

.pth 파일은 site-packages에 놓이면 파이썬 인터프리터가 시작할 때 자동으로 읽힌다. cook.pth는 매번 cook 모듈을 불러오고, 그 결과 CI 잡이든 IDE 언어 서버든 그 환경의 모든 파이썬 프로세스에서 탈취기가 실행된다. JFrog는 "애플리케이션이 litellm을 한 번도 import하지 않아도 페이로드가 돌 수 있다"고 강조했다.

cook.py는 zlib과 base64로 한 겹 감싼 약 61KB짜리 파이썬 코드다. 분석가들이 "privcollect"라 부르는 이 탈취기는 16개 수집 모듈로 다음을 노린다. 호스트·네트워크 정보, 브라우저에 저장된 비밀번호와 기록, SSH 키와 셸 히스토리, AWS·Azure·GCP·쿠버네티스·도커·GitHub CLI 자격 증명, GitLab·젠킨스·테라폼·볼트 같은 CI/CD 비밀, Claude Code·커서·코파일럿·코덱스 같은 AI 개발 도구 토큰, /proc를 통한 다른 프로세스의 환경 변수, AWS 인스턴스 메타데이터 자격 증명, GPG·TLS 개인 키. 그리고 LiteLLM 프록시 데이터베이스에서 LLM 프로바이더 API 키가 담긴 테이블을 직접 조회한다.

모은 데이터는 AES-256-CBC로 암호화하고 그 키를 RSA-4096(OAEP)으로 감싼 뒤 tar.gz로 묶어 가짜 도메인 www.litellm[.]dev의 /healthcheck 경로로 POST한다. JFrog에 따르면 이 도메인은 10월 13일에 등록돼 18일에 갱신됐다. 적어도 일주일 전부터 준비된 공격이라는 얘기다. 전송에 성공하면 /tmp에 표식 파일을 남겨 호스트당 한 번만 수집한다.

2) 원격 코드 로더 (sitecustomize.py)

sitecustomize.py 역시 파이썬 시작 시 자동 실행되는 파일이다. 이 코드는 /tmp의 잠금 파일로 호스트당 데몬을 하나만 띄운 뒤, 포크해서 세션을 분리하고 표준 입출력을 버린 채 백그라운드에 숨는다. 이후 1시간마다 GitHub 계정 letters9813 아래 저장소의 텍스트 파일에서 도메인을 읽고, 내장된 탈취기를 다시 돌리고(이쪽에는 1회 제한이 없다), 그 도메인의 /codepy에서 2단계 스크립트를 받아 실행한다. GitHub 파일만 고치면 공격자가 명령 서버를 마음대로 바꿀 수 있는 이른바 데드 드롭 구조다.

GitHub은 13시 30~40분께 이 계정을 내렸다. SafeDep은 계정 삭제 뒤로는 백도어가 서버를 찾지 못해 아무것도 하지 않는다고 봤고, JFrog도 관찰 시점에 데드 드롭 파일이 비어 있어 원격 단계는 잠들어 있었다고 했다. 다만 삭제 전에 데몬이 돌았던 호스트는 2단계 코드를 실행했을 수 있으며, SafeDep은 그 서버 주소를 끝내 복구하지 못했다. 로컬 탈취기는 원격 단계와 상관없이 그대로 동작했다.

고수준 IOC (전체 해시·다이제스트는 출처 참조)
파일 — litellm-1.107.0.dist-info, site-packages의 cook.pth·cook.py·sitecustomize.py와 그 .pyc, /tmp/remote_code_loader.lock 등 /tmp의 표식·잠금 파일
프로세스 — 환경 변수 PRIVCOLLECT_ACTIVE=1, 부모 PID 1·작업 디렉터리 /인 분리된 파이썬 프로세스
네트워크 — www.litellm[.]dev/healthcheck POST, GitHub letters9813 조회, /codepy 다운로드, 파이썬의 169.254.169.254 접근
컨테이너 — 이미지 인덱스 sha256:1bbcb1443bc9...
LiteLLM은 흔적이 보이면 노출 확정이지만, 안 보인다고 깨끗하다는 증거는 아니라고 밝혔다.

왜 중요한가서명된 공식 빌드도 믿을 근거가 되지 못했다

3월 사건은 CI에서 쓰던 Trivy가 먼저 털리면서 PyPI 게시 자격 증명이 샌 경우였다. LiteLLM은 그 뒤 할 수 있는 조치를 꽤 했다. 그런데 이번에는 공격자가 게시 토큰을 훔친 게 아니라 파이프라인 자체를 조종했다. 정식 워크플로가 정식 서명을 달아 정식 레지스트리에 올렸다. PyPI의 신뢰된 게시나 이미지 서명이 "누가 올렸나"는 증명해도 "무엇을 빌드했나"까지 보증하지는 못한다는 점이 그대로 드러났다. JFrog와 Wiz는 3월 사건(TeamPCP)과 이번 사건을 잇는 증거는 찾지 못했다며 별개 캠페인으로 보고 있다.

표적도 영리하다. LLM 게이트웨이는 구조상 회사의 프로바이더 키, 마스터 키, 데이터베이스 비밀번호가 한 곳에 모이는 자리다. 탈취기에 LiteLLM 프록시 DB를 직접 조회하는 모듈이 따로 있다는 건 공격자가 이걸 노렸다는 뜻이다. Wiz는 실제로 몇 시간 뒤 탈취된 AWS 인스턴스 역할 자격 증명으로 베드록 모델 목록을 조회하는 활동을 관찰했다며 이를 LLM재킹(LLMjacking)으로 분류했다. 훔친 클라우드 권한으로 남의 돈으로 모델을 돌리려는 시도다.

또 하나, 도커의 latest나 main-stable처럼 움직이는 태그를 쓰던 곳도 고스란히 당했다. 버전을 명시하지 않고 "안정판"이라는 이름만 믿고 당겨 온 컨테이너가 2시간 넘게 악성 이미지를 받았다.

영삼
영삼의 한마디
내 서버에서도 LiteLLM 비슷한 게이트웨이를 붙여 쓰는 실험을 해 봤는데, 편하자고 키를 한데 모으는 순간 그 서버가 가장 비싼 표적이 된다는 걸 이번에 다시 실감했다. 개인적으로 제일 섬뜩한 대목은 유출 경로다. AI 코딩 도구의 응답 로그에 GitHub 토큰이 남았고, 그 로그가 인터넷에 열린 내부 서비스에 있었다. 공격자가 특별한 제로데이를 쓴 게 아니라 우리가 다들 하는 "일단 로그는 다 남겨 두자"를 파고든 셈이다. 프록시 로그에 무엇이 찍히는지 한 번이라도 열어 본 적이 있는지, 나부터 점검해 볼 생각이다.

개발자가 할 일먼저 파이썬을 띄우지 말고 파일부터 본다

흔히 "pip show litellm으로 버전을 확인하라"고 하지만, 이번 건은 순서가 중요하다. pip도 파이썬 프로그램이라 감염된 환경에서 실행하면 cook.pth와 sitecustomize.py가 다시 돈다. LiteLLM과 JFrog 모두 감염이 의심되는 환경에서는 파이썬을 띄우지 말고 파일 시스템과 잠금 파일(lockfile)부터 확인하라고 권했다. 아래는 파이썬을 실행하지 않고 확인하는 예시다.

# 1) 락파일·요구사항 파일에서 1.107.0 고정 여부 확인 (파이썬 실행 없음)
grep -rnE 'litellm[^0-9]*1\.107\.0' requirements*.txt poetry.lock uv.lock Pipfile.lock pdm.lock 2>/dev/null

# 2) 설치 흔적 찾기: dist-info 디렉터리와 심어진 파일
find / -type d -name 'litellm-1.107.0.dist-info' 2>/dev/null
find / -path '*site-packages*' \( -name 'cook.pth' -o -name 'cook.py' -o -name 'sitecustomize.py' \) 2>/dev/null

# 3) 백그라운드 데몬·환경 변수 흔적
ls -la /tmp/remote_code_loader.lock 2>/dev/null
grep -l 'PRIVCOLLECT_ACTIVE' /proc/*/environ 2>/dev/null

# 4) 도커: 사용 중인 이미지 다이제스트 확인 (sha256:1bbcb1443bc9... 이면 감염)
docker images --digests | grep -i litellm

# 5) 위 흔적이 전혀 없는 깨끗한 환경에서만
pip show litellm   # Version: 1.107.0 이면 감염
감염 시 조치 체크리스트
  • 의존성을 litellm==1.106.2(또는 사건 이전 명시 버전)로 고정하고, 도커는 latest·main-stable 대신 버전 태그나 다이제스트로 고정한다.
  • 감염 환경에서 pip install부터 하지 않는다. 심어진 세 파일과 .pyc를 지우고, /tmp/remote_code_loader.lock을 잡은 분리된 파이썬 데몬을 종료한다.
  • 가상 환경과 컨테이너 이미지는 제자리 재설치보다 처음부터 다시 만든다. LiteLLM도 재빌드가 "더 낫다"고 했다.
  • 외부로 나갈 수 있던 호스트라면 유출됐다고 가정하고 교체한다: IAM 역할, 프록시 DB의 모든 프로바이더 키, 마스터·가상 키, DATABASE_URL 비밀번호, 환경 변수의 비밀, SSH·GPG 키, CI/CD·AI 도구 토큰.
  • CloudTrail에서 설치 전후 GetCallerIdentity와 외부에서 온 역할 사용·베드록 호출을 찾고, litellm[.]dev 통신을 차단한다.

내 조직 쪽도 같이 봐야 한다. 이번 출발점은 패키지가 아니라 GitHub 조직이었다. 조직 소유자 계정 수, 관리자가 브랜치 보호를 우회할 수 있는지, 서비스 로그에 OAuth 토큰이나 API 키가 평문으로 찍히는지 확인해 볼 만하다.

앞으로조사는 진행 중, 추가 IOC 공개 예정

LiteLLM은 사고가 "봉쇄됐다(contained)"고 밝히면서도 조사는 계속 중이라고 했다. 지금까지 한 조치는 PyPI 패키지 제거와 "침해 범위를 조사하는 동안 메인테이너 자격 증명을 최소한으로 축소"한 것 정도이고, 앞으로 어떤 통제를 추가할지는 아직 구체적으로 내놓지 않았다. 엔터프라이즈 고객용 비공개 레지스트리 배포본은 영향이 없었다고 했다. 반면 LiteLLM 클라우드 서비스에 대해서는 보고서에 언급이 없다.

사건 당일 오후(UTC) LiteLLM 저장소에는 릴리스 태그와 pyproject.toml 버전이 일치하는지 검증하는 CI 변경이 올라왔다. v1.107.0 태그의 pyproject.toml에 버전이 1.108.0으로 적혀 있다는 버그 리포트에 대응한 것이다. 이번 사건과 직접 관련됐는지는 공식적으로 밝혀지지 않았다. 한 해에 두 번 같은 패키지가 뚫렸다는 사실은 남는다. LiteLLM을 계속 쓸지, 버전을 얼마나 오래 묶어 둘지는 각 팀이 다시 따져 봐야 할 문제가 됐다.

자주 묻는 질문

LiteLLM 1.107.0 악성 버전이란?

10월 21일(UTC) 릴리스 파이프라인이 탈취되면서 PyPI와 도커에 배포된 악성 LiteLLM 빌드다. 휠에 숨겨진 파일이 파이썬 시작 때마다 자격 증명을 훔쳐 외부로 보내고, 1시간마다 원격 코드를 받아 실행하는 백도어를 띄운다. GitHub의 v1.107.0 소스 태그에는 악성 코드가 없다.

내가 LiteLLM 1.107.0을 설치했는지 확인하려면?

먼저 파이썬을 실행하지 말고 락파일에서 1.107.0을 찾고, 디스크에서 litellm-1.107.0.dist-info 디렉터리와 site-packages의 cook.pth·sitecustomize.py를 검색한다. 흔적이 없는 깨끗한 환경에서만 pip show litellm으로 버전을 본다. 도커는 이미지 다이제스트가 sha256:1bbcb1443bc9...인지 확인한다.

LiteLLM 안전한 버전은?

LiteLLM은 1.106.2로 고정하라고 권고했고, 1.107.0 이전 버전은 모두 안전하다고 밝혔다. GitHub 이슈 제보자와 SafeDep은 1.107.0rc1·rc2도 깨끗하다고 확인했다. 현재 PyPI의 최신 버전도 1.106.2로 표시된다.

litellm을 import하지 않았는데도 감염되나?

그렇다. 악성 파일이 .pth와 sitecustomize.py 형태라 패키지가 설치된 환경에서 어떤 파이썬 프로세스든 시작되기만 하면 실행된다. 그래서 같은 가상 환경을 쓰는 CI 잡, 테스트, IDE 언어 서버도 모두 영향을 받는다.

LiteLLM 감염 시 어떤 키를 교체해야 하나?

그 호스트가 접근할 수 있던 비밀은 전부 교체 대상이다. LiteLLM은 인스턴스·태스크 IAM 역할, 프록시 DB의 모든 프로바이더 키, 마스터·가상 키, DATABASE_URL 비밀번호, 프로세스 환경 변수의 비밀을 명시했고, 탈취기 수집 범위를 보면 SSH·GPG 키와 CI/CD·AI 도구 토큰도 바꾸는 게 맞다.

출처

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록