2P by GN⁺ | ★ favorite | 댓글 1개
  • Go의 checksum database와 public module proxy는 Go 코드가 없는 저장소까지 받아들일 수 있어, 임의 Git 저장소 데이터를 Go 인프라에 적재하고 다시 내려받는 경로가 생김
  • sum.golang.org/lookup/$module@$version 요청은 기록되지 않은 모듈 버전을 만나면 원본 서버에서 가져오며, 이 과정에서 proxy.golang.org에도 해당 저장소 zip이 제공됨
  • Homebrew의 Ruby 저장소와 Rust 저장소 fork가 checksum database에 있었고, 새 Go 모듈뿐 아니라 Go 파일이 없는 저장소도 pseudo-version으로 등록되는 실험이 성공함
  • 모듈 zip에는 압축·비압축 모두 최대 500 MiB 제한이 있지만, 개발자 머신·CI/CD의 다운로드 제한 우회, 페이로드 저장, C2 구현에는 충분히 큰 규모임
  • sum.golang.org의 고유 경로 약 159만 개 중 GitHub 경로가 약 151만 개로 95% 수준이며, Go 생태계의 GitHub 의존도와 public proxy 악용 가능성이 함께 드러남

Go checksum database에서 발견된 비-Go 저장소

  • Go의 checksum database를 살펴보던 중 modules 테이블에서 github.com/homebrew/homebrew-core가 매우 많이 나타남
    • github.com/homebrew/homebrew-core: 39,438개
    • github.com/Homebrew/homebrew-core: 30,896개
    • github.com/concourse/concourse: 25,372개
    • github.com/openshift/release: 24,065개
    • github.com/cilium/cilium: 22,138개
  • Homebrew 저장소는 Ruby를 사용하는 것으로 알려져 있고, 저장소와 클론한 파일에서도 go.mod나 Go 소스 파일이 발견되지 않음
  • 대소문자 차이는 Go 문서case encoding 규칙으로 설명됨
    • 대문자는 !와 해당 소문자로 인코딩돼, 대소문자를 구분하지 않는 파일 시스템에서도 example.com/Mexample.com/m을 함께 저장할 수 있음
  • github.com/Edu4rdSHL/rust-headless-chrome도 Go와 관련 없는 Rust 저장소 fork인데 checksum database에 나타남

/lookup이 저장소를 가져오는 방식

  • Go 모듈 문서의 Go Modules Reference에 따르면, Go 명령은 checksum database를 조회할 때 먼저 /lookup 엔드포인트에서 레코드 데이터를 가져옴
  • 모듈 버전이 아직 로그에 기록되지 않았다면 checksum database가 응답 전에 원본 서버에서 해당 모듈을 가져오려 시도함
  • 엔드포인트 형식은 $base/lookup/$module@$version
    • $module$version에 대한 로그 레코드 번호, go.sum 라인, 서명된 트리 설명을 반환함
  • github.com/homebrew/homebrew-core의 pseudo-version을 조회하면 checksum record와 go.mod 해시가 반환됨
  • 저장소에 버전 태그가 없으면 Go의 pseudo-version 규칙이 사용됨

새 Go 모듈 등록 실험

  • 새 Go 모듈 github.com/gdbinit/fluxmatter를 만든 뒤 lookup 요청으로 등록 여부를 확인함
  • @latest 조회는 canonical version이 아니라는 오류를 반환함
    • bad request: version "latest" is not canonical
  • @v0.0.0 조회는 해당 revision을 모른다는 오류를 반환함
    • not found: ... invalid version: unknown revision v0.0.0
  • 하지만 checksum database를 다시 동기화해 조회하자 모듈이 등록돼 있었음
    • github.com/gdbinit/fluxmatter|v0.0.0-20240524163826-a7e64ffd69f2|2024-05-24T16:40:51.203837Z
  • proxy.golang.org/github.com/gdbinit/fluxmatter/@latest는 pseudo-version과 GitHub 원본 정보를 반환했고, 해당 버전의 zip 파일도 다운로드 및 압축 검증이 가능했음
  • 초기 시딩에는 정확한 버전을 지정하지 않아도, 모듈 경로와 버전처럼 보이는 값을 담은 lookup 쿼리만으로 동작함

Go 코드가 없는 저장소도 public proxy에 적재됨

  • Go 코드가 전혀 없는 github.com/gdbinit/readmem 저장소로도 같은 실험이 성공함
  • lookup 요청은 v0.0.0 revision을 모른다는 오류를 반환했지만, checksum database에는 pseudo-version으로 등록됨
    • github.com/gdbinit/readmem|v0.0.0-20131006075740-407cb0a56933|2024-05-24T16:45:35.88456Z
  • proxy.golang.org@latest는 해당 저장소의 pseudo-version과 Git 원본 정보를 반환함
  • 내려받은 zip에는 Go 파일이 아니라 Entitlements.plist, README, Xcode project 파일, main.c 등이 들어 있었음
  • 이 실험은 GitHub 저장소를 사용했지만, 동작하는 VCS라면 다른 호스팅 사이트에서도 가능할 수 있음

GitHub 의존도와 크기 제한

  • checksum database의 고유 경로 수는 1,591,375개였고, 그중 github.com% 경로는 1,515,957개였음
  • 고유 경로의 약 95% 가 GitHub에 호스팅됨 {p:95}
  • 이 수치는 fork나 실제 Go 코드가 아닌 대상을 제거하지 않은 원시 통계임
  • Go 모듈 zip에는 File path and size constraints가 적용됨
    • module zip 파일은 최대 500 MiB
    • 파일의 총 비압축 크기도 최대 500 MiB
    • go.mod 파일은 최대 16 MiB
    • LICENSE 파일도 최대 16 MiB
  • 이 제한은 사용자, proxy, 모듈 생태계의 다른 부분에 대한 서비스 거부 공격을 완화하기 위한 장치임
  • 500 MiB는 악용 가능성을 고려하면 충분히 큰 크기임

가능한 악용 시나리오

  • public Go proxy는 개발자 머신이나 CI/CD 서버에서 목적지 다운로드 제한을 우회하는 데 쓰일 수 있음
    • private GOPROXY가 없는 상황을 전제로 함
    • 악성코드는 페이로드를 저장소에 올린 뒤 필요할 때 proxy에서 내려받을 수 있음
    • 원본 소스가 사라져도 checksum database entry에는 작은 흔적만 남을 수 있음
  • proxy.golang.org에 대한 DoS는 실행이 어려울 수 있음
    • 임의 Git 저장소를 proxy가 다운로드하도록 요청할 수 있음
    • 가능한 공격은 GitHub URL을 많이 수집한 뒤 lookup API에 많은 요청을 보내는 방식임
    • 서버 구현을 알 수 없지만 work queue 같은 병렬 처리 제한이 있을 수 있음
    • GitHub 측 대역폭 보호가 작동할 가능성도 있음
    • 저장 공간을 대상으로 한 DoS 가능성도 있으나 추정에 그침
  • C2(command and control)proxy.golang.org 위에 쉽게 만들 수 있음
    • @latest 쿼리로 특정 모듈의 최신 버전을 찾을 수 있음
    • 페이로드는 단순 파일일 수도 있고, go.mod나 Go 소스 파일 안에 숨길 수도 있음
    • 단일 저장소 사용을 피하려면 module DGA를 쓸 수 있음

C2 다운로드 흐름

결론과 남은 질문

  • Go checksum database와 proxy는 문서화된 절차에 따라, 기록되지 않은 모듈을 원본 서버에서 가져와 저장할 수 있음
  • 현재 상태가 Go 인프라의 심각한 문제로 보이지는 않지만, 쉽게 악용될 수 있고 개선 여지도 있음
  • Go 코드가 아닌 저장소가 proxy와 checksum database에 올라갈 수 있도록 둔 문서화된 이유나 비공개 이유가 있을 수 있음
  • 이미 누군가 이를 악용하고 있는지 확인하려면 약 160만 개 고유 저장소와 최신 로컬 database 기준 약 2,200만 개 entry를 조사해야 함
  • 일부 유효한 non-Go 프로젝트가 왜 database에 있는지는 아직 열린 질문으로 남아 있음

댓글과 토론

Hacker News 의견들
  • 사용자가 자료를 올리고 그 자료가 공개로 보이는 온라인 서비스는 결국 명령제어, 저작권 침해, CSAM 호스팅에 쓰이게 됨
    파일 호스팅 외에도 중요한 용도가 있어 차단하기 어려운 서비스일수록 특히 그렇고, Twitter[1], Telegram[2], PGP 키 인프라[3]에서도 이미 벌어진 일이며 GitHub 같은 뻔한 대상은 말할 것도 없음
    [1] https://pentestlab.blog/2017/09/26/command-and-control-twitt...
    [2] https://www.blazeinfosec.com/post/leveraging-telegram-as-a-c...
    [3] https://torrentfreak.com/openpgp-keyservers-now-store-irremo...

    • Gmail, Google Groups, Google Drive, Gchat도 마찬가지였고, 저장한 데이터가 공개일 필요조차 없었음
      Gmail의 경우 자격 증명을 배포해서 로그인하게 한 뒤, IMAP으로 올려둔 첨부파일을 읽게 했음
      전 Google SAD-SRE(Spam, Abuse, Delivery)였음
    • PyPI도 패키지에 임의의 비 Python 파일을 넣을 수 있으니 이런 용도로 쓰기 쉬워 보임
      Python 코드 문자열 안에 파일을 Base64로 인코딩해 넣는 식도 가능함
    • 이미 있었는지는 모르겠지만, 덜 obvious한 대상은 HuggingFace로 보임
  • Googler이고 개인 의견이며, 이 분야는 잘 모름
    Go 팀이 GCP와 Drive 쪽과 협업했기를 바라는데, 악성 파일 호스팅은 Google이 늘 다루는 문제이기 때문임
    사람들이 임의 데이터를 넣도록 Google이 이미 허용하는 다른 엔드포인트와 크게 다르지 않음

    • 전 Googler이고 Go Dev Tools 팀은 잘 모르지만, Google은 내가 일했거나 가까운 친구들에게 들은 거대 기업들 중 이런 식의 사내 협업을 거의 가장 잘하는 편임
      Google은 중앙 팀이 인프라를 관리하고 전사에 공유하는 데 매우 능숙함. 메신저 앱만 아니면 그렇고, 순전히 추측이지만 Go 팀은 내부 blob 저장소를 쓰고 있을 것이며 악용 대응과 파일 스캔을 자동 처리하는 내부 인프라 팀도 있을 것 같음
  • PyPI에도 비 Python 프로젝트가 꽤 있음
    Python은 사용자가 라이브러리 코드를 컴파일하지 못할 수 있으니 컴파일된 바이너리인 wheel 배포 능력이 필요함
    그런 코드는 C로 많이 쓰지만 Golang[1]도 가능하고, 예시는 못 찾겠지만 라이브러리가 아니라 애플리케이션 배포에도 쓰인 걸 본 것 같음
    C로 앱을 작성해 PyPI에 올리고 사용자에게 pip install로 설치하라고 하는 건 꽤 멋짐
    [1] https://github.com/popatam/gopy_build_wheel_example

    • 가령 Python 사용 요건을 넣더라도, 악의적으로 맞추려는 사람은 최소한의 Python 스텁 코드만 제공하면 되지 않나 싶음
      Linux인데 ls가 Python으로 쓰인 셈이니, 이런 게임은 안 하는 편이 나을 것 같음
    • pip가 패키지를 내려받으려면 venv/virtualenv/pipenv/pyenv 같은 환경 안에 있어야 한다고 요구하기 전에는 이 용도가 훨씬 유용했을 것 같음
    • pip install cmake도 있고, 독점 바이너리도 pip install nvidia-cudnn-cu12처럼 가능함
    • 최근 PyPI를 FFmpeg와 Eigen 같은 비 Python 도구에 많이 쓰고 있음
      덕분에 Homebrew를 완전히 버릴 수 있었던 이유 중 하나임
  • 순진한 생각일 수도 있지만, 이게 GitHub 저장소에 파일을 올리는 것과 어떻게 다른지 모르겠음
    차이가 GitHub는 계정을 만들어야 한다는 점뿐인가? GitHub에도 임의 데이터를 저장할 수 있고, 500MB 제한도 없음

    • GitHub는 익명 요청에 꽤 엄격한 요청 제한이 있음
  • CUE의 모듈 시스템이 드디어 출시되고 있고, MVS는 Go와 비슷하지만 OCI 인프라 위에 만들어졌음
    의존성 관리 시스템에 관심이 있다면 참고할 링크들임
    proposal: https://github.com/cue-lang/proposal/tree/main/designs/modul...
    custom registry: https://cuelang.org/docs/tutorial/working-with-a-custom-modu...
    road map: https://github.com/orgs/cue-lang/projects/10/views/8
    0.9.0-alpha-5부터 모듈이 기본 활성화됨: https://github.com/cue-lang/cue/releases/tag/v0.9.0-alpha.5
    Go Sum에서는 Trillian 프로젝트가 투명성 로그를 뒷받침함: https://github.com/google/trillian
    CUE는 증명(attestation) 같은 OCI 옵션에 얹혀 가려는 계획임

    • 그게 링크된 글과 무슨 관련이 있는지 모르겠음
  • golang proxy와 sumdb에 편승, 즉 남용해서 임의 URL의 체크섬에 대한 무료 투명성 로그를 만드는 아이디어를 가지고 놀아봤음
    https://getsum.pub/

  • 내가 멍청한 건지도 모르겠지만, 여기서 정확히 뭐가 문제인지 모르겠음
    프록시가 비 Go 저장소를 캐시하는 건 조금 낭비일 수 있지만, 그걸 안 하더라도 Go 저장소를 캐시하게 만들면 어차피 임의 데이터를 저장하게 할 수 있지 않나?
    뭔가 놓친 게 아니라면 완전히 별일 아닌 것처럼 들림

    • 놓친 건 없는 것 같음
      여기서 새 소식은 보안 없는 공개 프록시가 프록시할 것을 받아서 보안 없는 방식으로 공개 제공한다는 정도로 보임
      글에서는 일부 감시되는 네트워크가 임의 웹 URL보다 golang proxy URL을 더 신뢰할 수 있어 평판 필터 우회 등에 쓰일 수 있다고 하지만, 그런 방법은 이미 여러 가지가 있고 이 방식이 특별해 보이진 않음
  • 주제에서 벗어나지만 도메인을 보고 말장난이 불길해서 put.as를 확인해봤고, 대체로 예상한 내용이었음

    • https://put.as/ 약간 NSFW임
    • 직장에서 그걸 열어보는 실수를 했음
    • 스페인어 복수형 단어이긴 함. 하지만…
  • 이미 알려진 이슈임: https://github.com/golang/go/issues/31866

    • 그 수정은 실수 방지에는 도움이 되겠지만, 의도적으로 하려는 사람은 루트에 .mod.go 파일을 추가하면 되는 것 아닌가?
    • Marwan이 이 이슈에 있는 건 전혀 놀랍지 않음
      그와 Aaron은 Athens를 만들었고, 내가 알기로 Marwan은 Athens의 기반이 된 최초의 Go 다운로드 프로토콜 구현을 작성했음
      이 이슈가 흥미로운 건 Athens가 이미 모듈 검증의 사전 확인으로 언급된 go mod download -json 명령을 사용한다는 점임
      대체로 저장소가 Go 모듈 명령이 이해하는 모듈로 통과되면 Athens가 제공함
      더 금지된 표현으로 말하면, 모듈 버전·의사 버전·+incompatible을 만들 수 있어야 하고, 그 모듈과 의존성이 유효한 체크섬을 만들어야 함
      모듈 체크섬은 현재 .mod와 모든 파일, 그리고 각 의존성을 재귀적으로 포함하는 것과 관련 있음
      그래서 글쓴이가 말한 것처럼 기본적인 Go 프로그램만 있으면 설계상 임의 파일을 위한 공간을 많이 가질 수 있음
  • W3C는 웹의 모든 것이 강하게 캐시될 수 있도록 기반을 깔았는데, 범용 프록시 캐시가 이렇게 적은 건 이상함
    게시자가 필요도 없는데 짧은 Cache-Control: max-ageVary: Cookie 응답을 보내고 있는 걸까?
    ISP들이 피어링보다 트랜짓 비용을 너무 많이 내고 있는 걸까?

    • 일반적으로 캐시가 내용을 변조하지 않았는지 보장할 방법이 없음
      예를 들어 HTTPS가 아닌 사이트에서는 ISP 프록시가 광고를 삽입할 수 있음
      소프트웨어 다운로드는 보통 서명과 체크섬이 있지만, 임의 콘텐츠에는 그런 게 별로 없음