前言
開局一張圖,姿勢全靠yy

模板引擎可以讓(網站)程序實現界面與數據分離,業務代碼與邏輯代碼的分離,這大大提升了開發效率,良好的設計也使得代碼重用變得更加容易。與此同時,它也擴展了黑客的攻擊面。除了常規的 XSS 外,注入到模板中的代碼還有可能引發 RCE(遠程代碼執行)。通常來說,這類問題會在博客,CMS,wiki 中產生。雖然模板引擎會提供沙箱機制,攻擊者依然有許多手段繞過它。
和常見Web注入的成因一樣,也是服務端接收了用戶的輸入,將其作為 Web 應用模板內容的一部分,在進行目標編譯渲染的過程中,執行了用戶插入的惡意內容,因而可能導致了敏感信息泄露、代碼執行、GetShell 等問題。其影響范圍主要取決於模版引擎的復雜性。
小課堂---模板渲染是what?
首先 模板渲染分解為前端渲染和后端渲染,還有瀏覽器渲染。
模板只是一種提供給程序來解析的一種語法,換句話說,模板是用於從數據(變量)到實際的視覺表現(HTML代碼)這項工作的一種實現手段,而這種手段不論在前端還是后端都有應用。
通俗點理解:拿到數據,塞到模板里,然后讓渲染引擎將賽進去的東西生成 html 的文本,返回給瀏覽器,這樣做的好處展示數據快,大大提升效率。
后端渲染
后端渲染HTML的情況下,瀏覽器會直接接收到經過服務器計算之后的呈現給用戶的最終的HTML字符串,這里的計算就是服務器經過解析存放在服務器端的模板文件來完成的,在這種情況下,瀏覽器只進行了HTML的解析,以及通過操作系統提供的操縱顯示器顯示內容的系統調用在顯示器上把HTML所代表的圖像顯示給用戶。
實現:后端拼字符串唄…… (理論上后端模板也是字符串)
好處:模板統一在后端。前端(相對)省事,不占用客戶端運算資源(解析模板),只要不大改結構,文字啥的小修改后端改了就好了。
壞處:占用(部分、少部分)服務器運算資源、,response 出的數據量會(稍)大點,模板改了前端的交互和樣式什么的一樣得跟着聯動修改。
前端渲染
前端渲染就是指瀏覽器會從后端得到一些信息,這些信息或許是適用於題主所說的angularjs的模板文件,亦或是JSON等各種數據交換格式所包裝的數據,甚至是直接的合法的HTML字符串。這些形式都不重要,重要的是,將這些信息組織排列形成最終可讀的HTML字符串是由瀏覽器來完成的,在形成了HTML字符串之后,再進行顯示。
好處:不占用服務端運算資源(解析模板),模板在前端(很有可能僅部分在前端),改結構變交互都前端自己來了,改完自己調就行。不用麻煩后端再聯調神馬的。
壞處:占用(一部分、少部分)客戶端運算資源(解析模板)。前端代碼多點,畢竟包含模板代碼了么。腳本是不是首次下就慢點了(看你在意不在意這個畢竟能304和CDN啥的)。可能造成前后兩份模板的情況,總歸要后端吐出個首屏啥的先讓用戶看見吧。那這部分頁面模板不就是后端拼好了吐出來的么。
示例1:定義一個模板,例如
<html>
<div>{$what}</div>
</html>
這只是一個模板。{$what}是數據。此時不知道數據是什么。
如果我想html里面成為
<div>peiqi</div>
渲染到html代碼里
渲染完成后
<html>
<div>peiqi</div>
</html>
當然這只是最簡答的例子;
一般來說,至少會提供分支,迭代。還有一些內置函數,如格式化等等
那么 {$what}這個數據如何代碼傳入。
比如我工作用的模板引擎是smarty
我定義了一個模板。
<html>
<div>{$what}</div>
</html>
js代碼就是這么寫就可以了
var tpl= new jSmart(tplStr);//tplStr就是模板的字符串。
var content = "Hello World";
tpl.fetch({what:content});
這樣就可以了
其余的就交給引擎去渲染執行了。
示例2:
模板:front.tpl
<div>
{%$a%}
</div>
后端:
設置變量:$smarty->assign('a', 'give data');
展示模板:$smarty->display("front.tpl");
到前端時是渲染好的html串:
<div>
give data
</div>
這種方式的特點是展示數據快,直接后端拼裝好數據與模板,展現到用戶面前。
為什么需要服務器模板
首先解釋一下為什么HTML代碼和應用程序邏輯混合在一起不好,看下面的例子你就知道了。假如你使用下面的代碼為你的用戶提供服務:

這不僅僅是靜態HTML代碼。用戶名是從cookie里獲取並且自動填寫的。這樣一來,只要你之前登錄過該網站,你就無需再次輸入。但是這有一個問題,那就是你必須通過某種形式將值插入到HTML文檔中,有兩種方法可以實現,一種是正確的,一種是有危害的。不過我們要先問一下,為什么要這么做。
下圖展示了解決該問題的完全錯誤的方法:

這段代碼有很多問題,作者不僅僅沒有對用戶的輸入進行處理,HTML代碼和PHP代碼復雜的混合在一起,非常難以理解。部分HTML代碼分布在多個函數中,這還不算什么,當你嘗試修改HTML代碼中任何內容時,你才發現有多難受,比如新增css類或修改HTML標簽的順序。
上面這個例子顯然是有意寫的這么爛的,不過可以通過格式化進行優化。然而,在大型的代碼庫中,即使代碼格式良好,也會很快變得無法管理。這就是為什么我們需要模板。
與上面混亂的代碼相比,服務器端模板提供了一種更加簡單的方法來管理動態生成的HTML代碼。最大的優點就是你可以在服務器端動態生成HTML頁面,看起來跟靜態HTML頁面一樣。現在我們來看看,當我們使用服務器端模板時,復雜的代碼看起來如何。

這對前面的代碼做了一些優化,它依然是靜態的。為了顯示正確的信息而不是大括號占位符,我們需要一個替換它們的模板引擎。后端代碼可能是這樣的。

這段代碼的意思非常清楚,首先加載login.tpl模板文件,然后對與模板中名稱相同的變量賦值(大括號里的變量),然后調用show()函數,相應的替換它們的內容並輸出HTML代碼。然而,我們在模板中增加了新的功能,這將會向用戶展示模板渲染的時間。
什么是服務端模板注入
通過模板,Web應用可以把輸入轉換成特定的HTML文件或者email格式。就拿一個銷售軟件來說,我們假設它會發送大量的郵件給客戶,並在每封郵件前SKE插入問候語,它會通過Twig(一個模板引擎)做如下處理:
$output = $twig->render( $_GET['custom_email'] , array("first_name" => $user.first_name) );
有經驗的讀者可能迅速發現 XSS,但是問題不止如此。這行代碼其實有更深層次的隱患,假設我們發送如下請求:
custom_email={{7*7}} // GET 參數
49 // $output 結果
還有更神奇的結果:
custom_email={{self}} // GET 參數
Object of class
__TwigTemplate_7ae62e582f8a35e5ea6cc639800ecf15b96c0d6f78db3538221c1145580ca4a5
could not be converted to string // 錯誤
我們不難猜到服務器執行了我們傳過去的數據。每當服務器用模板引擎解析用戶的輸入時,這類問題都有可能發生。除了常規的輸入外,攻擊者還可以通過 LFI(文件包含)觸發它。模板注入和 SQL 注入的產生原因有幾分相似——都是將未過濾的數據傳給引擎解析。
為什么我們在模板注入前加“服務端”呢?這是為了和 jQuery,KnockoutJS 產生的客戶端模板注入區別開來。通常的來講,前者甚至可以讓攻擊者執行任意代碼,而后者只能 XSS。
注入原理
<?php
require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
Twig_Autoloader::register(true);
$twig = new Twig_Environment(new Twig_Loader_String());
$output = $twig->render("Hello {{name}}", array("name" => $_GET["name"])); // 將用戶輸入作為模版變量的值
echo $output;
?>
使用 Twig 模版引擎渲染頁面,其中模版含有 {{name}} 變量,其模版變量值來自於 GET 請求參數 $_GET["name"]。
顯然這段代碼並沒有什么問題,即使你想通過 name 參數傳遞一段 JavaScript 代碼給服務端進行渲染,也許你會認為這里可以進行 XSS,
但是由於模版引擎一般都默認對渲染的變量值進行編碼和轉義,所以並不會造成跨站腳本攻擊:

但是,如果渲染的模版內容受到用戶的控制,情況就不一樣了。修改代碼為:
<?php
require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
Twig_Autoloader::register(true);
$twig = new Twig_Environment(new Twig_Loader_String());
$output = $twig->render("Hello {$_GET['name']}"); // 將用戶輸入作為模版內容的一部分
echo $output;
?>

上面這段代碼在構建模版時,拼接了用戶輸入作為模板的內容,現在如果再向服務端直接傳遞 JavaScript 代碼,用戶輸入會原樣輸出,測試結果顯而易見。
在 Twig 模板引擎里, {{var}} 除了可以輸出傳遞的變量以外,還能執行一些基本的表達式然后將其結果作為該模板變量的值,例如這里用戶輸入 name={{2*10}} ,則在服務端拼接的模版內容為:

這里簡單分析一下,由於 {# comment #} 作為 Twig 模板引擎的默認注釋形式,所以在前端輸出的時候並不會顯示,而 {{2*8}} 作為模板變量最終會返回 16 作為其值進行顯示,因此前端最終會返回內容 Hello IsVuln16OK ,如下圖:

漏洞發掘
每一個(重)模板引擎都有着自己的語法(點),Payload 的構造需要針對各類模板引擎制定其不同的掃描規則,就如同 SQL 注入中有着不同的數據庫類型一樣。
簡單來說,就是更改請求參數使之承載含有模板引擎語法的 Payload,通過頁面渲染返回的內容檢測承載的 Payload 是否有得到編譯解析,有解析則可以判定含有 Payload 對應模板引擎注入,否則不存在 SSTI。
模板語言的語法和 HTML 語法相差甚大,因此我們可以用其獨特的語法來探測漏洞。雖然各種模板的實現細節不大一樣,不過它們的基本語法大致相同,我們可以發送如下 payload:
smarty=Hello ${7*7}
Hello 49
freemarker=Hello ${7*7}
Hello 49
來確認漏洞。
在一些環境下,用戶的輸入也會被當作模板的可執行代碼。比如說變量名:
personal_greeting=username
Hello user01
這種情況下,XSS 的方法就無效了。但是我們可以通過破壞 template 語句,並附加注入的HTML標簽以確認漏洞:
personal_greeting=username<tag>
Hello
personal_greeting=username}}<tag>
Hello user01 <tag>
但是,如果渲染的模版內容受到用戶的控制,情況就不一樣了。修改代碼為:
<?php
require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
Twig_Autoloader::register(true);
$twig = new Twig_Environment(new Twig_Loader_String());
$output = $twig->render("Hello {$_GET['name']}"); // 將用戶輸入作為模版內容的一部分
echo $output;
?>
上面這段代碼在構建模版時,拼接了用戶輸入作為模板的內容,現在如果再向服務端直接傳遞 JavaScript 代碼,用戶輸入會原樣輸出,測試結果顯而易見。
模板引擎注入
模板引擎
模板引擎(這里特指用於Web開發的模板引擎)是為了使用戶界面與業務數據(內容)分離而產生的,它可以生成特定格式的文檔,用於網站的模板引擎就會生成一個標准的HTML文檔。
一些模板引擎:Smarty,Mako,Jinja2,Jade,Velocity,Freemaker和Twig,模板注入是一種注入攻擊,可以產生一些特別有趣的影響。對於AngularJS的情況,這可能意味着XSS,並且在服務器端注入的情況下可能意味着遠程代碼執行。
重點來了,不同引擎有不同的測試以及注入方式!
flask/jinja2模板注入
Flask是一個使用 Python 編寫的輕量級 Web 應用框架。其 WSGI 工具箱采用 Werkzeug ,模板引擎則使用 Jinja2
Flask框架中提供的模版引擎可能會被一些無量開發者利用引入一個服務端模版注入漏洞,如果對此感到有些困惑可以看看James Kettle在黑帽大會中分享的議題(PDF),簡而言之這個漏洞允許將語言/語法注入到模板中。在服務器的context中執行這個輸入重現,根據應用的context可能導致任意遠程代碼執行(遠端控制設備)
參考文章:https://www.freebuf.com/articles/web/88768.html
參考文章:https://www.freebuf.com/articles/web/98928.html
PHP/模版引擎Twig注入
可以參考本博客文章 Flask從零到無 。
這里給出一個漏洞環境代碼,本地測試
from flask import Flask
from flask import render_template
from flask import request
from flask import render_template_string
app = Flask(__name__)
@app.route('/test',methods=['GET', 'POST'])
def test():
template = '''
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
''' %(request.url)
return render_template_string(template)
if __name__ == '__main__':
app.debug = True
app.run()
代碼簡析: 我們自己簡單寫一個string類型的 html,html返回當前url,我們放入到渲染函數render_template_string進行渲染,然后頁面會打印出當前url,如果url里含有{{}} 那么便可以進行模板注入。
測試url http://127.0.0.1:5000/test?{{config}}
測試結果如下:

而如果我們使用render_template函數,
@app.route('/',methods=['GET', 'POST'])
@app.route('/index',methods=['GET', 'POST'])#我們訪問/或者/index都會跳轉
def index():
return render_template("index.html",title='Home',user=request.args.get("key"))
index.html
<html>
<head>
<title>{{title}} - 小豬佩奇</title>
</head>
<body>
<h1>Hello, {{user}}!</h1>
</body>
</html>
那么將不會有模板注入,因為render_template已經傳入一個固定好了的模板,沒法再去修改,在渲染之后傳入數據,只有當第一種代碼,我們模板可控的時候,先傳入后渲染,這樣才會導致ssti模板注入。
參考:https://www.freebuf.com/vuls/83999.html
CTF---模板注入
tornado的一道 ssti
https://blog.csdn.net/cccccfive/article/details/83145669
防御
為了防止此類漏洞,你應該像使用eval()函數一樣處理字符串加載功能。盡可能加載靜態模板文件。
注意:我們已經確定此功能類似於require()函數調用。因此,你也應該防止本地文件包含(LFI)漏洞。不要允許用戶控制此類文件或其內容的路徑。
另外,無論在何時,如果需要將動態數據傳遞給模板,不要直接在模板文件中執行,你可以使用模板引擎的內置功能來擴展表達式,實現同樣的效果。
附表
