Goインフラの悪用
(reverse.put.as)- 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/Mとexample.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.0revisionを知らないというエラーを返したが、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 MiBLICENSEファイルも最大16 MiB
- この制限は、ユーザー、proxy、モジュールエコシステムの他の部分に対するサービス拒否攻撃を緩和するための仕組みである
- 500 MiBは悪用可能性を考えると十分に大きいサイズである
想定される悪用シナリオ
- public Go proxyは、開発者マシンやCI/CDサーバーで宛先ダウンロード制限を回避するために使われる可能性がある
- private
GOPROXYがない状況を前提とする - マルウェアはペイロードをリポジトリにアップロードし、必要なときにproxyからダウンロードできる
- オリジンソースが消えても、checksum database entryには小さな痕跡だけが残る可能性がある
- private
proxy.golang.orgに対するDoSは実行が難しい可能性がある- 任意のGitリポジトリをproxyにダウンロードさせるようリクエストできる
- あり得る攻撃は、GitHub URLを大量に収集した後、
lookupAPIへ多数のリクエストを送る方式である - サーバー実装は不明だが、work queueのような並列処理制限があるかもしれない
- GitHub側の帯域保護が機能する可能性もある
- ストレージ容量を対象にしたDoSの可能性もあるが、推測にとどまる
- C2(command and control) は
proxy.golang.org上に容易に構築できる@latestクエリで特定モジュールの最新バージョンを見つけられる- ペイロードは単純なファイルでもよく、
go.modやGoソースファイルの中に隠すこともできる - 単一リポジトリの使用を避けるにはmodule DGAを使える
C2ダウンロードフロー
- implantがコマンドを受け取るには、次の手順を実行すればよい
https://proxy.golang.org/module_path/@latestリクエスト- JSON結果からpseudo-versionまたはversionを抽出
https://proxy.golang.org/module_path/@v/version.zipリクエストでzipをダウンロード- zipの内容を展開し、コマンドをパース
- このフローはGoコード300行以下でも実装できるほど単純である
結論と残る疑問
- Go checksum databaseとproxyは、文書化された手順に従い、未記録のモジュールをオリジンサーバーから取得して保存できる
- 現在の状態がGoインフラの深刻な問題に見えるわけではないが、容易に悪用でき、改善の余地もある
- Goコードではないリポジトリがproxyとchecksum databaseに載ることを許容している、文書化された理由または非公開の理由がある可能性がある
- すでに誰かがこれを悪用しているか確認するには、約160万件の一意なリポジトリと、最新のローカルdatabase基準で約2,200万件のentryを調査する必要がある
- 一部の有効なnon-Goプロジェクトがなぜdatabaseにあるのかは、まだ未解決の疑問として残っている
1件のコメント
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 の場合、認証情報を配布してログインさせたうえで、IMAP でアップロード済みの添付ファイルを読ませていた
元 Google SAD-SRE(Spam, Abuse, Delivery)だった
Python コード文字列の中にファイルを Base64 エンコードして入れることも可能
Googler であり個人の意見だが、この分野には詳しくない
Go チームが GCP や Drive 側と協業していたことを願う。悪意あるファイルホスティングは Google が常に扱っている問題だからだ
Google がすでに人々に任意データの投入を許可している他のエンドポイントと大きくは変わらない
Google は中央チームがインフラを管理し、全社に共有することに非常に長けている。メッセンジャーアプリでなければそうだし、完全な推測だが、Go チームは社内の blob ストレージを使っていて、不正利用対応やファイルスキャンを自動処理する社内インフラチームもいるのではないかと思う
PyPI にも非 Python プロジェクトはかなりある
Python ではユーザーがライブラリコードをコンパイルできない場合があるので、コンパイル済みバイナリである wheel を配布する能力が必要になる
そうしたコードは C で書かれることが多いが Golang[1] でも可能で、例は見つけられないものの、ライブラリではなくアプリケーション配布にも使われていたのを見た気がする
C でアプリを書いて PyPI に上げ、ユーザーに
pip installでインストールするよう案内するのはかなりクールだ[1] https://github.com/popatam/gopy_build_wheel_example
Linux なのに
lsが Python で書かれているようなものなので、こういうゲームはしないほうがよさそうだpipがパッケージをダウンロードするには venv/virtualenv/pipenv/pyenv のような環境内にいる必要がある、と要求するようになる前なら、この用途ははるかに有用だったと思うpip install cmakeもあるし、プロプライエタリなバイナリもpip install nvidia-cudnn-cu12のように可能だおかげで Homebrew を完全に捨てられた理由の一つになった
素朴な考えかもしれないが、これが GitHub リポジトリにファイルをアップロードすることとどう違うのか分からない
違いは GitHub ではアカウントを作る必要があるという点だけなのか? GitHub にも任意のデータを保存できるし、500MB 制限もない
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/
公開の透明性ログが欲しいだけなら、sigstore プロジェクトの公開 rekor インスタンスのほうがずっと適している
https://www.sigstore.dev/
https://docs.sigstore.dev/logging/overview/
自分が間抜けなだけかもしれないが、ここで正確に何が問題なのか分からない
プロキシが Go ではないリポジトリをキャッシュするのは少し無駄かもしれないが、そうしなくても Go リポジトリをキャッシュさせれば、結局任意のデータを保存させられるのでは?
何か見落としていない限り、完全に大したことではないように聞こえる
ここでの新しい話は、セキュリティのない公開プロキシがプロキシ対象を受け取り、それをセキュリティのない形で公開提供する、という程度に見える
記事では、一部の監視されたネットワークでは任意の Web URL より golang proxy URL のほうが信頼されやすく、レピュテーションフィルターの回避などに使える可能性があるとしているが、そうした方法はすでにいくつもあり、この方式が特別だとは思えない
話題からは外れるが、ドメインを見てダジャレが不穏だったので put.as を確認してみたら、だいたい予想どおりの内容だった
すでに知られている issue: https://github.com/golang/go/issues/31866
.modと.goファイルを追加すればよいだけではないか?彼と Aaron は Athens を作ったし、私の知る限り Marwan は Athens の基盤となった最初の Go ダウンロードプロトコル実装を書いた
この issue が興味深いのは、Athens がすでにモジュール検証の事前確認として言及されている
go mod download -jsonコマンドを使っている点だ概ね、リポジトリが Go モジュールコマンドの理解するモジュールとして通れば、Athens が提供する
もっと禁じ手っぽい言い方をすれば、モジュールバージョン・疑似バージョン・
+incompatibleを作れる必要があり、そのモジュールと依存関係が有効なチェックサムを生成しなければならないモジュールのチェックサムは現在、
.modとすべてのファイル、さらに各依存関係を再帰的に含めることに関係しているそのため筆者が言うように、基本的な Go プログラムさえあれば、設計上、任意ファイル用の領域を大量に持てる
W3C は Web 上のあらゆるものを強力にキャッシュできるよう基盤を整えたのに、汎用プロキシキャッシュがこれほど少ないのは奇妙だ
パブリッシャーが必要もないのに短い
Cache-Control: max-ageやVary: Cookieレスポンスを送っているのだろうか?ISP はピアリングよりもトランジット費用を払いすぎているのだろうか?
たとえば HTTPS ではないサイトでは、ISP のプロキシが広告を挿入できる
ソフトウェアのダウンロードには通常、署名やチェックサムがあるが、任意のコンテンツにはそうしたものはあまりない