Python格式化字符串漏洞(Django為例)
在C語言里有一類特別有趣的漏洞,格式化字符串漏洞。輕則破壞內存,重則讀寫任意地址內容。
Python中的格式化字符串
Python中也有格式化字符串的方法,在Python2老版本中使用如下方法格式化字符串:
"My name is %s" % ('phithon', )
"My name is %(name)%" % {'name':'phithon'}
后面為字符串對象增加了format方法,改進后的格式化字符串用法為:
"My name is {}".format('phithon')
"My name is {name}".format(name='phithon')
很多人一直認為前后兩者的差別,僅僅是換了一個寫法而已,但實際上format方法已經包羅萬象了。
舉一些例子吧:
"{username}".format(username='phithon') # 普通用法
"{username!r}".format(username='phithon') # 等同于 repr(username)
"{number:0.2f}".format(number=0.5678) # 等同于 "%0.2f" % 0.5678,保留兩位小數
"int: {0:d}; hex: {0:#x}; oct: {0:#o}; bin: {0:#b}".format(42) # 轉換進制
"{user.username}".format(user=request.username) # 獲取對象屬性
"{arr[2]}".format(arr=[0,1,2,3,4]) # 獲取數組鍵值
上述用法在Python2.7和Python3均可行,所以可以說是一個通用用法。
格式化字符串導致的敏感信息泄露漏洞
那么,如果格式化字符串被控制,會發送什么事情?
我的思路是這樣,首先我們暫時無法通過格式化字符串來執行代碼,但我們可以利用格式化字符串中的“獲取對象屬性”、“獲取數組數值”等方法來尋找、取得一些敏感信息。
以Django為例,如下的view:
def view(request, *args, **kwargs):
template = 'Hello {user}, This is your email: ' + request.GET.get('email')
return HttpResponse(template.format(user=request.user))
原意為顯示登陸用戶傳入的email地址:
但因為我們控制了格式化字符串的一部分,將會導致一些意料之外的問題。最簡單的,比如:
輸出了當前已登陸用戶哈希過的密碼。看一下為什么會出現這樣的問題: user 是當前上下文中僅有的一個變量,也就是format函數傳入的 user=request.user ,Django中 request.user 是當前用戶對象,這個對象包含一個屬性 password ,也就是該用戶的密碼。
所以, {user.password} 實際上就是輸出了 request.user.password 。
如果改動一下view:
def view(request, *args, **kwargs):
user = get_object_or_404(User, pk=request.GET.get('uid'))
template = 'This is {user}\'s email: ' + request.GET.get('email')
return HttpResponse(template.format(user=user))
將導致一個任意用戶密碼泄露的漏洞:
利用格式化字符串漏洞泄露Django配置信息
上述任意密碼泄露的案例可能過于理想了,我們還是用最先的那個案例:
def view(request, *args, **kwargs):
template = 'Hello {user}, This is your email: ' + request.GET.get('email')
return HttpResponse(template.format(user=request.user))
我能夠獲取到的變量只有 request.user ,這種情況下怎么利用呢?
Django是一個龐大的框架,其數據庫關系錯綜復雜,我們其實是可以通過屬性之間的關系去一點點挖掘敏感信息。但Django僅僅是一個框架,在沒有目標源碼的情況下很難去挖掘信息,所以我的思路就是:去挖掘Django自帶的應用中的一些路徑,最終讀取到Django的配置項。
經過翻找,我發現Django自帶的應用“admin”(也就是Django自帶的后臺)的models.py中導入了當前網站的配置文件:
所以,思路就很明確了:我們只需要通過某種方式,找到Django默認應用admin的model,再通過這個model獲取settings對象,進而獲取數據庫賬號密碼、Web加密密鑰等信息。
Jinja 2.8.1 模板沙盒繞過
字符串格式化漏洞造成了一個實際的案例——Jinja模板的沙盒繞過
Jinja2是一個在Python web框架中使用廣泛的模板引擎,可以直接被被Flask/Django等框架引用。Jinja2在防御SSTI(模板注入漏洞)時引入了沙盒機制,也就是說即使模板引擎被用戶所控制,其也無法繞過沙盒執行代碼或者獲取敏感信息。
但由于format帶來的字符串格式化漏洞,導致在Jinja2.8.1以前的沙盒可以被繞過,進而讀取到配置文件等敏感信息。
大家可以使用pip安裝Jinja2.8:
pip install https://github.com/pallets/jinja/archive/2.8.zip
并嘗試使用Jinja2的沙盒來執行format字符串格式化漏洞代碼:
>>> from jinja2.sandbox import SandboxedEnvironment
>>> env = SandboxedEnvironment()
>>> class User(object):
... def __init__(self, name):
... self.name = name
...
>>> t = env.from_string(
... '{{ "{0.__class__.__init__.__globals__}".format(user) }}')
>>> t.render(user=User('joe'))
成功讀取到當前環境所有變量 __globals__ ,如果當前環境導入了settings或其他敏感配置項,將導致信息泄露漏洞:
相比之下,Jinja2.8.1修復了該漏洞,則會拋出一個SecurityError異常:
f修飾符與任意代碼執行
在PEP 498中引入了新的字符串類型修飾符:f或F,用f修飾的字符串將可以執行代碼。
用docker體驗一下:
docker pull python:3.6.0-slim
docker run -it --rm --name py3.6 python:3.6.0-slim bash
pip install ipython
ipython
# 或者不用ipython
python -c "f'''{__import__('os').system('id')}'''"
可見,這種代碼執行方法和PHP中的 <?php "${@phpinfo()}"; ?> 很類似,這是Python中很少有的幾個能夠直接將字符串轉變成的代碼的方式之一,這將導致很多“舶來”漏洞。
舉個栗子吧,有些開發者喜歡用eval的方法來解析json:
在有了f字符串后,即使我們不閉合雙引號,也能插入任意代碼了:
不過實際利用中并不會這么簡單,關鍵問題還在于:Python并沒有提供一個方法,將普通字符串轉換成f字符串。
但從上圖中的eval,到Python模板中的SSTI,有了這個新方法,可能都將有一些突破吧,這個留給大家分析了。
另外,PEP 498在Python3.6中才被實現,在現在看來還不算普及,但我相信之后會有一些由于該特性造成的實際漏洞案例。
來自:https://xianzhi.aliyun.com/forum/read/615.html