⚠️ 歷史紀錄:本報告描述的問題已於 2026-11-07 全部修正並合回 Agent 主線(修正卡 CM-3559~3564、3573~3576、3584,驗收紀錄見母卡 CM-3507)。文中的檔名與行號是掃描當下(commit
bab2aa3)的版本,修正後已變動,請勿拿來定位現行程式。
沒有要緊急處理的。 只找到 1 個問題,嚴重度「低」:DNS 探測收回覆的迴圈沒有總時限,有人一直丟垃圾封包過來,這次診斷就永遠結束不了,之後的網路診斷全卡在排隊,要重啟 Agent 才恢復。三個獨立檢查員投票 3 比 0 成立,我也自己開檔核對過。卡片列的「重點看什麼」6 條逐條人工查過,都不成立(詳見後面的查證表)。最受關注的「目標字串以 - 開頭、被 ping 當成選項」這條打不到:使用者填的目標在進探測之前,一律先過 ipaddress.ip_address(),只有合法 IP 才會往下走。
主控台「網路診斷」的十項固定探測:ping、traceroute、mtr(持續路徑監測)、MTU、DNS、TCP 埠、TLS 憑證、HTTP、SSH、到主系統(cloud)連線序列,加上共用的 __init__.py(結果格式、目標 Target、執行情境 ProbeContext)。這裡是 Agent 上唯一准許執行外部指令(ping/traceroute)的地方,指令全部經 shared/timeouts.run_cmd 執行:只收字串陣列、shell=False(不經過 shell 解析)、固定逾時、輸出上限 64KB。
範圍:guidant_agent/netdiag/adapters/probes,11 檔、897 行。為了查證目標從哪裡來,翻了範圍外的 netdiag/app/runner.py(組 Target 的地方)、netdiag/app/dashboard.py(讀閘道與 DNS 伺服器)、netdiag/domain/limits.py(埠數上限)、shared/timeouts.py;這些檔只為查證而讀,不當作發現。
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 怎麼修 | 嚴重度 |
|---|---|---|---|---|---|---|
| F1 | DNS 探測等回覆時,收到對不上的封包就丟掉再等,沒有總時限,也不檢查封包是誰送的 | 這次診斷一直不結束,佔住「同時只跑一個診斷」的鎖和排隊位置;之後所有網路診斷(含未同步時唯一能跑的主系統連線診斷)都卡住,直到 Agent 重啟 | ①有網診權限的主控台使用者對「本機 DNS 伺服器」跑 DNS 探測 ②攻擊者就是那台 DNS 伺服器,或在同網段能把 UDP 封包打到 Agent 的臨時埠 | guidant_agent/netdiag/adapters/probes/dns.py:53(_query_server) |
迴圈開始前先算好截止時間,每圈只等剩下的時間,到點就丟逾時;socket 先 connect((server_ip, 53)),或丟掉來源不是 server_ip 的封包;限制最多收幾個封包 |
低 |
為什麼是低: 影響只有「網診功能停擺」,不會洩漏資料、不能執行程式,重啟就好;而且攻擊者要嘛就是那台 DNS 伺服器,要嘛得在網路上看得到、打得到這個查詢。
白話: DNS 探測對本機設定的 DNS 伺服器送一筆查詢,然後在 while True 迴圈裡等回覆。每次 recvfrom 等 N 秒,收到的封包交易號(txid,查詢時隨機產生的 16 位元編號)對不上就丟掉、繼續等。問題有兩個:一是每收到一個垃圾封包,等待的計時就重新開始,沒有「整個探測最多等多久」的上限(不看 ctx.deadline,也不數圈數);二是 socket 沒有 connect,誰送來的封包都收,也不檢查來源位址。
出事會怎樣: 只要每隔幾秒(短於單次逾時)丟一個 txid 錯的 UDP 封包過來,_query_server 就永遠不會回傳。網路診斷同時只能跑一個、排隊只有一格,所以之後的每一次網路診斷都卡住,直到 Agent 重啟。另外,如果偽造的封包剛好猜中 txid(1/65536),報告上會出現假的 DNS 結果。
要先有什麼才打得到: 有網診權限的主控台使用者對 dns 類目標跑 DNS 探測;攻擊者是那台 DNS 伺服器(惡意或被入侵),或者在路徑上、同網段,能把 UDP 封包噴到 Agent 的臨時來源埠。
在哪裡: guidant_agent/netdiag/adapters/probes/dns.py:50-57。:51 只設一次單次逾時,:53 是 while True,:54 的 recvfrom 把來源位址收進 _addr,但從來沒有使用。
怎麼修: 迴圈前先算 end = time.monotonic() + timeout,每圈設 sock.settimeout(剩餘時間),時間到就丟 TimeoutError;socket connect 到 (server_ip, 53),讓作業系統直接擋掉別人送來的封包;再加一個最多收幾個封包的上限。
runner 開檔核對: 成立。 dns.py:50-57 的 socket 沒有 connect、_addr 沒有使用、迴圈沒有任何出口條件,除了收到對得上的回覆,或單次 recvfrom 逾時拋出例外。只要封包持續進來,就不會逾時。嚴重度同意「低」。
| # | 首腦標的重點 | 人工查過 | 為什麼 |
|---|---|---|---|
| 1 | ping.py:27/traceroute.py:28:target.ip 以 - 開頭會被當成選項 |
不成立 | Target 只在 netdiag/app/runner.py 組。使用者填的目標走 _address_target(runner.py:117),先 ipaddress.ip_address() 再過授權清冊,所以進 argv 的一定是合法 IP 字串,不可能以 - 開頭。主系統目標也一樣,先 ip_address(),不是 IP 才用 getaddrinfo 解出位址(:89-101);閘道從 /proc/net/route 解成 IPv4Address,或取 ip -j route 的輸出。小瑕疵:DNS 伺服器位址取自 /etc/resolv.conf 原文,沒過 ip_address(),但要改那個檔得先有 root,不算可利用。建議當加固項:argv 在目標前加 --,或在 Target 建構時統一驗 IP。 |
| 2 | mtr.py 上限、mtu.py 的 size 來源 |
不成立 | mtr 總探測數上限是 limits.CONNECTIONS["mtr"],總時間上限是 limits.TIMEOUT_SEC["mtr"],先到先停,每個探測單次逾時 3 秒;size 是寫死的常數(1472/1452),沒有外部輸入。 |
| 3 | cloud.py:raw=str(e)[:300] 帶出什麼、TLS 有沒有真的驗證 |
不成立 | 目標由 Agent 設定的 cloud_endpoint 算出,不收外部輸入;str(e) 是作業系統或 SSL 的錯誤字串,最多 300 字,不含 DNS 回應原文。TLS 用 ssl.create_default_context()(或主系統信任錨 cafile),會驗證憑證鏈和主機名稱;驗證不過只回原因。tls.py 同樣用預設驗證 context,憑證主體只在驗證通過後才回報。 |
| 4 | http.py 跟不跟重導向、body 存多少;ssh.py 會不會登入;port.py 上限 |
不成立 | http.py:follow_redirects=False,自己追最多 5 跳,換 host 就停;只讀標頭、不讀 body,標頭總長超過 16KB 就停,只保留 6 個指定標頭,每個最多 200 字。而且目標本身已經過授權清冊,不構成新的 SSRF(伺服器端請求偽造)跳板。ssh.py:只讀最多 255 bytes 的版本 banner,不送任何資料、不嘗試認證。port.py:埠數上限 5 個,在 limits.normalize 擋一次、port.run 再擋一次;不提供埠範圍,每個埠之間經 pacer 間隔。 |
| 5 | __init__.py:ctx.timeout() 來源、有沒有探測繞過 run_cmd |
不成立 | timeout() 取固定逾時和 deadline 剩餘時間的較小值(最少 0.5 秒)。grep -rn subprocess probes/ 只命中 __init__.py:4 的註解,沒有任何探測直接呼叫 subprocess。但也因此發現 F1:純 socket 的探測如果自己寫迴圈,ctx.timeout() 管不到。 |
| 6 | 原始輸出進報告,有沒有帶出 Agent 自己的環境資訊 | 不成立 | 進 raw 的是 ping/traceroute 的輸出(統計行、每一跳的 IP,這是刻意要給的),以及 socket 或 SSL 的錯誤字串,都不含容器 hostname 或檔案路徑。代理資訊經 cloud_env.proxy_info(),dashboard._strip_userinfo 會去掉網址裡夾的帳密。 |
verified。--)。所有結論都來自讀程式碼,沒有實際執行或打 exploit。wf_a08f8798-b45;revision 6d1e6ca(working tree 乾淨);effort low;scope guidant_agent/netdiag/adapters/probes(11 檔)verification:status verified;candidates 1;candidates_deduped 1;panel_votes 3;panel_reviewed_findings 1;panel_quorum_findings 1;unreviewed_candidate_sites 0;incomplete_panel_candidates 0;reason null;researchers_dispatched 1/returned 1docs/security-scan/FR-177/runs/CLAUDE-SECURITY-20261106-035701/