iT邦幫忙

2026 iThome 鐵人賽

DAY 10
1

相信許多 IT 人員對 SSH 都不陌生(想陌生都不行)。

lab-01

講到 SSH,不得不提 Telnet。早期常使用 Telnet 遠端操作主機,但傳統 Telnet 以明文傳輸資料。若攻擊者能擷取通訊流量,就可能讀到其中的帳號、密碼與操作內容。

SSH 則提供加密保護,降低通訊內容在傳輸途中遭到竊聽的風險。

SSH 除了使用密碼登入,也支援公鑰驗證。採用這種方式時,客戶端會使用私鑰產生數位簽章,伺服器則確認對應的公鑰是否獲准用於登入目標帳號,並驗證簽章是否正確。以下用流程圖說明這個驗證過程:

lab-02-2

使用公鑰驗證前,通常會先產生一對金鑰:私鑰自行保管,公鑰則配置到伺服器上。以常見的 OpenSSH 設定為例,會將公鑰加入目標帳號的 ~/.ssh/authorized_keys。這是事前準備,不需要每次登入都重新產生金鑰。

進行公鑰驗證時:

  1. 客戶端以私鑰簽署本次驗證資料,其中包含連線識別碼(session identifier)與驗證請求的相關欄位,再送出公鑰與簽章。
  2. 伺服器確認該公鑰是否獲准用於登入目標帳號,並用公鑰驗證簽章。
  3. 公鑰獲授權且簽章正確,就通過公鑰驗證;滿足其餘登入條件後,才允許登入。

這裡呈現的是核心流程;SSH 也允許客戶端先詢問伺服器是否接受某把公鑰,再送出簽章。參考:RFC 4252,第 7 節

重點:在這個驗證流程中,私鑰負責產生簽章,公鑰負責驗證簽章。例如 Ed25519 是數位簽章演算法,不用於資料加解密;但私鑰檔案本身仍可透過 passphrase 加密保護。

私鑰能被用來證明相應的登入身分,因此必須妥善管理檔案擁有者、存取權限與保存位置。

接下來將示範一個因 SSH 私鑰保管不當而造成的提權案例,看看設定檔與歷史紀錄如何成為尋找登入憑證的線索。

實作部分

情資蒐集

我們一樣以 user 帳號登入實驗主機,目標是取得這台主機的 root 權限。先做 SSH 的情資蒐集:

cat ~/.ssh/config 2>/dev/null
cat ~/.ssh/known_hosts 2>/dev/null
cat ~/.bash_history 2>/dev/null | grep -iE 'ssh|scp|rsync|key'
env | grep -i SSH_AUTH_SOCK

lab-03:SSH 設定檔、已知主機與歷史指令

這邊資訊有點多,我們一起慢慢看。

1. ~/.ssh/config:使用者的 SSH 客戶端設定

可以注意到,deploy-target 這個 Host 別名被設定為連線至 127.0.0.1,使用 root 帳號,並指定 /opt/lab10/ansible/deploy_key 作為驗證用的私鑰。

這是值得追查的線索,但設定檔本身還不能證明這把金鑰目前仍能登入 root。接下來要確認檔案是否可讀,以及伺服器是否仍接受它。

2. ~/.ssh/known_hosts:已記錄的伺服器主機公鑰

這個檔案用來儲存主機識別資訊與主機公鑰,讓客戶端在後續連線時比對伺服器提供的公鑰是否一致。它與用來授權使用者登入的 authorized_keys 用途不同。

在一般預設設定下,連線到尚未記錄主機公鑰的伺服器時,會看到這類畫面:

lab-04:首次遇到未知主機公鑰時的確認提示

SSH 會顯示主機公鑰的指紋,詢問是否接受。確認指紋正確並輸入 yes 後,就會記錄該主機公鑰;後續連線若公鑰與紀錄相符,通常不會再次詢問。

要注意,按下 yes 是接受這把主機公鑰,不代表已經確認對方就是預期的主機。主機身分仍應透過可信管道核對。

3. ~/.bash_history:歷史指令

搜尋 ssh、scp、rsync、key 等關鍵字,可能找到曾使用的帳號、主機位址或私鑰路徑。不過,歷史指令只能提供操作線索,不能單靠它確認過去的連線是否成功。

4. SSH_AUTH_SOCK:驗證代理程式的連線線索

SSH_AUTH_SOCK 通常記錄用來連接 ssh-agent 的 Unix socket 路徑。ssh-agent 可以在記憶體中管理已載入的私鑰,並代替客戶端執行簽章,減少直接操作私鑰檔案或重複輸入 passphrase 的需求。

但環境變數有值,不代表 agent 一定可連線,也不代表它已載入金鑰;本例的查詢沒有輸出,因此沒有從這個變數取得可繼續追查的線索。參考:ssh-agent(1)

盤點可讀到的私鑰

接著搜尋目前帳號可讀、可能含有私鑰的檔案:

find / -name 'id_*' ! -name '*.pub' -readable -type f 2>/dev/null

find / -type f -readable \
  \( -name '*.pem' -o -name '*deploy*key*' -o -name '*_key' \) \
  -exec ls -l -- {} + 2>/dev/null

grep -rlZ 'BEGIN OPENSSH PRIVATE KEY' /opt /home /var /tmp 2>/dev/null \
  | xargs -0 -r ls -l

前兩條指令依檔名尋找候選檔案,第三條則搜尋 OpenSSH 私鑰格式的標頭。檔名或標頭只是線索:.pem 也可能是憑證,而其他格式或其他位置的私鑰也可能沒有被這些指令找到。

為什麼要找私鑰?因為前面提到,私鑰可以用來產生登入驗證所需的簽章。所以,找到可讀的私鑰,就找到一條值得進一步驗證的潛在登入途徑。至於能登入哪個帳號、能否解鎖私鑰,以及伺服器是否仍接受對應公鑰,都要繼續確認。

最後我們找到兩把私鑰:

lab-05:可被一般使用者讀取的私鑰

從檔案權限可以看到,私鑰由 root 擁有,但權限是 644,群組與其他使用者也有讀取權限。搭配這次實際讀取成功的結果,可以確認 user 能取得私鑰內容。

找到私鑰後,先嘗試讀取它並輸出對應公鑰。前面的設定檔指定 /opt/lab10/ansible/deploy_key 用於登入 root,所以我們先檢查它:

ssh-keygen -y -f /opt/lab10/ansible/deploy_key

簡單拆解參數:

  • ssh-keygen:OpenSSH 內建的金鑰管理工具。
  • -y:讀取私鑰,將對應的公鑰輸出至標準輸出,不修改原始私鑰檔案。
  • -f <私鑰>:指定要讀取的私鑰檔案。

lab-06:由私鑰輸出對應公鑰

上圖可以看到,工具沒有要求輸入 passphrase,就成功輸出公鑰,表示這把私鑰未受非空 passphrase 保護,可以直接載入。若私鑰有這類保護,就需要先提供正確的 passphrase 才能完成讀取。

這個指令能確認本機可否載入私鑰,但不會驗證伺服器是否接受這把金鑰。接下來才是登入測試。參考:ssh-keygen(1)

帳號提權

根據設定檔的線索,我們嘗試使用這把私鑰登入本機的 root 帳號。以下依截圖中的操作,先複製一份私鑰並將副本權限設為 600:

cp /opt/lab10/ansible/deploy_key /tmp/dk && chmod 600 /tmp/dk
ssh -i /tmp/dk -o StrictHostKeyChecking=no root@127.0.0.1

其中,-i /tmp/dk 指定驗證用的私鑰檔案,root@127.0.0.1 表示透過 SSH 嘗試登入本機的 root 帳號。StrictHostKeyChecking 的作用會在後面補充。

lab-08:複製私鑰並嘗試登入 root

畫面沒有出現帳號密碼提示,並進入 root 的命令列。這個案例展示了:一般使用者若能取得高權限帳號所授權的私鑰,就可能在不知道帳號密碼的情況下取得該帳號的權限。

由於連線目標是 127.0.0.1,這裡是透過本機 SSH 服務提升權限。

驗證補充:登入後可以再執行 id,確認實際的 UID。若要嚴格確認這次登入使用的就是指定私鑰,還可透過 SSH 的 -v 輸出核對被接受的金鑰;單獨使用 -i,不代表客戶端完全不會嘗試其他設定或 agent 提供的身分。

接著補充兩個容易混淆的地方。

私鑰權限是 644,OpenSSH 一定會阻擋嗎?

OpenSSH Portable 的客戶端有一項私鑰權限檢查:當私鑰檔案的擁有者等於執行者的實際 UID,且群組或其他人具有權限時,會顯示 UNPROTECTED PRIVATE KEY FILE 警告,並忽略這把私鑰。

下圖呈現 /tmp/dk 權限為 644 時,客戶端拒絕使用它的結果:

lab-07:私鑰副本權限過寬時的警告

這與前面設為 600 的成功案例是不同的權限狀態。若要在實驗環境重現這個警告,可在副本由目前使用者擁有的前提下執行:

chmod 644 /tmp/dk
ssh -i /tmp/dk root@127.0.0.1

本例接著出現密碼提示,但忽略私鑰後不一定都會改用密碼;客戶端仍會依雙方設定嘗試其他可用的驗證方式,也可能直接登入失敗。

原始的 /opt/lab10/ansible/deploy_key 則由 root 擁有。如果由 user 執行客戶端,這項檢查不會因為檔案具有群組或其他人的權限而拒絕它。不過,沒有觸發這項檢查,不等於一定能登入,仍要確認私鑰可讀、可載入,以及伺服器接受對應公鑰等條件。參考:OpenSSH Portable 的 sshkey_perm_ok()

-o StrictHostKeyChecking=no 的作用

-o 可以在命令列指定 SSH 客戶端設定。StrictHostKeyChecking=no 會放寬本次連線的主機公鑰確認政策,自動接受並記錄新的主機公鑰;對於已變更的主機公鑰,也可能在部分限制下繼續連線。

雖然選項是在命令列指定,新的主機公鑰仍可能寫入 known_hosts,並非完全不留下持續性的紀錄。參考:ssh_config(5) 的 StrictHostKeyChecking

小結

今天的錯誤設定是 SSH 私鑰保管不當:一把可用於登入 root 的私鑰,卻能被一般使用者讀取,讓攻擊者有機會利用它取得 root 權限。

從設定檔找到帳號與私鑰路徑,再確認私鑰可讀、可以載入,最後測試登入,這些步驟才把一條線索逐步連成實際的提權途徑。

公鑰驗證本身不是問題,關鍵在於私鑰的保護與公鑰授權管理。除了設定正確的檔案擁有者與適當權限,例如 600,若私鑰已外洩或疑似遭到複製,還應撤銷舊公鑰的登入授權並更換金鑰,不能只修改原始檔案權限。

這幾天整理了不少 Linux 提權手法,明天我們做個總結,聊聊提權之前有哪些要注意的地方。

謝謝大家的收看,我們明天見!


上一篇
Day 09|PATH Hijacking:為什麼系統會執行到攻擊者的程式?
下一篇
Day 11|從 sudo 到 SSH:回顧提權的成因與成立條件
系列文
我以前被社會打穿,現在輪到我研究怎麼把系統打穿:資安工程師的 30 天紅隊轉職實驗 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言