Skip to content
AI Ferryman Alex
Go back

你的home目錄被刪了?先別找入侵者,動手的可能是你自己

你的home目錄被刪了?先別找入侵者,動手的可能是你自己

你有沒有想過,有一天回到電腦前,發現倉庫裏的文件沒了,嚴重一點,整個倉庫都不見了。這種事不用別人黑進 GitHub 也能發生,一條住在你自己電腦上的蠕蟲就能做到,它叫 testmetest。它刪文件的辦法很特別:它自己不急着動手,它等你動手。等你發現令牌被偷、按下吊銷的那一秒,它就把你電腦上的 home 目錄清空。

被刪的到底是什麼:GitHub 網站上的倉庫本體,這次沒有被刪。被刪的是你電腦上的 home 目錄,就是你平時放項目、文件和鑰匙的那個用戶目錄。大多數開發者的倉庫克隆就放在裏面,還沒推送的改動、SSH 鑰匙、.env 文件也都在裏面。home 目錄一清,這些東西一起沒。對天天在本地改代碼的人來說,這和倉庫被刪了沒有區別。

testmetest 不是一個包,是一批包:一串名字以 -testmetest 結尾的 npm 小工具,event-bus-lite-testmetest、data-pipeline-testmetest 這一類,名字聽起來都是正經幹活的。從 10 月 19 日起,這批包陸續出現在 npm 上,其中一些版本帶着蠕蟲。

我維護開源項目,裝包、開倉庫、管令牌就是日常工作。這條蠕蟲的每一步,都是衝着我這類人的日常來的,所以兩份報告我都從頭讀了:一份是 SafeDep 10 月 22 日拆包的那份,一份是 JFrog 順着倉庫查下去、10 月 26 日發的那份。

漫畫:testmetest 蠕蟲守在開發者電腦旁,等着令牌被吊銷的那一刻

SafeDep 拆完包給出的判斷是,它和今年 6 月那條叫 Miasma 的 npm 蠕蟲同源,又多裝了一件從 Mini Shai-Hulud 那裏來的東西,就是那道吊銷引信。血統只能說到這裏,Shai-Hulud 的源碼今年 5 月就公開了,誰都能拿去接着做,所以不能就此認定是原班人馬乾的。

它進機器,是在你裝包的那一步動的手。它沒有用常規的安裝腳本,而是把執行藏進 binding.gyp 裏。binding.gyp 是帶原生代碼的包安裝時要用的編譯配置文件,這類包一裝,它就一定會被處理到。所以 npm install 還在跑,代碼就已經執行了。SafeDep 專門提醒過,連 —ignore-scripts 這個裝包時擋腳本的開關都攔不住它,因爲它根本不靠腳本鉤子。

跑起來之後,它在你的機器上做三件事。第一件是偷憑證,範圍很寬:GitHub 和 npm 的令牌、SSH 私鑰、.env 文件,連 Claude、Codex 這些 AI 工具存在本機的憑證,都一起打包帶走。

第二件是在系統裏留一個看守的服務,常駐着,其中一個叫 net-health-checker,它就是刪文件那道陷阱的本體。第三件是繁殖,它拿到你的 GitHub 令牌之後,會去碰你名下的、還有你能協作的倉庫。

net-health-checker 是這三件事裏最要命的一件。它每 45 秒拿着你那把被偷走的令牌,去問一次 GitHub 還認不認,連續問 48 小時。它等的就是答案變成不認的那一刻。

答案一變成不認,它就動手刪你電腦上的 home 目錄。發現令牌被偷,馬上吊銷,是對的第一反應,每本應急手冊也都是這麼寫的。而這條蠕蟲就是圍着這一秒建的,你按下吊銷的那一下,就是它動手的信號。你的自救動作,成了它的發令槍。

所以真出了事,順序比速度重要。JFrog 的處置清單裏把順序寫得很直白:先把 net-health-checker 和另一個常駐服務 pkg-runtime 從機器上拔掉,然後再去吊銷被盜的令牌。順序要是反了,令牌照樣廢得掉,只是你按下去之前,home 目錄可能已經在刪了。

說明圖:爲什麼搜索查不到被塞進去的大文件,以及爲什麼要先拔掉看守服務再吊銷令牌

它在 GitHub 的倉庫裏沒有刪任何東西,它做的是往裏面塞東西。JFrog 查到的是 42 個真實倉庫、11 個所有者,被塞進了給 AI 編程工具和編輯器的鉤子:Claude Code 的啓動鉤子、Gemini 和 Cursor 的配置、VS Code 打開文件夾時自動運行的任務,全都指向同一個文件,就是 .github 目錄下的 init.js。

這個 init.js 有 12.9 到 13.7 MB,裏面是一個發佈器。以後你或協作者用這些工具打開這個倉庫,鉤子就會把 init.js 跑起來,發佈器藉着打開者的身份幹活,借的是 npm 的可信發佈。可信發佈是倉庫預先跟 npm 綁好的通道,倉庫那邊一觸發,新版本直接就能發,不用當場輸令牌。所以借到的身份當場就能用,新的帶毒包就這樣發出去。倉庫還在,裏面已經不是你放的東西了。

這批倉庫不是一個一個慢慢感染上的。JFrog 還原的時間線是,9 月 28 日有一波腳本化的批量武裝,幾分鐘內推完一個又一個賬號,提交裏寫的作者日期,還是從各倉庫的舊記錄裏抄來的假時間。其中一個被劫的賬號,是 2013 年註冊的一個阿根廷 PHP 開發者,他的 13 個倉庫在兩分鐘裏被裝好。

它能藏這麼久,還有一個原因:GitHub 的代碼搜索不收這麼大的文件,內容接口也取不出來。SafeDep 10 月 22 日發報告的時候說,沒有找到第三方受害者,看到的都是操作者自己的測試賬號,這個結論就是這麼來的:按搜索去查,看得見鉤子,看不見載荷,就會以爲那些倉庫裏放的是空佔位。

JFrog 換了查法。它繞開搜索,把每個賬號名下的倉庫按目錄樹一棵棵枚舉下去,把大文件按原始數據取回來,離線拆開看,才確認 42 個倉庫個個都裝好了引信。它找到的十個版本的大文件個頭各不相同,解碼之後,是同一個大約 2 MB 的發佈器,大小的差別只是編碼方式留下的副產品。

行動後期,它又換了一副樣子。新的包裏連 JavaScript 加載器都沒有了,換成一個預先編譯好的原生插件,旁邊放一個加密的緩存文件。插件先看環境:在 CI 裏、在容器裏、開着調試器或者抓包工具,它就不動,只在真實開發者的機器上才醒。自動掃描器平時待的地方,它都躲開了。

我最在意的是,這兩件事瞄準的是同一種人:一個認真負責的維護者。查倉庫,用平臺給的搜索查;救賬號,先把令牌吊銷。兩步都是標準動作,兩步都被提前讀過一遍。這條蠕蟲算的不是你會不會犯錯,而是你做對的時候,會先做哪一步。

如果你裝過這批包裏帶毒的版本,名單在 SafeDep 和 JFrog 兩份報告裏都有,先把那臺機器按已經失陷來處理。順序照 JFrog 的清單走:先清掉那兩個常駐服務,把丟不起的東西存出來,再換一臺你信得過的乾淨機器,去吊銷和輪換它可能見過的每一把憑證,GitHub、npm、SSH、雲服務和 AI 工具的都算。

如果你維護倉庫,查的時候別隻靠 GitHub 搜索,搜索恰恰是看不見它的那個工具。按目錄樹查 .github/init.js,再看看鉤子文件在不在:.claude、.gemini、.cursor 的配置,還有 .vscode 的任務文件,只要多出一個不是你放的,就要當真處理。

要是你沒裝過這些包,倉庫也不是你的,這件事和你沒有直接關係。可以帶走的只有一條:一把令牌被偷之後,先別急着在中毒的那臺機器上吊銷它,先想清楚,那臺機器上有沒有什麼東西,還在盯着這把令牌的狀態。

這篇的事實來自 SafeDep 和 JFrog 兩份報告,蠕蟲的血統在兩份報告裏都沒有定論,我不替任何一方下結論。JFrog 還提到,同一批被劫的賬號裏,出現過另一套拿 GitHub Actions 去掃 cPanel 漏洞的工具,這是同一批賬號上的關聯活動,是不是同一夥人、同一條鏈,他們沒有下結論,我也不下。

以前講供應鏈安全,講來講去是別裝來路不明的包。這條蠕蟲多走了一步,它把你出事之後的處置流程,也算進了自己的設計裏。對我自己的提醒很具體:手冊還是要照着做,只是按下第一步之前,先看看機器上是不是留着什麼東西,正等着我按下去。說到底,它賭的是我們所有人的第一反應都一樣。你發現自己令牌被偷的那一秒,手會先伸向哪一步?

來源:JFrog 報告原文 ;SafeDep 報告原文


在 X 上討論→

評論都在 X 上——回覆、引用和討論都在那邊進行。