iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
Security

Agentic Era,一年來 LLM 到底都挖了些什麼洞!系列 第 5

Day5. SSTI 之二:沙盒之力 啟動!

  • 分享至 

  • xImage
  •  

沒錯,有許多 Template Engine 其實都有內建的沙盒保護。不同於大家最熟知的 Flask/Jinja2,今天就挑 PHP Twig 來談談他們過去由人類發展出來的各種技術堆疊。

PHP Twig

你可能沒聽過 Twig,但做後端/PHP 開發的應該多少都有接觸過 Symfony 這套框架,而 Twig 就是 Symfony 原生的模板引擎。

Twig 一般的 Environment 同樣把 Template Source 當作受信任程式碼;Twig 官方現在的 Security Policy 也直接寫明,只有 Sandbox 才是執行 Untrusted Template 的安全邊界 [1]。在 2015 年的 Twig 1.x 中,啟用方式大致長這樣:

$policy = new Twig_Sandbox_SecurityPolicy(
    array('if'),                          // allowedTags
    array('escape'),                      // allowedFilters
    array('User' => array('getName')),    // allowedMethods
    array('User' => array('name')),       // allowedProperties
    array()                               // allowedFunctions
);

$sandbox = new Twig_Extension_Sandbox($policy, true);
$twig->addExtension($sandbox);

Twig_Sandbox_SecurityPolicy 的五個參數依序就是允許使用的 Tag、Filter、Object Method、Object Property 與 Function。Method/Property 會再依 Class 分組,所以這份 Policy 的意思是:Template 可以呼叫 User::getName() 與讀取 User::$name,但不能因為拿到了 User Object,就順手呼叫上面的其他 Public Method。

Twig_Extension_Sandbox($policy, true) 的第二個參數代表全域啟用 Sandbox。它大致分成兩層檢查:

Template Compile
  -> NodeVisitor 收集 Tag/Filter/Function
  -> 在編譯結果的進入點插入 checkSecurity()

Template Render Start
  -> checkSecurity() 對照 Allow List

Template Runtime
  -> Object Method/Property Access
  -> checkMethodAllowed()/checkPropertyAllowed()
  -> 再依 Object Class 對照 Allow List

這個設計並沒有什麼問題:在 Compile 階段確定的語法就先檢查,必須等到 Runtime 才知道實際 Class 的 Object Access 則晚一點再檢查。理論上,只要所有存取都會經過這兩層 Policy,就算 Template 是攻擊者寫的,也只能使用開發者親自允許的能力。

Sandbox Bypass 出現在同一篇研究的下一段。照理說,如果開發者只允許 userObject.safeMethod(),那 Template 就不該呼叫 userObject.vulnerableMethod();但當時 checkMethodAllowed() 中存在下面這條特例:

public function checkMethodAllowed($obj, $method)
{
    if ($obj instanceof Twig_TemplateInterface || $obj instanceof Twig_Markup) {
        return true;
    }

    // 其他 Object 才會繼續查詢 allowedMethods
    // ...
}

只要 Object 實作 Twig_TemplateInterface 或屬於 Twig_Markup,任何 Method 都會直接視為允許。這條捷徑原本是為了讓 Twig 自己的 Template/Markup 物件能正常運作;偏偏 Sandbox 中一定存在的 _self,剛好就是目前的 Twig_Template,也實作了 Twig_TemplateInterface

也就是說,攻擊者不用在 allowedMethods 裡找到任何東西,呼叫 _self 的 Method 就會直接通過。而 _self 上剛好有一個給 Twig 內部使用的 displayBlock()

public function displayBlock($name, array $context, array $blocks = array(), $useBlocks = true)
{
    if ($useBlocks && isset($blocks[$name])) {
        $template = $blocks[$name][0];
        $block = $blocks[$name][1];
    }

    if (null !== $template) {
        $template->$block($context, $blocks);
    }
}

$blocks 原本應該是 Twig 內部維護的 Block Table,每一筆內容是 [Template Object, Block Method Name]。但這個 Method 又是 Public,而且沒有驗證 Table 裡的第一個元素真的是 Template,因此 James Kettle 構造出下面這段 Payload:

{{_self.displayBlock("id",[],{"id":[userObject,"vulnerableMethod"]})}}

每個參數拆開來看就很清楚:

  • "id"$name,也就是準備顯示的 Block 名稱。
  • []$context,這次呼叫要傳給 Block 的變數環境。
  • {"id":[userObject,"vulnerableMethod"]} 是攻擊者自製的 $blocks;其中 $blocks["id"][0] 會變成 $template = userObject$blocks["id"][1] 則會變成 $block = "vulnerableMethod"
  • $useBlocks 沒有明寫,所以使用預設值 true,程式便會優先採用攻擊者送進來的 $blocks

最後的 Dynamic Dispatch 就不再是呼叫真正的 Twig Block,而是:

$template->$block($context, $blocks);
// 等同 userObject->vulnerableMethod($context, $blocks)

整條 Bypass Chain 也就變成:

_self 實作 Twig_TemplateInterface
  -> checkMethodAllowed(_self, "displayBlock") 無條件放行
  -> displayBlock() 信任攻擊者提供的 Block Table
  -> 動態呼叫 userObject->vulnerableMethod()
  -> 目標 Method 沒有再經過 SecurityPolicy

這裡最關鍵的地方是:Policy 確實有檢查 Method,只是它檢查的是 _self.displayBlock();真正危險的 userObject->vulnerableMethod() 發生在 displayBlock() 內部,Twig 沒有再做第二次檢查。

能不能直接 RCE,仍然取決於開發者把哪些 Object 放進 Template Context;但只要其中有能寫檔、執行程式,或能繼續取得其他危險 Object 的 Public Method,Sandbox 的 Method Allow List 就形同虛設。

Twig 1.20.0 的 Patch [2] 非常精準:既然 Block Table 的 Target 理論上只能是編譯後的 Twig Template,那就在 Dynamic Dispatch 前把這個 Invariant 寫成檢查 [3]

 if (null !== $template) {
+    // avoid RCEs when sandbox is enabled
+    if (!$template instanceof Twig_Template) {
+        throw new \LogicException('A block must be a method on a Twig_Template instance.');
+    }
+
     try {
         $template->$block($context, $blocks);

修補後,$template 如果是 userObject 就會在 Dynamic Dispatch 前直接丟出 Exception;只有真正的 Twig_Template 才能成為 Block Target。如此一來,$block 即使仍然是字串,也只能指向 Twig 編譯出的 Template Method,不能再把任意 Application Object 當作跳板。

1.20.0 同時禁止 Template 透過 envenvironmentgetEnvironment() 取得 Twig Environment,並棄用 _selffromimport 以外的用途。前面那條未開 Sandbox 時的 registerUndefinedFilterCallback("exec") Chain,也因此一起失去入口。

Reference


上一篇
Day4. SSTI: 模板上的攻與防
下一篇
Day 6. 拆家:Mythos 的流水式奇襲,突破沙盒保護網
系列文
Agentic Era,一年來 LLM 到底都挖了些什麼洞!8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言