Skip to content
AI Ferryman Alex
Go back

蠕蟲用三個開發者的賬號推了惡意代碼,git 日誌卻說那就是他們本人

10 月 29 號早上,一個叫 Ridham Parmar 的工程師打開 GitHub,發現自己的賬號在一夜之間往 9 個倉庫裏強推了 99 個分支。推送發生在凌晨 0 點 33 分到 0 點 49 分之間,那會兒他正在睡覺。每一筆提交都留着他的名字、他寫的提交信息和原來的日期。你上一次檢查自己的賬號有沒有替你推過你沒推過的東西,是什麼時候?

這個蠕蟲叫 PolinRider。被它打中的團隊叫 Improwised,他們在 10 月 31 號把整個經過寫了出來。我寫這篇,就是因爲他們肯把時間線原原本本貼出來,而不是藏起來,所以別人能順着事情發生的順序學到東西。

入侵時間線

事情是從 10 月 1 號開始的。那一天,他們那個開源答題應用的 20 個分支先被強推了一遍,推的人顯示的是一個同事的賬號。

到了 10 月 4 號,第二個賬號又往他們官網倉庫的 20 個分支強推了一遍。也是在同一天,一個外面來的研究員在公開 issue 裏報了這個惡意代碼,因爲他們公司的安全郵箱根本收不進他那封郵件。研究員隨後又提了一個 PR,想把惡意代碼刪掉,但是這個 PR 當天就被關了,沒有合進去。

10 月 8 號,第三個開發者克隆了官網倉庫,還跑了一下它的開發服務器,蠕蟲就這樣裝到了他的筆記本上。

到了 10 月 28 號,一次 GitHub CLI 登錄把一把能動所有倉庫的 token 放到了那臺已經被感染的筆記本上。第二天凌晨,蠕蟲就用這把 token 把那 99 個分支強推了一遍。

總共算下來,10 個倉庫裏的 139 個分支,都是從 3 個開發者的賬號推出去的,而蠕蟲在他們的倉庫裏待了 28 天。就他們目前所知,還沒有客戶的系統和數據受到影響。

蠕蟲在 28 天裏怎麼傳開的:10 月 1 號答題應用的 20 個分支被強推,10 月 4 號官網倉庫的 20 個分支又被強推,10 月 8 號第三位開發者克隆官網倉庫、跑了開發服務器,蠕蟲裝上了他的筆記本,10 月 28 號一把能動所有倉庫的 token 落到那臺筆記本上,10 月 29 號凌晨強推 99 個分支;合計 10 個倉庫、139 個分支、3 個賬號、28 天

蠕蟲留下的痕跡

139 個分支聽起來就是整件事,但是先別盯着這個數看。比起這個數,我讀完他們的文章之後忘不掉的,是這個蠕蟲留下的痕跡有多麼少。

它改的是每個分支上最新的一筆提交,改完再強推上去,而提交信息、作者和日期全都原樣留着。所以你打開 git log 去看,被改過的那筆提交跟原來那筆長得一模一樣。

提交日期在這裏一點用都沒有,因爲蠕蟲把原來的日期也留下來了。所以唯一可信的時鐘是服務器那邊的推送時間,得去 GitHub 的活動日誌和強推記錄裏查。

蠕蟲改的是每個分支上最新的一筆提交,名字、提交信息和日期全都原樣留着,再強推上去,所以被改過的提交跟原來那筆長得一模一樣;提交日期在這裏一點用都沒有,唯一可信的時鐘是 GitHub 活動日誌和強推記錄裏的推送時間

惡意代碼的藏身處

惡意代碼本身就藏在明處。它是一段混淆過的 JavaScript 代碼,接在一個本來就有的行尾後面,前面墊了幾百個空格。

它藏的地方,是項目在跑 install、dev、build 或者 test 的時候會加載的那幾個配置文件。文件名一共五個:svelte.config.js、postcss.config.js、nuxt.config.ts,還有 next.config.js 和 server.ts。

在編輯器或者 diff 界面裏看,這一行看着完全正常,除非你一路往右滾到底。

命令通道與常駐

它要聯繫的那個服務器地址,也不是一個能封掉的域名,因爲惡意代碼是從以太坊和 BNB 智能鏈的交易記錄裏讀出這個地址的,走的是公開的 RPC 服務,所以你提前沒東西可封。

它還騎在你每天要跑上百次的工具上。在一個開發者的筆記本上,惡意代碼把自己寫進了 npm 自己的 lib/cli.js。所以從那以後,每一次 npm 或者 npx 命令都會把它重新啓動一遍,那個開機自啓的工具也不例外。這就是爲什麼他們給團隊的第一條指令是先別再跑 npm。

惡意代碼藏在行尾幾百個空格後面,躲在 install、dev、build、test 時會加載的五個配置文件裏,標記是寫成 global.o='N-NNNN-du' 的全局變量;它還把自己寫進 npm 的 lib/cli.js,每跑一次 npm 或者 npx 就重啓一遍;命令服務器的地址從以太坊和 BNB 智能鏈的交易記錄裏讀,走公開 RPC,所以沒有域名可以提前封

CI 構建機

你的構建機器也會把這個東西跑起來,而這一層是大多數團隊從來沒想過的。惡意代碼就住在構建過程要加載的配置文件裏,所以只要有一次提交是被感染的,那一次的 CI 構建就會在構建機器上把它跑一遍。靜態站點的構建也一樣,因爲它加載的是同一批配置文件。

CI 就是你每次推送代碼之後,幫你構建和測試代碼的那臺機器。在這件事裏,CI 拿被改過的提交構建並且推送了兩個容器鏡像,其中一個裏面就裝着蠕蟲的遠程控制工具包。

那個鏡像最後沒在他們的集羣裏跑起來,因爲那個容器只用 nginx 提供靜態文件,但是他們還是把兩個鏡像都當成已經被污染處理,直接從 Docker Hub 上刪掉了。

他們的官網構建從 10 月 4 號開始就一直在加載被感染的配置文件。但是惡意代碼是衝着構建機器去的,不是衝着頁面去的,所以他們檢查了線上站點,裏面確實沒有。郵箱的憑證也很可能沒暴露,因爲這些憑證放在 Cloudflare Workers 裏當作運行時機密,而不是放在構建環境裏。

被波及的流水線機密

所以這件事的波及面比代碼本身大得多。因爲蠕蟲是騎着構建過程走的,而構建過程能看到你流水線裏的所有機密:鏡像倉庫的密碼、部署用的密鑰、放在 CI 變量裏的 API token。

只要你的構建跑過一次被感染的提交,那麼這些東西就全得輪換一遍,而且所有的鏡像和部署都要拿乾淨的提交重新構建。

構建這一層的波及面:被感染的提交一跑構建,蠕蟲就在構建機上跑起來,CI 還構建並推送了兩個容器鏡像,其中一個裝着蠕蟲的遠程控制工具包;構建能看到的流水線機密全得輪換,鏡像和部署都要拿乾淨的提交重新構建

錯過的機會

他們其實有三次機會可以早點抓住它,而且他們都老老實實寫出來了。10 月 4 號,那個外面來的研究員就已經在公開 issue 裏,把他們 10 月 29 號才拼出來的大部分結論交到他們手裏了,整整早了 25 天。可是那個想刪掉惡意代碼的 PR,同一天就被關了。

到了 10 月 13 號,一個升級依賴的 PR 把惡意代碼從一個項目的一個分支裏刪掉了。但是沒有人接着問下去,這段代碼是怎麼進來的、是哪個賬號推的,還有哪些分支也有。他們自己對這次修復的說法是,它給他們爭取了 16 天。

你要是發現了被注入的代碼,那麼就別把它當成一個 bug 來修,而要把它當成一次事件來處理:先找到推它的那個賬號,然後再去找這個賬號還能推到的所有地方。

第一條規矩

這是我會給任何一個團隊立的第一條規矩。一筆提交掛着你的名字,不代表就是你寫的,因爲蠕蟲會把名字、提交信息和日期都原樣留下來。而主分支幹乾淨淨,也說明不了其他分支沒事,因爲蠕蟲會碰它能推到的每一個分支。

蠕蟲能在他們的倉庫裏待 28 天,是因爲該跟的線索一次都沒有人跟到底。10 月 4 號那次,關掉的是一個能刪掉惡意代碼的 PR。10 月 13 號那次,刪掉的是一個分支上的一段代碼。兩次都沒有人順着賬號往下跟。

自己動手的三項檢查

我自己這週也會從賬號這條線查起。第一件,查活動日誌,而不是提交日誌。去找團隊裏沒有人記得的強推記錄,而且要看推送時間,因爲提交日期是僞造的。

第二件,查那些特別長的行。去配置文件裏搜行尾墊了 150 個以上空格的代碼,還要搜惡意代碼留下的標記,也就是一個被設成 global.o=‘N-NNNN-du’ 這種樣子的全局變量。

第三件,查進程。看 npm 命令跑完之後,是不是還掛着一個脫離出來的 node -e 進程,還要看那些在你不認識的推送之後開始失敗的 CI 構建。

他們把一個只讀的掃描器開源了出來。它會檢查一臺機器上的每一個分支、標籤和暫存區,也會檢查進程和網絡連接。它不會運行它掃到的任何代碼。

本該早做的防範

而那些本來可以在第一天就攔住它的改動,都是些無聊的東西,所以才一直沒人去做。在默認分支和發佈分支上禁止強推。把審計日誌裏的強推事件接上告警,這樣一次大規模推送當天夜裏就會有人被叫起來,而不是等到第二天早上再發現。在受保護的分支上要求提交簽名。

把那些能推所有地方的長期 token 和 SSH 密鑰換掉,改成短期的、權限收得很窄的 token。拿不準的代碼只放在一次性的虛擬機裏跑,永遠別放在工作用的機器上,面試題和別人讓你「隨便看一眼」的倉庫也一樣算。

機器要是已經中了,就別在原地清理。先把它斷開,再從另一臺乾淨的機器上,把這臺機器存過的所有憑證都輪換一遍。最後把整臺機器重裝。

幕後歸因

知道是誰幹的,對查案的人有幫助,但是對你沒什麼用,因爲這個辦法換誰來抄都管用。這個蠕蟲是 OpenSourceMalware 團隊認定跟朝鮮有關的,而且跟「Contagious Interview」那種假面試的騙局活動連在一起。

我寫了 24 年代碼,這種事看得多了。這件事裏最讓我在意的,是每一次強推其實都記在 GitHub 的活動日誌裏,第一天夜裏就記下了,可是 28 天裏沒有人去翻過那個日誌。要是那些強推事件接了告警,10 月 1 號那天夜裏就會有人被叫起來。

要是這篇讓你去翻了自己的活動日誌,歡迎訂閱這個博客,或者先收藏起來。我會接着寫 AI、安全和開源這幾塊的實事,下一篇還是一個你能自己動手查的東西。

來源:Improwised 的事件文章 https://www.improwised.com/blog/polinrider-malware-infection-cleanup/ ;掃描器 https://github.com/Improwised/polinrider-scanner