원문 캡처 · research.jfrog.com
원문 캡처 · research.jfrog.com

JFrog Security Research 팀은 널리 쓰이는 Python 인터페이스·프록시 패키지 litellm 1.107.0의 악성 휠을 확인했다. 영향을 받는 7개 플랫폼 휠에는 Python 시작 시 실행되는 파일이 포함돼 자격 증명과 비밀 정보를 훔치며, 애플리케이션이 litellm을 가져오지 않아도 작동할 수 있다.

이번 사건은 2026년에 기록된 두 번째 LiteLLM 공급망 침해다. 공개된 소스 배포판(sdist)에는 휠에서 발견된 악성 파일 3개가 없다. 따라서 소스 코드나 Git 태그만 검토하면 플랫폼별 바이너리 아티팩트에 실린 악성 동작을 놓칠 수 있다. 관련 논의는 LiteLLM 보안 이슈 #47826에서 진행 중이다.

JFrog 보안 연구원 가이 코롤레프스키(Guy Korolevski)에 따르면, 모든 litellm 1.107.0 휠에는 cook.pth, cook.py, sitecustomize.py가 들어 있다. 반면 소스 타르볼에는 이 파일들이 없지만, 대응하는 Git 태그에 없는 pyproject.toml 설정 data = "cook"가 추가돼 있다. 휠 파일의 타임스탬프와 메타데이터는 악성 파일이 ZIP을 수동으로 다시 포장하는 과정이 아니라 정상적인 휠 빌드 과정에서 포함됐을 가능성을 뒷받침한다. 다만 누가 빌드 트리를 바꿨는지, 어떻게 변경됐는지, 어떤 경로로 배포됐는지는 확인되지 않았다.

JFrog는 2026년 3월에 발생한 첫 침해도 기록했다. 당시 파이프라인에서 버전이 고정되지 않은 악성 Trivy 0.69.4가 실행돼 CI/CD와 배포 자격 증명이 노출됐고, 탈취된 자격 증명으로 백도어가 포함된 litellm 1.82.7과 1.82.8이 배포됐다. 이 버전들은 자격 증명을 수집하고 Kubernetes 노드로 확산해 지속성 있는 백도어를 설치했다. JFrog는 이를 XRAY-955589로 추적한다. 이번 침해는 페이로드와 인프라가 다르며, 현재 TeamPCP나 이전 침입 경로와 연결하는 증거는 없다. 두 사건은 별개의 캠페인으로 다뤄야 한다. 자세한 내용은 이전 침해와의 비교와 소스 배포판과 휠의 차이를 참조할 수 있다.

휠 설치 프로그램은 purelib 디렉터리의 파일을 환경 내 site-packages 루트에 놓는다. 악성 휠의 RECORD에는 다음 파일들이 등록돼 있다: litellm-1.107.0.data/purelib/cook.pth, litellm-1.107.0.data/purelib/cook.py, litellm-1.107.0.data/purelib/sitecustomize.py. 정상 라이브러리 트리와 비교했을 때 컴파일된 Rust 브리지 등 휠별 차이를 제외한 나머지 코드는 정상 릴리스와 일치하는 것으로 보였다.

cook.pth에는 import cook 한 줄이 들어 있다. Python의 site 모듈은 인터프리터 초기화 중 .pth 파일을 처리하며, import로 시작하는 줄을 실행한다. 그 결과 영향을 받은 환경에서 Python이 시작되면 애플리케이션이 litellm을 가져오지 않아도 cook.py가 실행된다. sitecustomize.py는 Python이 시작할 때 자동으로 가져오려는 모듈 이름을 이용해 별도의 로더를 시작한다. 따라서 영향을 받은 환경에서 pip 같은 관리 도구를 실행하는 것도 악성 코드를 다시 작동시킬 수 있다. 관련 분석은 Python 시작 훅의 동작에서 확인할 수 있다.

cook.py는 Base64로 인코딩하고 zlib으로 압축한 Python 프로그램을 exec()에 전달한다. 코드를 실행하지 않고 정적으로 해독하면 약 61KB의 Python 소스가 나오며, 프로그램은 자신을 privcollect라고 부른다. 이 코드는 데몬 스레드에서 수집기를 실행하고, 성공적으로 정보를 빼낸 뒤에야 실행 완료 표시 파일 /tmp/.pc_once_72129118c59f5636c0bb를 만든다. 따라서 이 파일이 없다는 사실만으로 호스트가 깨끗하다고 판단할 수 없다. 자세한 내용은 cook.py와 자격 증명 수집기를 참조하면 된다.

수집 대상에는 개발자 작업 공간, CI/CD 실행기, 클라우드 작업 부하, 컨테이너와 LiteLLM 프록시 배포 환경의 정보가 포함된다. SSH 파일, GPG·TLS 개인 키, 셸 및 데이터베이스 클라이언트 기록, 브라우저 암호·쿠키·기록, 클라우드와 Kubernetes 자격 증명, Docker 비밀 정보, 소스 관리·패키지 관리자 자격 증명을 노린다. GitLab Runner, Jenkins, Vault, Terraform, Packer의 비밀 정보와 Cursor, Claude, Codex, Gemini, GitHub Copilot, Hugging Face 등 AI 개발 도구의 자격 증명과 설정도 대상이다. Linux에서는 현재 프로세스와 읽을 수 있는 다른 프로세스의 환경 변수, AWS·Google Cloud·Azure의 인스턴스 메타데이터 자격 증명도 수집을 시도한다. 전체 대상 목록은 수집 정보 분석에 정리돼 있다.

LiteLLM 프록시 호스트에서는 DATABASE_URL을 통해 LiteLLM_CredentialsTable과 LiteLLM_ProxyModelTable의 데이터도 노린다. 데이터베이스에 연결해 여러 LLM 공급자의 자격 증명과 프록시 모델 설정을 덤프하려는 기능이다. 클라우드 모듈은 탈취한 AWS 자격 증명으로 GetCallerIdentity를 호출해 유효성을 확인하려 한다. 따라서 대응할 때는 로컬에서의 정보 탈취뿐 아니라 유출된 클라우드 자격 증명이 외부에서 사용됐는지도 살펴야 한다.

수집 데이터는 JSON으로 직렬화한 뒤 무작위 AES-256-CBC 키로 암호화한다. 이 키는 내장된 RSA-4096 공개 키로 OAEP-SHA256 방식으로 감싼 다음, 암호화한 자료를 tar.gz 아카이브로 묶는다. 아카이브는 평문 HTTP를 통해 hxxp[:]//www[.]litellm[.]dev/healthcheck로 전송되며, 요청에는 X-Token: XWiA8gZNa7NcX2XRoAEb와 X-SHA256 헤더가 포함된다. 전송 계층은 암호화되지 않지만, 공격자의 RSA 개인 키가 없으면 네트워크 캡처만으로 유출된 내용을 복원할 수 없다. www[.]litellm[.]dev는 LiteLLM의 정상 도메인 litellm[.]ai와 다르며, 이전 사건의 models[.]litellm[.]cloud와도 구별해야 한다. 암호화된 정보 유출 분석에서 자세한 내용을 볼 수 있다.

sitecustomize.py는 /tmp/remote_code_loader.lock에 비차단 단독 잠금을 걸고, 잠금에 성공하면 os.fork()로 분리된 로더를 시작한다. 이 프로세스는 매시간 다음 주소에서 C2 도메인을 가져온다: hxxps[:]//raw[.]githubusercontent[.]com/letters9813/pythonclient-test/main/domain.txt. 유효한 도메인을 찾으면 내장된 privcollect 사본을 동기적으로 실행한 뒤 hxxp[:]//{domain}/codepy에서 Python 코드를 평문 HTTP로 받아 compile()과 exec()로 실행한다. 무결성 확인이나 인증 절차가 없어 해당 서버를 제어하는 쪽은 영향을 받은 사용자 권한으로 임의 코드를 실행할 수 있다.

관찰 당시 domain.txt를 읽기 전용으로 요청한 결과는 리디렉션 없는 HTTP 200 응답과 길이가 정확히 0바이트인 빈 텍스트였다. 따라서 확인 시점에는 로더가 비활성 상태였지만, 파일 내용을 바꾸면 다시 활성화할 수 있다. 이 관찰은 cook.py의 자격 증명 수집에는 영향을 주지 않는다. 또한 sitecustomize.py의 포크 방식은 os.fork()에 의존하므로 Windows에서는 데몬 실행이 조용히 실패하지만, cook.pth가 cook.py를 불러오므로 Windows 설치 환경도 자격 증명 탈취에 노출된다. 로더 분석은 sitecustomize.py의 시간별 원격 코드 실행에서 확인할 수 있다.

대응 방법

JFrog는 litellm 1.107.0 휠 7개를 모두 피하라고 권고한다. litellm==1.106.2로 고정할 때도 시작 훅을 먼저 제거하거나 새로 만든 깨끗한 환경에서 설치해야 한다. 영향을 받은 휠이 설치됐거나 해당 환경에서 Python이 실행된 호스트와 컨테이너는 격리하고, 접근 가능했던 자격 증명과 데이터는 침해된 것으로 취급해야 한다.

영향을 받은 환경에서 pip를 실행하는 것으로 복구를 시작하면 안 된다. Python 시작 과정에서 cook.pth와 sitecustomize.py가 다시 실행될 수 있다. 영향을 받은 인터프리터를 호출하지 않는 도구를 사용해 cook.pth, cook.py, sitecustomize.py, __pycache__ 아래의 대응 컴파일 파일, /tmp/.pc_once_72129118c59f5636c0bb, /tmp/remote_code_loader.lock을 먼저 제거해야 한다. sitecustomize.py와 관련된 분리 Python 프로세스, 특히 잠금 파일을 보유하거나 setsid를 사용하거나 작업 디렉터리가 /인 프로세스도 조사하고 종료해야 한다. /tmp/.pc_sq_*와 /tmp/.pc_e_* 파일도 확인 대상이다.

www[.]litellm[.]dev, raw[.]githubusercontent[.]com/letters9813/pythonclient-test/main/domain.txt와 해당 파일이 반환한 도메인에 대한 접근을 차단하고 조사해야 한다. 프록시, DNS, 엔드포인트, 클라우드 감사 로그는 보존한다. 영향을 받은 호스트에서 접근할 수 있었던 브라우저 암호, SSH·GPG 키, TLS 개인 키, 패키지 배포·소스 관리 토큰, 클라우드·Kubernetes 자격 증명, AI 개발 토큰, DATABASE_URL, LiteLLM 마스터·가상 키, LiteLLM_CredentialsTable에 저장된 공급자 자격 증명을 모두 교체해야 한다. /codepy를 가져오거나 실행했는지도 조사해야 한다. 이 단계는 임의 Python 코드를 실행할 수 있으므로 내장 수집기의 동작만으로 영향을 한정할 수 없다.

영향받은 가상 환경, 호스트, 컨테이너와 이미지는 검증된 아티팩트로 포렌식 재구축하는 편이 안전하다. 시작 파일, 내려받은 코드, 노출된 비밀 정보가 모두 제거됐음을 하나씩 입증하려는 것보다 재구축을 권한다. 전체 권고는 대응 지침에서 확인할 수 있다.

JFrog는 이번 사건이 소스 출처 확인과 배포 아티팩트 무결성 사이의 차이를 보여준다고 설명한다. 변조된 빌드 설정과 휠 메타데이터는 조사에 중요한 단서지만 정확한 근본 원인을 입증하지는 않는다. JFrog Xray와 JFrog Curation은 XRAY-955589를 사용해 악성 litellm 1.107.0을 탐지한다. 결론과 침해 지표도 공개됐다.