iT邦幫忙

2026 iThome 鐵人賽

DAY 4
1
AI Security

合法呼叫湊出的攻擊鏈:AI Agent 防禦的 30 天觀念養成系列 第 4

Day 4|Agent 執行任務時,內部到底會怎麼運作?

  • 分享至 

  • xImage
  •  

前言

要看懂「每一步都被允許執行但串連後卻是惡意行為」的動作鏈,得先看懂 agent 執行任務時的內部運作。

今天就來看看一個真的會連網、會自己決定下一步動作的 agent 是如何運作的。(偏基礎導向)(使用Google Colab + Gemini API)


Agent 跟聊天機器人,差在哪一步?

聊天機器人是文字進、文字出:你輸入內容,它會從自己的訓練記憶中找出合適的內容來回覆。

Agent 的動作比起普通聊天機器人還多了一個環節:它在回答你之前,會先去「想辦法執行被交付的任務」——有可能得呼叫一個工具、取得工具執行後回傳的結果、再根據回傳結果決定下一步該做甚麼。所以對於 Agent 而言,我們交付給他的任務不是「單次即可生成」的,而是多個任務執行並串聯起的迴圈:模型決定要呼叫哪個工具 → 你的程式去執行 → 把結果餵回模型 → 模型再決定 → …… → 模型覺得足夠完成你指定的標準了,才給你最終回答。


來吧,我們來簡單的交付任務給 Agent 吧!

這是我們給 Agent 的任務內容:

task = (
    "第一步:fetch https://api.github.com/repos/python/cpython ,"
    "從回傳的 JSON 裡告訴我它有幾顆星(stargazers_count)、主要語言(language)。"
    "第二步:從同一份 JSON 裡找出 owner 的 API 網址(owner 物件裡的 url 欄位),"
    "fetch 那個網址,告訴我這個 owner 有多少個公開 repo(public_repos)。"
    "最後把星數、語言、公開 repo 數整理成一句話回覆我。"
)

接下來就是一些我們寫好的處理程序了。


比較重要的是:模型自己不會、也不能執行工具。它能做的只有去產出一個結構化的請求,一般而言內容大意是「請呼叫 fetch_url,參數 url 帶這個值」。真正去發出網路請求、去執行讀取檔案的動作的,是你自己寫的靜態程式碼。模型僅負責「決定」,你的程式負責「執行」,兩邊是分開的。

程式碼:

!pip install -q google-genai
from google import genai
from google.genai import types
from google.colab import userdata
client = genai.Client(api_key=userdata.get("GEMINI_API_KEY"))
import requests

def fetch_url(url: str) -> str:
    r = requests.get(url, timeout=10)
    return r.text[:8000]

TOOLS = {"fetch_url": fetch_url}

config = types.GenerateContentConfig(
    tools=list(TOOLS.values()),
    automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
)
config = types.GenerateContentConfig(
    tools=list(TOOLS.values()),
    automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
)

如上文,本次範例使用的工具只有一個 fetch_url,做的事很單純:拿一個網址、發 GET 請求、並把接收到的內容回傳。

程式碼:

task = (
    "第一步:fetch https://api.github.com/repos/python/cpython ,"
    "從回傳的 JSON 裡告訴我它有幾顆星(stargazers_count)、主要語言(language)。"
    "第二步:從同一份 JSON 裡找出 owner 的 API 網址(owner 物件裡的 url 欄位),"
    "fetch 那個網址,告訴我這個 owner 有多少個公開 repo(public_repos)。"
    "最後把星數、語言、公開 repo 數整理成一句話回覆我。"
)
contents = [types.Content(role="user", parts=[types.Part.from_text(text=task)])]
trace = []

for step in range(8):  # 設上限 8 次,避免無窮迴圈的浪費
    resp = client.models.generate_content(model="gemini-3.6-flash", contents=contents, config=config)

    calls = resp.function_calls
    if not calls:                       
        print("\n最終回覆:", resp.text)
        break

    contents.append(resp.candidates[0].content)   # 把模型這輪的決定接回對話
    for call in calls:
        result = TOOLS[call.name](**dict(call.args))   # ← 真正執行工具的是這行
        trace.append({"step": step, "action": call.name,
                      "args": dict(call.args),
                      "observation": str(result)[:200]})
        print(f"[步驟 {step}] 呼叫 {call.name}({dict(call.args)})  →  {str(result)[:120]} …")
        contents.append(types.Content(role="user", parts=[types.Part.from_function_response(name=call.name,response={"result": result})]))

而我們寫的這段迴圈就是整個 agent 的核心 —— 它反覆執行「問模型要不要呼叫工具 → 有的話就執行、把結果接回對話 → 再問模型」,直到模型不再需要工具為止。

特別注意迴圈裡真正執行工具的是 TOOLS[call.name](**call.args) 這一行:模型只是回報它想呼叫的名字和參數,是這一行把它的需求轉變成一個真實發生的request。後面談到「權限」與「攔截」時會有所著墨討論。


觀察這個簡易Agent運行的狀況

我給它的任務是:先去 fetch cpython 這個 repo 的資料,回報它幾顆星、主要語言;再從那份資料裡找出 owner 的 API 網址,去 fetch 它,回報這個 owner 有幾個公開專案。

開始執行

輸出:

[步驟 0] 呼叫 fetch_url({'url': 'https://api.github.com/repos/python/cpython'})  →  {"id":81598961,"node_id":"MDEwOlJlcG9zaXRvcnk4MTU5ODk2MQ==","name":"cpython","full_name":"python/cpython","private":fals …
[步驟 1] 呼叫 fetch_url({'url': 'https://api.github.com/users/python'})  →  {"login":"python","id":1525981,"node_id":"MDEyOk9yZ2FuaXphdGlvbjE1MjU5ODE=","avatar_url":"https://avatars.githubusercont …

最終回覆: `python/cpython` 擁有 **75,017** 顆星(`stargazers_count`),主要語言為 **Python**,而其擁有者(owner: python)共有 **94** 個公開專案(`public_repos`)。

如上文,agent 走了兩步:
步驟 0api.github.com/repos/python/cpython
步驟 1api.github.com/users/python,最後回覆「75,017 顆星、主要語言 Python、owner 有 94 個公開專案」。


現在看步驟 1 的網址 api.github.com/users/python——它是哪來的?

答案是 從步驟 0 的/repos/python/cpython 中取回的 JSON 檔裡的 owner.url 欄位找到的。agent 把一段外部找來的內容,原封不動塞進了下一個動作的參數,中間沒有做任何檢查。

反過來想就知道為什麼危險:既然它 fetch 的目標是「內容裡的某個欄位值」,那只要有人能改到那個欄位,就等於能決定 agent 下一步去打哪裡。攻擊者不用直接連你的伺服器,他只要能污染 agent 會讀到的一個欄位就夠了。


把動作軌跡記錄下來

我在迴圈裡順手把每一步記了下來,變成一份結構化的紀錄。

程式碼:

import json
print(json.dumps(trace, ensure_ascii=False, indent=2))

輸出:

{
    "step": 0,
    "action": "fetch_url",
    "args": {
      "url": "https://api.github.com/repos/python/cpython"
    },
    "observation": "{\"id\":81598961,\"node_id\":\"MDEwOlJlcG9zaXRvcnk4MTU5ODk2MQ==\",\"name\":\"cpython\",\"full_name\":\"python/cpython\",\"private\":false,\"owner\":{\"login\":\"python\",\"id\":1525981,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjE1MjU5"
  },
  {
    "step": 1,
    "action": "fetch_url",
    "args": {
      "url": "https://api.github.com/users/python"
    },
    "observation": "{\"login\":\"python\",\"id\":1525981,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjE1MjU5ODE=\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/1525981?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/pyth"
  }

如上文,這份 軌跡(trace) 每一筆就四個欄位:

  • 第幾步(step)
  • 呼叫了哪個工具(action)
  • 帶回了哪些參數(args)
  • 拿回什麼結果(observation)。

這就是這個系列需要擁有的基礎條件,我們要看懂 trace 的輸出 —— agent 做過的每一個動作、它的執行順序、每一步驟獲得甚麼資料並回傳了哪些內容,全都記錄在這裡。希望這次的範例,能讓大家都對Agent是怎麼執行任務的問題都有更清楚的認知。


Agent 的下一步,是它讀取到的內容決定的

今天這句話看似普通,是因為今天我讓它讀取到的資料,是在我的控制範圍內,且是已知來自 GitHub 的安全資料,所以它做了很正常的安全任務。但把如果把這句話倒過來唸的話就會變成:誰能控制 agent 讀到的內容,誰就能控制它的下一步動作。 決定權會從你手上,換到埋藏惡意內容給它的人的手上。

這是甚麼意思呢?請大家仔細想想看剛剛的範例。既然fetch_url接收到什麼網址就輸出什麼內容,而我們剛剛也看到了,那個網址可以是「從讀取到的內容裡挖出來的資料」。那這是不是就代表著,有人可以把步驟 0 讀取回來的內容裡,放置 http://169.254.169.254/latest/meta-data/(雲端主機拿身分憑證的 metadata 位址)或你內網某台服務的位址,agent 也會照抓不誤,然後把抓到的東西再往下傳,對吧?

其實這就是 SSRF,而且它不是攻擊者直接打進來的——是被 agent 讀取到的內容給觸發的。一個看起來人畜無害的小小工具,一旦遇到「動作由內容決定」這件事,就成了攻擊面。


小結

今天把 agent 的運作拆成「決定 → 執行 → 看結果 → 再決定」的基礎迴圈;也學會簡易的捕捉 Agent 的動作軌跡。

明天我們會在同一套程式上,塞一封「有問題」的信進去,看著這條原本的安全軌跡,是因為甚麼原因與方式被突破的。

感謝大家今日份的閱讀。


上一篇
Day 3|防禦機制就像不同崗位的守衛
下一篇
Day 5|如果過度授權 Agent的話 ,會怎樣?
系列文
合法呼叫湊出的攻擊鏈:AI Agent 防禦的 30 天觀念養成16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言