FR-177 C2 掃描報告:CINC Auditor/InSpec connector

⚠️ 歷史紀錄:本報告描述的問題已於 2026-11-07 全部修正並合回 Agent 主線(修正卡 CM-3559~3564、3573~3576、3584,驗收紀錄見母卡 CM-3507)。文中的檔名與行號是掃描當下(commit bab2aa3)的版本,修正後已變動,請勿拿來定位現行程式。

§1

🟡 一句話結論

有一條新問題要排修。 工具找到 2 個問題。F1(高)就是總表已登記的 #223(使用者手填檢測規則包 profile → Agent 執行任意 Ruby),本棒不重報,只當作再次確認。新的是 F2(中):cinc-auditor 用 SSH 連 Linux 目標主機時,從不檢查對方的「主機指紋」(host key,SSH 用來確認「我連到的真的是那台機器」的身分證)。所以內網裡有人冒充目標主機的 IP,Agent 照樣把稽核帳號的密碼交給他。同一支程式裡,OpenSCAP 和 nmap 兩個 connector 都有做這個檢查,只有 InSpec 漏了。兩條都經三個獨立檢查員投票 3 比 0 成立,我也自己開檔核對過。

§2

這一棒在檢查什麼

  • Agent 用 cinc-auditor(開源版 InSpec 合規檢測引擎)以 SSH 或 WinRM(Windows 遠端管理協定)連到客戶的目標主機,跑檢測規則包(profile);
  • 重點是 connector 拿到參數之後:哪些欄位會進到外部指令、SSH/WinRM 帳密怎麼交給 cinc、暫存的設定檔放哪裡、用完有沒有刪、錯誤訊息會不會漏出密碼。

範圍只有 guidant_agent/scan/adapters/connectors/inspec.py 一個檔,約 1,050 行。為了查證,翻過範圍外的 openscap.py、nmap.py、ssh_host_keys.py,以及 cinc 內附的 train-core(SSH 連線函式庫)原始碼。這些檔只為查證而讀,本身不當作發現。

§3

掃到什麼:總覽表

# 這是什麼問題 出事會怎樣 要先有什麼才打得到 在哪裡 怎麼修 嚴重度
F1 手填 profile 原樣交給 cinc-auditor exec Agent 主機被執行任意程式 主控台 run-local-scan 權限 inspec.py:875-878、:913 已登記 #223,本棒不重報 高(已登記)
F2 SSH 連線不驗主機指紋 冒充目標主機的人拿到稽核帳號密碼,或回傳假的檢測結果 攻擊者在客戶內網、能搶到目標主機的 IP(例如 ARP 欺騙:在區網裡謊稱自己是那個 IP) inspec.py:482-488 _build_config 比照 OpenSCAP/nmap,接 ssh_host_keys.py 首次信任後釘住指紋,config 帶 verify_host_key 中
§4

每條詳述

F1(高,已登記 #223,不重報)

_resolve_profile(:872-878)在沒有 _profile(主系統同步的庫內參照)時,直接回傳 params.profile,這個值原樣成為 exec 後面的參數(:913)。中間沒有任何驗證,前面也沒有 --。如果值以 - 開頭,還會被 cinc 當成選項。這裡跟 B5 F1/總表 #223 是同一個問題,連修法都一樣,所以不另外登記。

F2(中)SSH 不驗主機指紋

  • 白話:SSH 第一次連線時,正常的程式要記住對方的指紋,下次指紋不對就拒絕連線。這支 connector 交給 cinc 的設定檔(_build_config,:482)只寫了 host/user/port/password/key_files/sudo,沒有寫 verify_host_key。cinc 內附的 train-core 預設 verify_host_key=false,換算到 net-ssh 就是「:never(永不檢查)」,known_hosts(記錄已知指紋的檔)還被指到 /dev/null。所以 cinc 連到哪台都照單全收。
  • 出事會怎樣:
    • 認證方式是「密碼」時,密碼直接送給冒充者。這組帳號是全租戶共用、可以 sudo 的稽核帳號(:318 註解),拿到一組就等於拿到每台受檢主機。
    • 認證方式是「私鑰」時,私鑰偷不走,但冒充者可以回傳假的指令輸出,讓證據裡出現「全部通過」的假結果。對稽核產品來說,這等於證據被偽造。
  • 要先有什麼:攻擊者已經在客戶內網,而且能搶到目標主機的 IP 或 DNS(ARP 欺騙、搶走一個沒人用的 IP、竄改 DNS)。要偷密碼,還得是密碼認證(預設是私鑰)。
  • 在哪裡:guidant_agent/scan/adapters/connectors/inspec.py:482-488。probe 的 :640 與 exec 的 :912-931 都只帶 --config 和 -t,沒有任何指紋相關參數。ssh_host_keys.py 只被 openscap.py、nmap.py import,inspec.py 沒有用到(我用 grep 確認過)。
  • 怎麼修:比照 OpenSCAP。用 ssh_host_keys.py 做首次信任(TOFU:第一次連線時記住指紋),每次執行時產生一份暫存的 known_hosts,config 裡帶 verify_host_key: "always"(或 accept_new)並指定 user_known_hosts_file。指紋不符時要報成白話的「主機身分改變」。修是另外開卡的事。
  • WinRM 的對應情況:winrm_self_signed=True,或關掉 SSL 走 HTTP 5985 時,WinRM 也一樣不驗對方身分。不過這兩種狀況都是帳密設定裡管理員「明示選的」,預設值是 False(:424、:435),屬於設定選擇,不另外登記。
§5

重點逐條人工查證

卡上重點 人工查過:結論 為什麼
target_uri 怎麼組,host 含 @ : ? # 會不會改變 URI 解析;port 範圍 不成立(URI 那部分) -t 的值固定是 ssh://<_CRED_SET_NAME> 或 winrm://<_CRED_SET_NAME>(:507),只是指到 config 裡那組設定的名字。host 是寫進 JSON 的 host 欄位(:482),根本不進 URI,所以沒有拼字串、也沒有 URI 解析可以被改。port 走 _resolve_port(:438-441)的 int(),沒有檢查範圍,但值來自租戶工具設定,不是任務參數,最壞只是連不上,不算資安問題。目標主機要經授權清冊放行,是呼叫端(B5 已查)的事。
--config 裡的 JSON 放了密碼,檔案權限、放在哪、刪不刪 成立為良好作法(不是問題) _secure_temp_file(:453-473)優先寫到 /dev/shm(記憶體檔案系統),mkstemp 建檔再明確 chmod 0600(只有擁有者能讀寫),finally 一定會刪。私鑰另外寫一個暫存檔,config 裡只放它的路徑,兩層 context manager 巢狀包住(:508-516),例外或取消時也會刪。只有一個小地方:如果 os.fdopen 寫入途中失敗,fd 會由 fdopen 收掉,檔案仍然會被 finally 刪除。sudo 沒有另外的密碼欄位,只有一個 sudo 布林值(:488),所以 sudo 是沿用同一個帳號的權限。
:646 subprocess 繼承整包 os.environ 成立,低風險,不另外登記 _ruby_env(:767)把 os.environ 整包複製給 cinc。Agent 的環境變數裡可能有 AGENT_* 這類設定,cinc 和它載入的 profile Ruby 都讀得到。但能控制 profile 的人本來就能執行任意程式碼(#223),所以這不會開出新的攻擊面。與 C1 第 3 點同一類,建議一起修:只傳 PATH/HOME/LANG/RUBYOPT 這類白名單。
transport 只收 ssh/winrm;winrm_self_signed transport 不成立;self_signed 屬設定選擇 :342-348 遇到未知值就 raise。self_signed 和 ssl 都來自 self.credentials(租戶層工具帳密設定,由管理員設定),預設都是 False。
:238 錯誤分類,stderr 回給使用者前有沒有遮密碼 不成立(密碼不會出現在 URI 裡) 密碼只放在 config 檔裡,-t 是 ssh://<名字>,不會出現 user:pass@host 這種形式。_classify_failure 只回傳 host:port、帳號名稱和原始輸出的最後 500 字(:540)。train 的錯誤訊息不會印 config 裡的密碼,所以這 500 字只會帶到主機與帳號名稱。帳號名稱外露不算機密。
庫內參照 profile_cache 回傳的目錄,進 exec 前有沒有再驗證;lockfile 旗標 不成立 _profile 存在時交給 profile_source().resolve_profile_source(:882)處理,它會對帳 sha256 並安全解壓(在 sync 模組,屬於範圍外,A/B 棒已查),型別不對就 raise,不會偷偷改用手填路徑。connector 這邊沒有再檢查一次目錄是否存在,但如果目錄不存在,cinc 會報「could not fetch」並被歸類為本機失敗(:285),沒有資安影響。不寫 lockfile 的旗標是 --no-create-lockfile(:929)。
_terminate/_run_with_cancel:Ruby fork 殺不殺得乾淨;遠端 session 怎麼收 成立,低風險,不另外登記 _terminate 只對 Popen 那一個 pid 送 SIGTERM,等寬限期後再送 SIGKILL(:699-708),沒有開 process group(start_new_session),所以 cinc 自己 fork 出來的孫行程不會被一起殺掉,可能留下孤兒行程。遠端的 SSH/WinRM session 只能靠 SIGTERM 讓 cinc 自己關,被 SIGKILL 的話,就要等目標主機那邊逾時才會收掉。這是可用性和資源問題,不是資安漏洞。與 C1 第 6 點同一類。
§6

可信度兩層

  1. 工具層:stamp verification.status = verified。2 個候選問題全部經過三視角面板(可達性/影響/防禦)各投 3 票,兩條都 3:0。
  2. 人工層:F2 我自己開了 inspec.py:476-498、:500-516、:640、:912-931,並用 grep 確認 ssh_host_keys 只有 openscap、nmap 有用到。train-core 預設值這一點是採信檢查員讀到的版本(3.16.3/3.16.5 的 ssh.rb:65),我自己沒有重讀 gem 原始碼。F1 對照 :872-878、:913,與 #223 相同。
§7

執行概況(照 stamp 抄)

  • run ID:wf_a5bb8ac0-ee7;原始報告目錄 CLAUDE-SECURITY-20261106-032312/,歸檔到 runs/CLAUDE-SECURITY-20261106-032312/
  • 版本:3d5aa2afda96;effort low;範圍 1 檔
  • verification:status verified、candidates 2、panel_votes 6、panel_quorum_findings 2、unreviewed_candidate_sites 0、incomplete_panel_candidates 0、researchers 1/1、reason 無
  • 研究員被砍的次數:0(journal 裡沒有 failed);整趟約 25 分鐘