← 回到 nvnda.dev EN
閱讀時間 33 分鐘

逆向工程 Burp AT

拆開 PortSwigger 的 agentic AI pentest 工具,看看 agentic AI 實際上是怎麼被工程化的。

把一個東西拆開、搞懂每個零件,再自己做一個出來,大概是學習新概念最好的方法之一。

— M

TL;DR

這篇要講的是逆向工程 Burp AT,也就是 Burp Suite 用來驅動自主 web pentest 的 AI Agent。我做了完整的反編譯(decompilation)、call trace、動態分析和 TLS key 擷取,推導出它確切的 workflow,以及 PortSwigger 的雲端 endpoint,從這些可以看出 AI 的「智慧」到底住在哪裡。

這個架構呈現出五個 Agentic AI Engineering 的實務做法,我會一個一個深入討論,包括它們在 Burp AT 裡是怎麼實作的。最後會有一張架構圖,說明使用者在 Burp AT 介面上的 prompt 跟工具的執行結果,是怎麼跟 PortSwigger 雲端上的智慧溝通的。

讀者最好具備基礎到進階的 Agentic AI Engineering 知識、懂 Java 應用程式,也要對網路有不錯的了解。這篇會談到 SSE、sync/async、TLS handshake,以及從流量擷取到封包檢查等概念。

雖然這篇的資訊都有經過 fact-check,但很多概念是基於我自己的理解和詮釋。如果有任何觀念上的錯誤,歡迎透過 email 或 LinkedIn 跟我說。

希望你讀得開心 :)

目錄

關於 Burp AT

PortSwigger 在 2026 年 7 月 27 日針對 Burp Suite Professional 使用者推出了 Burp AT。它提供一個包在 chatbot 裡的 Agentic AI 能力,可以啟動 Burp Suite 內建的功能並執行任務。

它跟現有的 Burp AI 有什麼不同?

Burp 的老用戶可能知道 Burp AI,它是 Repeater 分頁裡的一個功能,讓使用者可以用 AI 來分析被攔截的封包並採取行動。它會建立一個 AI Task,接下來就交給 AI:產生並測試 payload、探索應用程式的路徑,然後像 pentester 一樣分析它的 response。

把兩者對準同一個 target,就能看出它們的差別。Burp AT 的 enforcement layer(強制執行層)讓測試流程更受控管,預設會在呼叫任何攻擊性工具之前先徵求使用者同意。測試過程中,它會謹慎地產生相關的 payload 和參數值,同時請使用者給回饋,這樣對 opsec 比較友善。相反地,Burp AI 展現的是更自主的測試方式。即使在發動 Intruder 的時候,它也沒有徵求使用者的同意或回饋。

雖然兩者最後都得出差不多的結論,但做法不同,而且 Burp AI 被偵測到的風險比 Burp AT 高。結果的呈現方式也不一樣:Burp AI 會顯示一份「Task Summary」,接下來要做什麼由使用者決定;Burp AT 則可以直接產出 write-up,並進一步做 exploitation。

我們要分析什麼?

Burp AT 由 53 個面向使用者、可以被呼叫的 AI 工具組成,另外還有一個刻意隱藏的工具。工具清單會顯示在 Burp AT 的 UI 上,讓使用者調整設定。

Burp AT 工具設定 —— 啟用或停用每個工具 圖 1 — Enable/Disable:允許或禁止 AI 在 engagement 期間呼叫這個工具。

計畫是搞懂這個工具是怎麼建構的,以及它怎麼跟 MCP client 溝通。分析會分三個目標逐步建立:

現況:拆解 JAR 檔

Burp 是用 Java 寫的,打包成一個獨立可執行的 JAR 檔。這個 JAR 裡有主要的 pentest 引擎:我們常用的工具(Proxy、Repeater、Intruder、Collaborator)、負責把 BApp 整合進 Burp 主介面的 Montoya API、內建瀏覽器、第三方函式庫、資源檔,還有 Agentic AI 工具本身 —— 它們只佔整個 bundle 的一小部分(61,000 個 entry 裡的 2 個目錄)。

這兩個目錄是 burp/agentic 和 burp/ai/sdk。前者包含可以被呼叫、拿來做事的 AI 工具,讓 Burp Suite 成為 MCP server。後者則主要負責跟 MCP client 溝通,也就是住在 PortSwigger 雲端、決定要呼叫哪個工具的 AI Agent。

Burp 是持有工具的 MCP server;雲端是持有智慧的 MCP client MCP 的角色分工:Burp 提供工具,雲端持有智慧。

為了大概了解我們面對的是什麼,我把這兩個目錄的內容解開來看。

burpsuite.jar 分支出它的頂層目錄 圖 2 — Burp Suite JAR 套件裡跟 AI 相關的目錄。

跟 AI agent 相關的主要目錄有兩個:burp/agentic 和 burp/ai/sdk。進一步檢查內容後,可以看出每個目錄各自負責處理什麼:

  1. burp/agentic – Agentic AI 工具。這個目錄定義並設定 AI agent 可以執行的 54 個工具。
  2. burp/ai/sdk – 通往 MCP client 的 transport。這個目錄定義 Burp 怎麼跟 PortSwigger 雲端上的 AI 溝通。

這兩個目錄裡面是依照用途分在不同資料夾的 Java class。

burp/agentic 和 burp/ai/sdk 的子目錄樹 圖 3 — AI 相關目錄底下的子目錄和 class。

burp/agentic 目錄由四個子目錄、兩個 Agentic AI module class,以及一堆被混淆(obfuscated)過的 Z class 組成。我用 CFR 把這些 class 反編譯成可讀的 Java 檔,這樣才能追蹤 end-to-end 的 workflow。完整分析下來,歸納出五個我想討論的 Agentic AI SDK 工程 pattern。

五個 agentic-AI 工程 pattern

在逐一深入之前,先看全貌 —— 這五個 pattern 對應到 Burp AT 的實際 workflow 上,從你的 prompt 一路到雲端再回來。把它當地圖快速掃過就好;下面每個 pattern 都會放大其中一個編號標記。

五個 agentic-AI pattern 對應到 Burp AT 的 workflow 圖 4 — 五個 agentic-AI pattern 對應到 Burp AT 的 workflow。

Pattern 1:Tool Catalog 與 Tool Logic 分離

實務做法

設計一個穩健的 AI Agent 架構,要先辨識出它的元件,再依功能把它們分開。Matt Pocock 在 AI Engineer Europe 的舞台上談過 Deep Module 和 Shallow Module,我在寫這一段的時候這個框架就浮現出來了。他建議用「比較少的介面、但每個介面有很多功能」來隱藏複雜度,而不是做很多小介面、每個都沒什麼功能。這直接反映在 Burp Suite 把每個 class 歸到各自目錄的方式上。

Shallow module 與 deep module 的比較 圖 5 — Shallow module 與 deep module。

除此之外,Burp 把 tool catalog 和 tool logic 分開,解決了糾纏問題(entanglement problem):也就是 AI 工具的邏輯跟環境變數載入、retry loop、函式宣告,以及所有基礎設施程式碼全部混在同一個檔案裡。

在每個 Agentic AI 的建構中,工具的邏輯是最少被碰到的部分,相較之下,像 loader 和 enum catalog 這類基礎設施程式碼被呼叫得更頻繁。把它們分開可以縮短執行時間,因為邏輯只有在需要時才會載入。

推薦閱讀:

Burp Suite 怎麼做

Burp Suite 在 tool/ 子目錄裡定義了它的 54 個 Agentic AI 工具,具體來說是 McpBurpTool class。這個 class 定義了所有現有工具的名稱、描述、群組、deterministic tier 以及相關資訊,跟工具的邏輯是分開的。

McpBurpTool enum class 圖 6 — McpBurpTool class。

tool/ 目錄雖然為每個工具提供了框架,但並沒有實際定義它們。邏輯寫在混淆過的 Z class 裡,這些 class 直接放在 burp/agentic/ 目錄底下。我們拿一個工具 ENCODE_DECODE 的 Z class 當樣本,聚焦在三個元件上:

EncodeDecode 工具的實作 圖 7 — EncodeDecode 工具實作。

元件 1:@AiToolMetadata

我們已經看到,工具的 metadata 存在 McpBurpTool class 裡,而它的邏輯則在對應的混淆 Z class 裡,如截圖所示。既然寫在兩個不同的地方,它們是怎麼串在一起的?

@AiToolMetadata 這個 tag 就是把工具的實作(Z class)跟它在 catalog 裡的 entry(McpBurpTool enum)綁在一起的那條線。它幫忙回答「這個 Z class 是哪個工具?」這個問題。這種實作方式把「工具是什麼」(它的 metadata)跟「工具做什麼」(它的程式碼)乾淨地分開,對 Burp Suite 的設計有幾個好處:

運作方式:

class Zm4  ──@AiToolMetadata(McpBurpTool.ENCODE_DECODE)──▶  McpBurpTool.ENCODE_DECODE
(behavior)                                                  (metadata: id, description, tier, group)

元件 2:AiTool<A extends Record>

就像其他會接收參數的函式一樣,這些工具也是;它們的參數定義在 toolargs/ 目錄裡。這個資料夾裡,每個需要參數的工具都有一個對應的 class 檔,不過 args 檔名可能跟工具名稱本身略有不同。AiTool 元件透過 type parameter 把工具跟它的 args record 綁在一起。

Tool-args 與 sub-structure 的差別

在 AI 的幫忙下,我算了一下有多少工具會接收參數,看看能不能乾淨地對應到 toolargs/ 目錄裡定義的 args class。結果算出來是準確的:

  • toolargs/ 裡有 90 個頂層 class:64 個 record(宣告 extends java.lang.Record)、22 個 enum,以及 4 個 interface。
  • 64 個 record 裡:54 個是工具專屬的 *Args(每個工具一個),另外 10 個是共用的 sub-structure,會被組合進工具專屬的 arg class 裡。

toolargs/ 目錄的拆解 圖 8 — toolargs/ 目錄結構。

算下來總共 54 個 *Args record:51 個至少帶有一個 field(參數),3 個完全沒有,另外再加上 10 個共用的 sub-structure。*Args record 裡有各種 field 型別,包括但不限於:String、boolean、int、List、Map、Optional<…>、enum。

*Args record 圖 9 — *Args record。

共用的 sub-structure 圖 10 — 共用的 sub-structure。

有兩個訊號可以區分 tool-args class 和 sub-structure class:(1) class 名稱本身是不是以 *Args 結尾,以及 (2) 這個 class 是不是另一個 record 或某個 tool class 的組成元件。我們拿 RequestSpec 當樣本,看看第二個訊號在實作裡長什麼樣子。它是另外三個 record 檔的組成元件,但沒有出現在任何 Z class 裡。

註:請忽略 match 的數量,因為這個 demo 只反編譯了部分選定的 Z class。

RequestSpec 在 toolargs/ 裡出現的位置 圖 11 — RequestSpec 在 toolargs/ 裡出現的位置。

另一方面,SendRequestArtifactArgs(它有 RequestSpec 這個元件)沒有出現在任何其他 record 的 header 裡,只出現在 Zbq 的 AiTool 實作和 ToolInvocation 函式呼叫裡。另外值得一提的是,SendRequestArtifactArgs 是某個工具的輸入,因為它是 AiTool<...> 的組成元件。

SendRequestArtifactArgs 在 JAR 檔裡出現的位置 圖 12 — SendRequestArtifactArgs 在 JAR 檔裡出現的位置。

元件 3:回傳 ToolInvocation 的 call() 函式

每個工具都有自己的邏輯,以函式的形式寫在各自的 Z class 裡。這一連串的函式呼叫最後會把值回傳給 MCP Bridge,形式是一個帶著 ToolResult 的 ToolInvocation 物件。實際的回傳值在 ToolResult 裡,而 ToolInvocation 則表示結果是現在就好了(Immediate),還是之後才會來(Async)。

Pattern 2:權限分級(Permission Tiers)

實務做法

治理(governance)很多年來一直是 AI 很早就被關注的議題。前提是要防止 AI 造成傷害,特別是在「攻擊」本身就是預設動詞的領域。PortSwigger 在打造 Burp AT 時就有這個意識,不只針對攻擊 target 限縮權限,也針對每一個工具限縮權限。

理想上,任何自主行動都必須遵守以下三條規則,才算是成熟的治理:

  1. 所有 asset 都有盤點,而且權限依照後果的嚴重程度來限縮。
  2. 人的監督放在會產生後果的地方。
  3. 每一步都落實持續性的治理紀律。

治理 AI 工具的方法很多,但這幾條大概是最低要求,特別是對於做 offensive security 工作的 AI Agent。

推薦閱讀:

Burp Suite 怎麼做

還記得 McpBurpTool 裡定義的每個工具都有一個 DeterministicTier field 嗎?這個 field 對應到 Burp 定義的一個 enum class,決定工具被呼叫前是否需要使用者的許可。它的值會是 ALWAYS_ALLOW、ALWAYS_ESCALATE 或 UNSPECIFIED 其中之一,並反映在 Burp AT UI 的 Manual Settings 裡。

Burp AT UI 裡的 Ask/Act 權限切換 圖 13 — Ask/Act:使用工具前是否要停下來徵求使用者許可。

這就是 Burp AT 跟 Burp AI 不一樣的地方。它嚴格把關攻擊性工具,讓它只在使用者定義的 scope 內採取行動。

Pattern 3:Sync 與 Async 工具執行

實務做法

編排(orchestrate)AI agent 去完成一個複雜任務,常常代表要把工作委派給多個 sub-agent。雖然主要動機是避免主 agent 的 context 塞得太亂,但有意識地產生 sub-agent 還有很多好處。例如平行處理(parallelism)讓多個任務可以同時執行,幫使用者省下很多時間。

Sync 和 async 的差別在於:parent agent 是要等 sub-agent 回傳結果才進行下一步,還是讓 sub-agent 獨立執行、自己繼續原本的 workflow。決定要發動哪種 sub-agent 時,有兩件事值得知道:

  1. 被委派的任務是否需要很長時間才能完成。
  2. Parent agent 的下一步是否依賴 sub-agent 的結果。

一般來說,方法的選擇取決於 parent agent 的下一步需不需要 sub-agent 的結果。但 Burp AT 是依照每個工具預估的執行時間,把它們分成四個 bucket。因此,下面的 Immediate 或 Async 標籤描述的是結果「怎麼回傳」,這也是為什麼一個工具可以是 Async 卻被 inline 等待,或是 Immediate 卻被交給背景任務處理。

推薦閱讀:

Burp Suite 怎麼做

工具執行時,可能馬上就完成,也可能啟動一個長時間執行的任務、之後才回傳結果。完成後,工具會回傳一個物件:Text 或 Error。這些都在同一個 tool/ 子目錄裡,由這兩個 class 處理:

ToolInvocation.class 圖 14 — 工具是立即回傳結果,還是非同步回傳。

ToolResult.class 圖 15 — Text 和 Error 物件是每個工具的回傳型別。

如果硬要依照工具的等待時間來分類,會有四個 bucket:

  1. 立即出結果
  2. 很短的時間(inline 等待,沒有 timeout 上限)
  3. 可能會跑一陣子(超過 timeout 就轉成背景任務)
  4. 不知道要多久才會完成

這四個 bucket 回答的問題是:「我們能不能幫等待時間設一個可以忍受的上限?」每個 bucket 對應到一種機制(Immediate 或 Async),其中 Bucket 3 在 Async 之上多了一個 syncWindow,Bucket 4 則在 Immediate 之上多了一個 task_id。這些機制是從工具在 McpBurpTool metadata 裡的標籤(syncWindow 或 longRunning)來判斷的。

Bucket等待時間機制Flag範例
立即無(立即)Immediate無encode_decode, compute_hash, inspect_scope
Inline 等待直到完成Async無summarize_dataset, list_issues_for_url, edit_source_rows
Inline 等待,可能轉交背景最多 15 秒AsyncsyncWindowsend_request, run_custom_script, poll
已知會長時間執行無(分配 task_id)ImmediatelongRunningcrawl, crawl_and_audit, fuzz_request

Bucket 1:立即出結果。 會立即回傳結果的工具屬於這個 bucket,實作的是 ToolInvocation.Immediate() 機制。這些工具沒有附加任何特定標籤,會等到完成。

Bucket 2:Inline 等待完成。 被 inline 等待、而且沒有硬性 timeout 上限的工具屬於這一類。這些工具實作 ToolInvocation.Async() 機制,會讓呼叫保持開啟。Bridge 會等工具執行完、回傳結果,再透過 MCP/SSE transport 把結果交給 PortSwigger 的 LLM,決定下一步要做什麼。

Bucket 3:有 timeout 上限,超過就轉成背景任務。 不要跟第二個 bucket 搞混,這個 bucket 的工具有 15 秒的等待時間,超過 timeout 上限就會被轉交成背景任務。這些工具實作 ToolInvocation.Async() 機制,並在 metadata 裡多帶一個 syncWindow 物件。

Bucket 4:無法預測完成時間。 這些是已知會長時間執行(幾分鐘到幾小時)的工具。Burp 會幫這些工具分配一個 task_id,讓它們在背景執行,同時允許其他工具平行執行。這些工具的 metadata 附有一個 longRunning 物件,告訴 agent 呼叫這個工具時會產生一個背景任務。它實作的是 ToolInvocation.Immediate() 搭配 task_id 的機制。

Zco class 展示了這些 bucket 實際運作的樣子。這個 class 是一個 MCP adapter,主要有兩個工作:(1) 把 Burp 內部的工具型別註冊成 MCP tool spec,以及 (2) 處理進來的 tool call,並把它導向三個 executor 之一,如下面的片段所示:

Zco 把 tool call 導向各個 executor 圖 16 — Zfq 處理 Bucket 1 和 2,Ztg = Bucket 3,Zse = Bucket 4。

burp.agentic.task.* 從來沒被引用過:Bucket 3 的 SyncWindow != task/SyncWindow.class?

雖然有一個同名的 class,但 Bucket 3 裡的 SyncWindow 其實來自 burp.agentic.tool package 裡的 McpBurpTool enum。它是一個 java.time.Duration field 而不是 class,兩者毫無關係。

javap -p all/burp/agentic/tool/McpBurpTool.class | grep -i syncWindow
private final java.time.Duration syncWindow;
public java.util.Optional<java.time.Duration> syncWindow();

於是就出現一個問題:task/ 裡的 class 在 JAR 的任何地方有被呼叫嗎?我一開始假設它是用來支援長時間執行的任務,但這個假設被推翻了,因為 Async 工具沒有引用任何 task/ 裡的 class,而是引用 McpBurpTool。

for c in Zse Ztg Zfq Zf7; do
  hits=$(javap -v -p all/burp/agentic/$c.class 2>/dev/null | grep -c "agentic/task/")
  echo "$c -> agentic/task refs: $hits"
done
Zse -> agentic/task refs: 0
Ztg -> agentic/task refs: 0
Zfq -> agentic/task refs: 0
Zf7 -> agentic/task refs: 0

把搜尋範圍擴大到整個 JAR 檔,task/ 的引用次數一樣是零。

grep -rla "burp/agentic/task/" all 2>/dev/null | sed 's#all/##' | grep -vc "^burp/agentic/task/"
0

所以沒錯,靜態分析顯示 Bucket 3 的 SyncWindow 跟 task/ 裡定義的 SyncWindow class 是不同的東西,而且 task/ 目錄裡的 class 在整個 JAR bundle 裡完全沒有被引用。

Pattern 4:Thin Client,智慧放在 Server 端

實務做法

Thin client、智慧放在 server 端這個概念已經存在很多年了。它的想法是把運算能力、安全性和管理都集中起來,同時讓 client 端的介面保持輕量。對提供 AI agent 工具給使用者的組織來說,這帶來很大的好處,因為在這裡,智慧本身就是高價值的 asset。

把 system prompt 和 skill 集中放在 server 上,可以做到有條理的管理:改一個地方就能直接影響所有使用者。當然,這也代表 server 必須對攻擊者有高度防護,因為一次致命的入侵會影響很多使用者。只要有夠強、夠完善的保護,這個縮小的攻擊面本身就是一種安全的實作。

推薦閱讀:

Burp Suite 怎麼做

PortSwigger 的雲端上跑著一個 AI model,它根據使用者在 Burp AT 的 prompt 決定要呼叫哪些工具,並根據工具的輸出決定下一步要做什麼。這就是 server 端的智慧,存放了驅動整個 engagement 的所有 skill 和 system prompt。

另一方面,Burp 這端只負責提供工具,完全沒有任何智慧。Burp Suite 用 Guice(一個 Java 的 dependency injection 函式庫)把 54 個 AI 工具綁定到一個 registry 上。這個 registry 是 source of truth,列出雲端 LLM 可以呼叫的每一個工具。

registry/ 子目錄:處於休眠狀態,但揭示了 tool-binding 的運作方式

看 registry/ 資料夾裡的程式碼是怎麼寫的,從它的 workflow 來看,可能會以為這個資料夾裡的 class 是在啟動時一起合作,蒐集並建立 54 個工具的 catalog:

  1. 唯一引用 registry/ 的 class AgenticAiModule,建立一個新的 binding 到 ToolRegistry。
  2. ToolRegistry 透過 @Inject annotation 接收工具集合,然後在啟動時跑迴圈,把它交給 ToolDescriptors。
  3. ToolDescriptors 讀取工具的 annotation 值,解析每個 entry,然後回傳一個包含每個工具 metadata 的 ToolDescriptor 物件給 ToolRegistry。
  4. ToolRegistry 產生每個工具與其 ToolDescriptor 物件的對應表。

流程停在第 4 步,沒有任何一個工具被加進 binding。技術上來說,工具只有在呼叫 Guice 的 addBinding().to(...) 函式時才會被綁定,但在目前的 workflow 裡找不到任何一次呼叫。下面這張圖呈現的是理想中的 workflow 應該長什麼樣子。

預期中的工具註冊 workflow,但從來沒有被呼叫 圖 17 — 工具註冊 workflow。

這樣就只剩最後一個檔案要檢查:AgenticAiBurpModule class。它由五個 Z class 的函式呼叫組成,我們會用動態分析來檢查。

AgenticAiBurpModule class 圖 18 — AgenticAiBurpModule class。

對 AgenticAiBurpModule class 做動態分析

靜態分析讓我們全面了解 Burp AT 的架構,而動態分析則會證實或推翻推導出來的假設,並呈現這些工具實際上是怎麼運作的。

我決定拿 CRAWL_AND_AUDIT 當樣本,它是一個已知會長時間執行的工具,會回傳一個 Immediate 值(也就是 task_id),然後把實際任務派給背景程序。過程中,事件會被擷取下來寫進 log 檔。動態分析用的工具是 JVM 的 Unified Logging Architecture。

在 Burp 啟動時擷取 class-load log 圖 19 — Burp AT 動態分析,在 Burp 啟動時擷取 log。

這會讓 Burp AT 啟動一個任務,可以在它的 dashboard 上分析。

Dashboard 上 Burp 正在執行的任務 圖 20 — Burp 正在執行的任務。

動態分析揭露了 registry/ 目錄的一件事:它從來沒有在自己的 class 以外被引用過。檢查 log、取得載入的不重複 class 數量後,registry 和 task 的載入次數都是 0。

CL=classload-39433.log
uniqcls() { grep -oE "class,load\] [^ ]+" "$CL" | cut -d " " -f2 | sort -u; }

echo "burp.agentic.* total       : $(uniqcls | grep -c '^burp\.agentic\.')"
echo "  tool.*                   : $(uniqcls | grep -c '^burp\.agentic\.tool\.')"
echo "  toolargs.*               : $(uniqcls | grep -c '^burp\.agentic\.toolargs\.')"
echo "  registry.*               : $(uniqcls | grep -c '^burp\.agentic\.registry\.')"
echo "  task.*                   : $(uniqcls | grep -c '^burp\.agentic\.task\.')"
echo "burp.ai.*                  : $(uniqcls | grep -c '^burp\.ai\.')"
echo "io.modelcontextprotocol.*: $(uniqcls | grep -c '^io\.modelcontextprotocol\.')"
burp.agentic.* total       : 873
  tool.*                   : 24
  toolargs.*               : 98
  registry.*               : 0
  task.*                   : 0
burp.ai.*                  : 75
io.modelcontextprotocol.*: 259

我追蹤 log,想找出 registry 目錄是從哪裡被呼叫的。結果發現除了 AgenticAiModule 以外,沒有任何地方引用它,而 AgenticAiModule 綁定的是 ToolRegistry 和 AgentToolBinder。有趣的是,檢查 log 後發現 AgenticAiModule 在 runtime 從來沒有被載入過。

AgenticAiModule 在 class-load log 裡沒有任何 match 圖 21 — 在 log 裡搜尋 AgenticAiModule 文字,結果是零筆 match。

那 registry 到底在哪裡?工具又是怎麼綁定上去的?

還記得 burp/agentic/ 目錄裡的 AgenticAiBurpModule class 還沒檢查過嗎?Log 檔證實這個 class 在 runtime 有被載入,而且在執行期間被引用了好幾次。

AgenticAiBurpModule 在啟動時被載入 圖 22 — AgenticAiBurpModule 在啟動時被載入。

既然知道 Burp 用 multibinder 把所有不重複的 AI 工具綁定到 registry,合理的做法就是去找 Guice multibindings 是在哪裡被 import 的,並算出這五個 Z class 各自引用了多少個不同的 burp.agentic.Z* class。

for c in Zww Zwd Zwa Zwm Zw0; do
  unzip -oq burpsuite.jar "burp/agentic/$c.class" -d clean 2>/dev/null
  java -jar cfr.jar "clean/burp/agentic/$c.class" > "$c.java" 2>/dev/null
  lines=$(wc -l "$c.java" | tr -d ' ')
  mb=$(grep -c "multibindings" "$c.java")
  zref=$(grep -oE "burp\.agentic\.Z[a-z0-9_]+" "$c.java" | sort -u | wc -l | tr -d ' ')
  printf "%-4s  %4sL  multibindings-import=%s  distinctZrefs=%s\n" "$c" "$lines" "$mb" "$zref"
done
Zww     51L  multibindings-import=0  distinctZrefs=2
Zwd    426L  multibindings-import=2  distinctZrefs=130
Zwa     32L  multibindings-import=0  distinctZrefs=6
Zwm    140L  multibindings-import=0  distinctZrefs=19
Zw0    581L  multibindings-import=0  distinctZrefs=9

有兩個強烈的訊號指向 Zwd 就是綁定所有工具的 sub-module:

  1. Zwd 是唯一 import multibindings 的 class,這是 Guice 用來建立 Set<AiTool> 的函式庫,裡面會裝進那 54 個工具。
  2. 跟另外四個比起來,130 個不同的 Z class 引用相當可觀。考量到我們有 54 個 AI 工具要註冊,這個 submodule 值得深入看看。

Zwd class 只有一個函式:configure()。它建立一個新的 Zl 物件,函式剩下的部分大多是 zl.Zm().Zx(...) 呼叫。

被綁定到 registry 的 Z class 圖 23 — 被綁定到 registry 的 Z class。

為了搞懂它在做什麼,我請 AI 幫我把這個函式反混淆(deobfuscate),結果如下:

protected void configure() {
    Multibinder<AiTool<?>> tools = AgentToolBinder.newBinder(binder());
    // Z_u.Z_(this.ZL())  =  Multibinder.newSetBinder(binder(), new TypeLiteral<AiTool<?>>(){})

    tools.addBinding().to(Zm4.class);   // zl.Zm().Zx(Zm4.class)  → encode_decode
    tools.addBinding().to(Zqd.class);   // zl.Zm().Zx(Zqd.class)  → compute_hash
    tools.addBinding().to(Zef.class);   // zl.Zm().Zx(Zef.class)  → add_to_scope
    // … 51 more addBinding().to(...) calls, one per tool
}

回頭參考圖 17 裡工具要怎樣才能成功綁定:它需要一個 addBinding().to(...) 函式。在這個 class 裡,Z_u.Z_(binder) 負責建立 multibinder,然後 zl.Zm().Zx() —— 正是 addBinding().to(...) 函式 —— 被呼叫了 54 次來填滿 multibinder,每個工具一次。

剩下的 workflow 跟圖上呈現的差不多。被綁定的工具會被注入到 registry,只是接下來會透過 SSE 傳到 PortSwigger 的雲端。

工具綁定之後:把 Set 注入 registry

AiTool multibinder 被建立並填滿之後,會被注入到 Ze4 class,然後從注入的 class 建出一個工具的 HashMap。下面是 Ze4 的樣子,不過我想特別強調 @Zk annotation 和 Ze4 constructor 的參數型別,這兩個東西決定了工具集合是怎麼被送到這裡的。

控制 Set 注入的兩個元件 圖 24 — 控制 Set 注入的兩個元件。

元件 1:@Zk annotation

快速盤點一下 Ze4 用到的框架,可以看出 Zk 就是 Google Guice 的 com.google.inject,程式碼本身也很像 GitHub 上開源的 Inject.java class。

Burp AT 的 Zk class 與 GitHub 上的 Guice 並排比較 圖 25 — Burp AT 的 Zk class 與 GitHub 上的 Guice 並排比較。

還記得透過 tools.addBinding().to(Z_class) 函式加進 Set binder 的工具,會被綁定到帶有 @Inject annotation 的 class 上。

元件 2:AiTool<?> set

Zk 這個 Inject annotation 在很多工具 class 裡都有用到,那引擎怎麼知道要把 AiTool Set 注入到哪裡?

Guice 是依照型別來把 producer(provider)跟 consumer(injection point)配對的。每個工具 class 的 constructor 參數型別都不一樣,而除了那個過時的 ToolRegistry class 之外,只有 Ze4 接收 AiTool multibinder 作為參數。既然 multibinder 已經建好並填滿,Ze4 的 @Inject constructor 又明確要求 AiTool,Guice 在建構 Ze4 的時候就能、也一定會把 multibinder 注入進去。

這個 class 剩下的部分就很直接了:建立一個新的 map,裡面裝著 54 個 AiTool instance 和它們的名稱(來自 source of truth:McpBurpTool)。它們存在一個 class 裡,這個 class 負責處理這些工具跟 PortSwigger 雲端背後的 AI 之間的溝通。我們把這個 class 稱為 registry。

Pattern 5:MCP 作為 Transport,透過 SSE/AG-UI 往下串流

實務做法

有沒有注意到你的 AI agent 會「把思考說出來」?那通常是 SSE 和 AG-UI protocol 一起運作的結果。SSE 即時串流事件,AG-UI 則把這些事件結構化,讓使用者可以看著 agent 一步步組出回應。這讓使用者看到的是像「Checking all the available tools…」這樣的訊息,而不是一句籠統的「The AI is thinking」。

AG-UI 對於需要把工具進度呈現給使用者的系統特別有用。它也像一個 adapter,協調不同的 backend,讓系統未來擴充時更容易整合更多基礎設施。

要特別注意的是,Burp AT 用的 AI model 沒辦法從流量或 AG-UI protocol 推導出來,這是 thin client 的必然結果。AG-UI 本身既 transport-agnostic 又 model-agnostic,不會透露太多關於 LLM 的資訊。

推薦閱讀:

Burp Suite 怎麼做

雲端和 Burp 之間的溝通發生在兩個單向通道上:一個用來接收(SSE)訊息,一個用來傳送(POST)訊息。

在這一段,值得先釐清 client 和 server 的角色。嚴格依照 HTTP protocol 的規則,Burp 是 HTTP client,因為連線是它發起的;PortSwigger 的雲端則是 HTTP server,因為它接受連線。這跟 MCP 層的角色不要搞混,在 MCP 層裡,Burp 是 MCP Server,PortSwigger 的雲端是 MCP Client。

下表詳細列出各層裡的主體和它們的角色。

層Burp雲端
MCP ProtocolServer(持有工具)Client(LLM,負責呼叫工具)
SSE / HTTP TransportClient(發起連線)Server(接受連線並串流)

為了讓概念一致,以下的溝通都從 Burp 的角度來描述。Burp 透過 SSE 接收訊息、透過 HTTP POST 傳送訊息,來跟雲端溝通。

透過 SSE GET 從雲端接收訊息

JAR 裡有一個 class 負責處理 registry 裡的工具跟雲端 model 之間的溝通。基本上它會:

  1. 建立一個 MCP server,註冊全部 54 個 AI 工具和它們的 spec。
  2. 註冊一個 message handler,管理進來訊息的路由。
  3. 開一條長時間存在的 SSE GET stream,從雲端接收訊息。

這些步驟由不同的元件各自獨立處理。下面這張圖詳細說明 workflow 以及每個元件負責什麼。

雲端訊息透過 SSE GET 送達 Burp AT 圖 26 — 雲端訊息透過 SSE GET 送達 Burp AT。

AccountScopedMcpSseHub 和 MultiSessionTransportProvider 都是從 ai/sdk/ module import 進來的,最終作為 transport layer,讓 Burp 能跟雲端溝通。

透過 HTTP POST 傳送訊息到雲端

跟 Burp 在整個 session 期間開著一條長時間的 SSE stream 來接收訊息不同,傳送訊息時,Burp 每送一則訊息就要發一次 HTTP POST。依照觸發來源,這個過程可以分成兩個主要的 workflow:

這兩個 workflow 是各自獨立處理的,接下來會分別討論。

Workflow 1:工具結果

在這個 workflow 裡,把訊息送到雲端的流程跟透過 SSE GET 接收訊息的流程很像,每一步各由一個元件負責,共三個元件:

  1. MCP server 接收工具的回傳值,包成 JSON-RPC response,再交給 transport。
  2. AccountScopedMcpTransport 接收 JSON-RPC 訊息,送到 hub。
  3. AccountScopedMcpSseHub 把訊息序列化成 JSON 文字,POST 到 PortSwigger 的雲端。

Burp AT 透過 HTTP POST 把工具結果送到 PortSwigger 的雲端 圖 27 — Burp AT 透過 HTTP POST 把工具結果送到 PortSwigger 的雲端。

因為每送一則訊息到雲端都會發一次 HTTP POST,而 Burp 可能同時跑著好幾個 session(例如一個是被派出去的長時間背景任務,另一個是跟使用者互動),所以每則送往雲端的訊息都會在 HTTP header 裡附上 sessionID 和 connectionID。(參考:AccountScopedMcpSseHub.class 裡的 postForSession)

Workflow 2:使用者 prompt

要追蹤使用者 prompt 的訊息是怎麼送到雲端的,有點棘手,因為明文訊息和所有對話相關的字串都被加密了。負責傳送工具回傳值的那些元件,看起來並不處理使用者的 prompt。

我做了動態分析來找出確切的 class。在搞清楚這個 workflow 之前,總共用了三個工具。

工具 1:Arthas

Arthas 是一個 Java 應用程式的診斷工具。它的運作方式是 attach 到一個正在執行的 JVM 上,讓開發者可以即時看到 method 的參數。這個工具原本是拿來 troubleshooting 用的。

往外送的訊息是用 sendPost() method 傳送的,這個 method 只定義在 McpNetworkProvider 裡,最終在 AccountScopedMcpSseHub 裡被用來 POST 訊息。第一個直覺是去觀察 sendPost() 是怎麼被呼叫的,但我們得先找出哪個 class 是 network provider。把 hub 的 instance dump 出來,就能看到答案,還附帶其他資訊。

Zns 是 network provider class,附帶 SSE 和 POST URL 圖 28 — Zns 是 network provider class,以及 SSE 和 POST URL 的值。

確定了 network provider class 之後,就可以觀察 sendPost 實際運作的樣子。我原本預期會看到明文的使用者 prompt,但在 runtime 擷取中完全沒看到類似的東西,雖然它吐出了 HTTP header 的值之類的資訊。

Zns class 裡 sendPost() 的 runtime 擷取 圖 29 — Zns class 裡 sendPost() 的 runtime 擷取。

工具 2:Mitmproxy

我的第二次嘗試用的是 Mitmproxy。用 local mode 跑、並把範圍限定在 Burp 的 PID,結果在 mitmweb 裡看不到任何封包。終端機反而顯示 TLS handshake 失敗,而且沒有建立任何連線,所以我決定放棄這個方法,改在更底層擷取封包。

工具 3:Wireshark

從工具 1 的輸出我們知道,Burp 是透過 HTTPS 跟雲端的 https://ai.portswigger.net/api/v1/mcp 溝通。把這個資訊搭配一個簡單的 lsof 來快照 Burp 的即時連線,就可以看到這段時間一直在跟 Burp 溝通的確切 host IP 位址:18.165.122.68:443,這是一個 AWS CloudFront 的 IP,走的是 en0 介面。

Burp 完全沒有用任何 native TLS,它純粹實作 JDK JSSE(Java Secure Socket Extension),這對我們有利。這裡的計畫是:

  1. 用 tcpdump 在 en0 介面上擷取流量。
  2. 把一個 key-extraction agent(GitHub)attach 到 Burp 的 PID 上(第 4 步會用到)。
  3. 觸發一次全新的 handshake,並送一個 prompt 給 Burp AT。
  4. 用 key-extraction agent 在 Wireshark 裡解密流量。

純 JSSE 代表 TLS session key 存在 JVM 裡的 Java 物件中,key-extraction agent 可以 hook JSSE 的 key-derivation 程式碼,把 key dump 到一個 key-log 檔裡。另一邊,tcpdump 則被動地擷取加密的流量。把擷取檔和 dump 出來的 key 一起載入 Wireshark,就能解密那些封包,得到明文流量。

流量解密之後,篩選出對話的 stream,就能看到完整的一輪對話明文。使用者的 prompt 是以 POST …/messages 加上 {"text":…} body 往上送,assistant 的回覆則透過 SSE 串流回來。

解密後的 Wireshark client-server 對話擷取 圖 30 — Client-server 溝通的 Wireshark 封包擷取。

親眼看到 AG-UI protocol 實際運作,呈現出 AI agent 完整的每一輪對話,其實滿有趣的:

使用者 prompt:

id:28461a13-9745-4e14-850b-d90ae6d28c8d:9193069
event:CUSTOM
data:{"type":"CUSTOM","timestamp":1788311235033,"name":"hakawai.user_message_persisted","value":{"type":"user_message_persisted","turn_id":"788becad-b745-5fff-b075-d6b04d1380e8","message_id":"f72b39a3-8cbd-5180-90a3-2f0336d3e62d","role":"user","text":"list my scopes","attachments":[],"staged_context":[],"created_at":"2026-09-02T01:07:15.004893Z"}}

Session 重新命名:

id:28461a13-9745-4e14-850b-d90ae6d28c8d:9193070
event:CUSTOM
data:{"type":"CUSTOM","timestamp":1788311235732,"name":"hakawai.session_renamed","value":{"type":"session_renamed","version":1,"burpai_session_id":"28461a13-9745-4e14-850b-d90ae6d28c8d","account_id":"{account_id}","name":"Scope Management Query","name_source":"auto","occurred_at":"2026-09-02T01:07:15.728343203Z","metadata":{"ephemeral":true}}}

Assistant 的回應:

id:28461a13-9745-4e14-850b-d90ae6d28c8d:9193072
event:TEXT_MESSAGE_START
data:{"type":"TEXT_MESSAGE_START","timestamp":1788311243278,"messageId":"98d146fc-6fe9-475f-a4d8-9d6367285756","role":"assistant"}

id:28461a13-9745-4e14-850b-d90ae6d28c8d:9193073
event:TEXT_MESSAGE_CONTENT
data:{"type":"TEXT_MESSAGE_CONTENT","timestamp":1788311243281,"messageId":"98d146fc-6fe9-475f-a4d8-9d6367285756","delta":"Your Burp Suite target scope is currently empty — there are no included or excluded rules configured. To begin testing, provide"}

id:28461a13-9745-4e14-850b-d90ae6d28c8d:9193075
event:TEXT_MESSAGE_CONTENT
data:{"type":"TEXT_MESSAGE_CONTENT","timestamp":1788311243664,"messageId":"98d146fc-6fe9-475f-a4d8-9d6367285756","delta":" the target domains, hosts, or URLs you'd like added to scope."}

結束推理:

id:28461a13-9745-4e14-850b-d90ae6d28c8d:9193076
event:TEXT_MESSAGE_END
data:{"type":"TEXT_MESSAGE_END","timestamp":1788311243737,"messageId":"98d146fc-6fe9-475f-a4d8-9d6367285756","metadata":{"finishReason":"stop"},"finishReason":"stop"}

id:28461a13-9745-4e14-850b-d90ae6d28c8d:9193077
event:RUN_FINISHED
data:{"type":"RUN_FINISHED","timestamp":1788311243740,"threadId":"28461a13-9745-4e14-850b-d90ae6d28c8d","runId":"788becad-b745-5fff-b075-d6b04d1380e8","outcome":{"type":"success"}}

這些 turn 加起來,就是一次完整的對話。

表層的 AG-UI 圖 31 — 表層的 AG-UI。

雲端 endpoint

雖然用 Arthas dump hub 的 instance 時,SSE、POST 和 Claim URL 全都看得到,但也可以透過追蹤 Z class 找出來(比較辛苦的方法)。

SSE/HTTP 的傳訊者 AccountScopedMcpSseHub 是在 Zwm class 裡建立的,裡面包含存放雲端 URL 的 class。這些 URL 就是 SSE GET 和 HTTP POST 的 endpoint。

Zwm.java,帶著 SSE/POST URL 的 hub 圖 32 — Zwm.java,與 SSE/POST URL 溝通的 hub。

追蹤 URL 值的過程包含一連串的解壓縮、反編譯,以及在混淆過的 Z class 裡搜尋;總共跳了 6 次。

直接跳到結果,server 的 endpoint 列出如下:

這些值是從反混淆 Z class 得來的:

zby=string2, zbB=string2, zb2=string3
String string2 = base + "/" + "api/v1/mcp";
String string3 = string2 + "/sessions/{…}/claim";

代入 Zwm 裡的 method 呼叫:

.sseUrl(zcq.Zby()).postUrl(zcq.ZbB()).claimUrlTemplate(zcq.Zb2())

解析之後就是:

.sseUrl(string2).postUrl(string2).claimUrlTemplate(string3)

註:base URL 的值來自 property 和 env,如果 env 和 property 都解析不到,就會退回預設值。這裡假設 property 和 env 是專門給 PortSwigger 自己的 dev/test 用的,production 則用預設值,因為 Arthas 顯示 Burp 是跟 https://ai.portswigger.net 溝通。

Workflow 圖

最後,用一張 PortSwigger 雲端與 Burp AT 工具之間完整的溝通 workflow 來收尾,一直細到使用者 prompt 和工具結果是怎麼跟 LLM 完成一整輪溝通的。

完整的 Burp AT workflow 圖 圖 33 — Burp AT workflow 圖。

綜合靜態分析、動態分析和解密流量的分析,Burp AT 透過提供 54 個專注的工具,讓 Burp Suite 成為一個 MCP server。而真正的智慧(LLM、skill 和 system prompt)則留在 PortSwigger 的雲端。Burp AT 從頭到尾都看不到 model,它的任務是:(1) 送出使用者 prompt 和工具結果,以及 (2) 執行雲端要求的工具。

這個設計有兩點值得注意:

整個過程揭示了:使用者 prompt 在送到雲端之前會經過序列化並透過 TLS 傳輸,而核心 model 始終在使用者端碰不到的地方。