這篇是叫 Claude 從我的筆記整理出來,事情真的發生過,字是他打的,連語氣也是叫他學我的,我只負責瞄一下的喔。
新聞說這個洞有人在用的時候,我家那個已經修好三個禮拜了耶
先講這是什麼東西
我自己有架一台 Gitea,就是一個像 GitHub 的東西,跑在一台便宜的 VPS 上。私人的 repo 都放在那邊,筆記、設定檔、各種小專案,這個 blog 也是。家裡幾台機器每天都會 mkf gits.sync 跟它同步一次,用了幾年,很少去管它。版本是 1.25,有點舊了,可是能用就沒去動它。
症狀:ssh 突然不給進
八月二號,mkf gits.sync 跑一跑,Permission denied (publickey)。昨天還好好的耶。
好,第一個反應是我這邊的 key 壞了吧,或是哪裡設定被我弄到了。可是順便 ssh 上 VPS top 看了一下,CPU 滿的,一個叫 javaa 的東西在跑。我沒有裝 Java 啊。
再看下去就有點涼了:
javaa是一個挖礦程式,旁邊還有一個看門的,它被殺掉會再把它叫起來- gitea 這個使用者的 cron 裡多了一條,每分鐘去某個 IP 抓一段 script 回來直接跑
- gitea 的
authorized_keys裡多了一把我不認識的 ssh key - 資料庫裡多了九十幾個使用者,其中兩個是 admin,還有一個人開了三百多個 repo
這台 Gitea 從頭到尾都好好的在服務喔,網頁開得起來、push 也 push 得進去,一點異狀都沒有。要不是 ssh 那天不給進,大概還會再多挖幾個禮拜。
怎麼來的
回頭查 log,答案有點不好意思。Gitea 有一個設定叫開放註冊,預設是開的,當初架的時候想說反正沒人知道這台,就沒關了。結果七月二十四號開始有人(大概是 bot 吧)來註冊帳號,然後就拿到了 gitea 這個使用者的 shell,裝了挖礦程式、cron、後門 key,順手在資料庫裡幫自己開了兩個 admin,在裡面住了一個多禮拜。
那段時間我完全沒有感覺,因為 Gitea 本身還是好好的在服務啊。挖礦的人也不想被發現,大概是故意把服務留著的吧,不知道耶。
比較幸運的是,整件事都停在 gitea 這個沒有權限的使用者上。root 的 cron 是乾淨的,沒有奇怪的 systemd 服務,沒有被動過的系統檔。對方有 Gitea 的 admin,但沒有機器的 root。
不洗了,重做
先止血:把挖礦程式殺掉、服務停掉、cron 跟那把 key 拿掉、防火牆把那個 IP 擋住,證據留一份。
然後要決定資料庫是要洗還是要重做。對方有 admin 好幾天,資料庫裡每一張表他都碰得到,洗完我也不敢說乾淨。那那,當然是重做啊。
repo 本身倒是可以信的,因為 git 自己有 checksum。76 個 repo 每一個 git fsck,全部過,再打包成 bundle。舊的 Gitea 整個砍掉,資料庫從空的開始,SECRET_KEY、INTERNAL_TOKEN、JWT 全部換新的,設定檔重寫:註冊關掉、OpenID 關掉、Actions 關掉、git hooks 關掉、只聽 localhost 讓前面的 haproxy 擋著。然後把 76 個 repo 從 bundle 匯回去,15 把自己的 ssh key 加回去,順便把 Gitea 升到當時最新的 1.27.1。
中間還順便抓到自己以前寫的 restore script 有一個 bug。裡面有一行 gitea admin auth keys sync,用意是把資料庫裡的 ssh key 重新寫成 authorized_keys,可是這個指令其實不存在,正確的是 gitea admin regenerate keys。它一直默默失敗,三月搬機器的時候就在了,沒人發現。
從發現到 mkf gits.sync 重新通,大概一天。
八月底的新聞
然後八月底,新聞開始講 Gitea 有一個洞,CVE-2026-60004,有人拿來挖礦,CISA 也把它列進有人在用的清單裡。點進去看,欸,這不就是我家七月底的事嗎。
那個洞是這樣:只要有一個 repo 的寫入權限,就可以透過一個 API 塞一個 git hook 進去,然後用 gitea 這個使用者跑任何指令。而「一個 repo 的寫入權限」,開放註冊的站,註冊一個帳號開一個 repo 就有了。拿到 shell 之後資料庫就在手上,幫自己開 admin 只是順手。開放註冊、gitea 使用者、挖礦,三個都對上了。
時間排一排有點妙:
| 日期 | 事情 |
|---|---|
| 7/24 | 我家的 Gitea 開始有人住進來 |
| 7/27 | Gitea 1.27.1 釋出,裡面有修這個洞 |
| 7/28 | 漏洞公告出來,附 PoC |
| 8/2 | 我發現了,重做,升到 1.27.1 |
| 8/25 | CISA 把它列進有人在用的清單 |
| 8/26 | 新聞開始寫,有人回報被拿來挖礦 |
所以我家被打的時候,修好的版本都還沒出來。發現的時候倒是出來了一個禮拜,升上去的 1.27.1 剛好就是有修的那一版,不過那時候我只是想說順便升個級而已,不知道這版就是在修這個喔。
證據沒有留到那麼細,不敢說一定是同一招。可是重做的時候把 git hooks 整個關掉,剛好也堵住了這條路,算是誤打誤撞吧。
接下來的功課才多
Gitea 修好只是開始。對方有 admin,所有的私人 repo 他都看得到,看了好幾天。所以 repo 裡面放過的東西,都要當作已經被看過了。
然後我就開始數 repo 裡到底放過什麼。數完有點想躺下:
- WireGuard 的 server key 跟 9 台 client 的設定檔,全部在 repo 裡
- 以前給幾個服務用的 mTLS,連 CA 的私鑰都 commit 進去了
- 手機上 termux 用的 ssh key,連 passphrase 一起寫在 setup script 裡
- 一個放帳號密碼的檔案,加密得沒有想像中徹底
一樣一個一個來。WireGuard 寫了一個 script 整個換掉,server 一把、client 9 把、外加 preshared key,9 台一台一台重新連。mTLS 直接退休,改成 haproxy 看一個 token 就好,反正 mTLS 平常用起來也很煩。termux 的 key 換成 ed25519,從 repo 搬出去。密碼檔搬進 sops + age 的 vault,每一個值都加密,再用 git-filter-repo 把它從整個 history 裡洗掉,force push,然後家裡每一台的 clone 都要 reset 一次。
弄完大概三天。原本只是一個 ssh 進不去而已耶。
兩個小插曲
第一個。這台 Gitea 是三月從另一台更舊的 VPS 搬過來的,舊的那台後來就刪掉了。這次整理才發現,它的 IP 早就被別人租走了,可是我的 DNS 還有兩個名字指著那個 IP。更好笑的是 ssh config 裡有一條舊的 proxy 設定,會把 ssh 透過其中一個名字繞出去。也就是說,那條如果有人用到,ssh 會乖乖去敲一個陌生人的門。機器刪掉了,名字要記得一起刪。
第二個。洗的時候太用力,把 rclone 的設定檔也當成可疑的東西刪掉了。那個檔案裡面是 Google Drive 的 token,是我的電子書閱讀器 libris 每四小時去 Drive 掃新書用的。結果三天沒有新書,看 log 才發現每次掃都 401。活的東西跟髒的東西要分清楚,不然就是自己把自己關在外面喔。
下次怎麼看
- 自己架的服務,開放註冊先關掉。沒人知道這台不代表 bot 不知道,而且預設是開的。
- 版本不要放到太舊。這次是修好的版本出來三天前就被打了,沒得救,可是平常差太多版,洞就是一直開著的。
- 服務好好的在跑,不代表裡面只有你。偶爾上去
top一下,看看 cron 跟authorized_keys有沒有多出東西。 - key 跟密碼不要放 repo,就算是私人的 repo。一旦出事,repo 裡放過的每一樣東西都要換,數起來會很想躺下。
- 機器刪掉之後,DNS 跟 ssh config 裡指著它的名字也要一起清,不然名字會指到陌生人家裡。
- 清理的時候一邊清一邊想一下,這個是髒東西還是活的服務在用的東西。
新聞說有人在用這個洞的時候,我家那個已經修好三個禮拜了耶