⚠️ 歷史紀錄:本報告描述的問題已於 2026-11-07 全部修正並合回 Agent 主線(修正卡 CM-3559~3564、3573~3576、3584,驗收紀錄見母卡 CM-3507)。文中的檔名與行號是掃描當下(commit
bab2aa3)的版本,修正後已變動,請勿拿來定位現行程式。
沒有要處理的漏洞。 工具只提出 1 個候選問題,三位獨立檢查員投票 1 比 2 否決,所以報告是 0 條。我另外把「重點看什麼」七條逐一開檔核對,也沒有找到能打得進去的洞,只有幾個「防線只有一層」的加固建議(列在最後,不算漏洞)。
Agent 用 SSH(paramiko 套件)登入客戶指定的一台「執行主機」,在那台機器上跑 nmap 掃描其他目標,再把報告用 SFTP 拉回來。這支 connector 是在客戶機器上用字串跑遠端指令,shlex.quote(把一段文字包成 shell 不會拆開的形式)是 shell 層唯一的防線。另一支 ssh_host_keys.py 處理 SSH 主機金鑰:第一次連線時記住對方的金鑰,之後金鑰不符就拒絕連線(TOFU,trust on first use,首次信任)。
範圍:guidant_agent/scan/adapters/connectors/nmap.py、guidant_agent/scan/adapters/connectors/ssh_host_keys.py,共 2 檔、約 813 行。為了查證,我翻了範圍外的 guidant_agent/sync/app/allowed_targets.py、scan/app/task_executor.py:74、probe_runner.py:51、local_runs.py:114,這些檔只為查證而讀,本身不當作發現。
| # | 這是什麼問題 | 面板結果 | 結論 |
|---|---|---|---|
| C1(候選) | SSH 主機金鑰是用「使用者打的主機字串」原樣記的,同一個 IPv6 位址換一種寫法(例如 2001:db8:0::5 和 2001:db8::5)會被當成新主機,重新走一次首次信任 |
1 成立(LOW)/2 否決 | 不成立,不報 |
機制是真的: nmap.py:240 把原始字串交給 paramiko,paramiko 用字串完全比對找已記住的金鑰;找不到就走 ssh_host_keys.py:56,記下新金鑰並送出帳密。授權清冊(allowed_targets.py:261)在比對時會把 IP 正規化,但不會把正規化後的值傳回來。
為什麼不算漏洞: 主機字串只有「派工的人」能決定:主系統派下來的任務,或是有 run-local-scan 權限的主控台使用者。能這樣派工的人,本來就能把掃描指到清冊裡任何還沒連過的 IP,直接拿到首次信任,不需要繞換寫法這一圈。純粹在網路上攔截的人又決定不了寫法。所以換寫法並沒有讓誰拿到比原本更多的東西。另外這只對 IPv6 有效:IPv4 的變體寫法會被 ipaddress 拒絕,裸主機名也被清冊擋掉(allowed_targets.py:265)。
可當加固: 在建 SSH 連線前,用 ipaddress 把 host 正規化,記金鑰和查金鑰都用正規化後的值。openscap 也共用這段程式,要一起改。
| # | 重點 | 人工查過 | 為什麼 |
|---|---|---|---|
| 1 | :535-552 遠端指令字串,scan_flag/port_range/targets 有沒有 quote;target 能不能夾帶 nmap 選項(例如 -iL /etc/passwd、--script) |
**不成立(有加固建議) | scan_flag 不 quote,但值只會來自 _SCAN_TYPE_FLAGS 白名單(:268,查不到就 raise),不會是使用者原本填的值。port_range 先過 ^[0-9,\-\s]+$(:158)再 quote。targets 逐一 quote,shell 層是安全的。選項夾帶: 程式裡沒有 -- 分隔,也沒有 target 字元白名單,但 targets 在派工的三個入口(task_executor.py:74、probe_runner.py:51、local_runs.py:114)都要過清冊。清冊會把每個值解析成 IP/CIDR/末段範圍,解析不出來就得到空集合,is_allowed 回 False(allowed_targets.py:186)。-iL、--script 都解析不成 IP,會被拒。實際拼出的指令長這樣:sudo /usr/bin/nmap -sS -oX '/home/u/tmp.Ab12.xml' -p '22,80' 10.0.0.5 10.0.1.0/24(單引號是 shlex.quote 加的;遇到純 IP 這種安全字元時,quote 不會加引號)。加固:** connector 本身不做第二道過濾,完全依賴清冊。建議在 targets 前面加 --,或者 - 開頭就拒。 |
| 2 | :549 sudo 由誰決定、密碼怎麼送;非 syn 也能配 sudo 嗎 |
不成立 | use_sudo 來自工具帳密設定(:199-200),預設 False。不送 sudo 密碼,只靠遠端 sudoers 設 NOPASSWD: /usr/bin/nmap,probe 用 sudo -n 確認(:721)。connect/version 掃描配 sudo 也放行,但這只是多拿 nmap 本身的 root 權限,指令內容還是被前一條鎖住。 |
| 3 | :231 _connect、:210 私鑰暫存檔;look_for_keys/allow_agent |
不成立 | 暫存檔優先放 /dev/shm(:216),用 mkstemp 建立再 chmod 0600,finally 一定刪除,包含 connect 丟例外的路徑(context manager 包住 client.connect)。look_for_keys=False, allow_agent=False 都已關閉(:242),不會拿 Agent 自己的金鑰去試。 |
| 4 | ssh_host_keys.py:46-60 TOFU 取捨 |
刻意設計,不是漏洞 | 金鑰記在 資料目錄/ssh_known_hosts,存檔後 chmod 0600(:59),寫檔有鎖。取捨:某個 IP 第一次連線時如果被攻擊者攔截,Agent 會一直信任攻擊者的機器,帳密也會送給它。這是 SR-03.4 接受的風險:第一次連線前,管理員要拿 probe 回傳的指紋(nmap.py:696)和 ssh-keygen -lf 對照。金鑰不符時的訊息(:81-84)只包含主機、兩個指紋和處理方式,不洩漏帳密。 |
| 5 | :297-339 輸出上限;:599 遠端報告留存 |
**加固建議(不算漏洞) | stdout/stderr 和 SFTP 拉回的 XML(:576 f.read())都沒有大小上限**,60 MB 上限(:457)是在全部讀進記憶體之後才檢查。前提是執行主機已經被攻陷、而且故意送超大輸出,最壞情況是 Agent 記憶體被吃光、行程掛掉(阻斷服務,不會外洩資料)。建議邊收邊計量,超過就斷開。遠端報告放在登入者家目錄(mktemp 預設權限 0600),finally 會 rm -f。刪除失敗時檔案會留下,但只有該帳號和 root 讀得到。 |
| 6 | :625-635 hosts(跳板)與 targets 都過清冊嗎 |
不成立 | TARGET_PARAM_KEYS = ("hosts", "targets", "host", "ips")(allowed_targets.py:37),三個入口的 check(params) 兩個欄位都會檢查,所以跳板主機也必須在清冊內。 |
| 7 | :676-692 probe 的 raw_host 與錯誤訊息 |
不成立(輕微) | raw_host 是 params.host,也在清冊欄位裡。遠端只跑固定字串 command -v '/usr/bin/nmap'、--version、sudo -n ... --version,沒有使用者輸入。錯誤訊息會帶 paramiko 例外原文(例如 Connection refused),但能探測的只有清冊內的主機,而這些主機使用者本來就有權連,算不上內網資訊外洩。 |
verified。「沒東西過關」不等於絕對乾淨:low effort 只跑一個研究員,這是快速初篩。wf_de9cde3e-611;scan_id 41e54ea3-c0fc-47aa-8a16-b5d54dc4d8bc3d5aa2afda96a6d7fe52b3c04db27f88346ed059,branch ws-CM-3507-CM-3527,dirty falsefailed 0)docs/security-scan/FR-177/runs/CLAUDE-SECURITY-20261106-032315/