Python 格式化字符串漏洞(Django為例)
在C語言里有一類特別有趣的漏洞,格式化字符串漏洞。輕則破壞內(nèi)存,重則讀寫任意地址內(nèi)容。
Python中的格式化字符串
Python中也有格式化字符串的方法,在Python2老版本中使用如下方法格式化字符串:
- "My name is %s" % ('phithon', )
- "My name is %(name)%" % {'name':'phithon'}
后面為字符串對象增加了format方法,改進(jìn)后的格式化字符串用法為:
- "My name is {}".format('phithon')
- "My name is {name}".format(name='phithon')
很多人一直認(rèn)為前后兩者的差別,僅僅是換了一個(gè)寫法而已,但實(shí)際上format方法已經(jīng)包羅萬象了。文檔在此: https://docs.python.org/3.6/library/string.html#formatstrings
舉一些例子吧:
- "{username}".format(username='phithon') # 普通用法
- "{username!r}".format(username='phithon') # 等同于 repr(username)
- "{number:0.2f}".format(number=0.5678) # 等同于 "%0.2f" % 0.5678,保留兩位小數(shù)
- "int: {0:d}; hex: {0:#x}; oct: {0:#o}; bin: {0:#b}".format(42) # 轉(zhuǎn)換進(jìn)制
- "{user.username}".format(user=request.username) # 獲取對象屬性
- "{arr[2]}".format(arr=[0,1,2,3,4]) # 獲取數(shù)組鍵值
上述用法在Python2.7和Python3均可行,所以可以說是一個(gè)通用用法。
格式化字符串導(dǎo)致的敏感信息泄露漏洞
那么,如果格式化字符串被控制,會(huì)發(fā)送什么事情?
我的思路是這樣,首先我們暫時(shí)無法通過格式化字符串來執(zhí)行代碼,但我們可以利用格式化字符串中的“獲取對象屬性”、“獲取數(shù)組數(shù)值”等方法來尋找、取得一些敏感信息。
以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地址:
但因?yàn)槲覀兛刂屏烁袷交址囊徊糠?,將?huì)導(dǎo)致一些意料之外的問題。最簡單的,比如:
輸出了當(dāng)前已登陸用戶哈希過的密碼??匆幌聻槭裁磿?huì)出現(xiàn)這樣的問題:user是當(dāng)前上下文中僅有的一個(gè)變量,也就是format函數(shù)傳入的user=request.user,Django中request.user是當(dāng)前用戶對象,這個(gè)對象包含一個(gè)屬性password,也就是該用戶的密碼。
所以,{user.password}實(shí)際上就是輸出了request.user.password。
如果改動(dòng)一下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(useruser=user))
將導(dǎo)致一個(gè)任意用戶密碼泄露的漏洞:
利用格式化字符串漏洞泄露Django配置信息
上述任意密碼泄露的案例可能過于理想了,我們還是用最先的那個(gè)案例:
- 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是一個(gè)龐大的框架,其數(shù)據(jù)庫關(guān)系錯(cuò)綜復(fù)雜,我們其實(shí)是可以通過屬性之間的關(guān)系去一點(diǎn)點(diǎn)挖掘敏感信息。但Django僅僅是一個(gè)框架,在沒有目標(biāo)源碼的情況下很難去挖掘信息,所以我的思路就是:去挖掘Django自帶的應(yīng)用中的一些路徑,最終讀取到Django的配置項(xiàng)。
經(jīng)過翻找,我發(fā)現(xiàn)Django自帶的應(yīng)用“admin”(也就是Django自帶的后臺)的models.py中導(dǎo)入了當(dāng)前網(wǎng)站的配置文件:
所以,思路就很明確了:我們只需要通過某種方式,找到Django默認(rèn)應(yīng)用admin的model,再通過這個(gè)model獲取settings對象,進(jìn)而獲取數(shù)據(jù)庫賬號密碼、Web加密密鑰等信息。
我隨便列出兩個(gè),還有幾個(gè)更有意思的我暫時(shí)不說:
http://localhost:8000/?email={user.groups.model._meta.app_config.module.admin.settings.SECRET_KEY}
Jinja 2.8.1 模板沙盒繞過
字符串格式化漏洞造成了一個(gè)實(shí)際的案例——Jinja模板的沙盒繞過( https://www.palletsprojects.com/blog/jinja-281-released/)
Jinja2是一個(gè)在Python web框架中使用廣泛的模板引擎,可以直接被被Flask/Django等框架引用。Jinja2在防御SSTI(模板注入漏洞)時(shí)引入了沙盒機(jī)制,也就是說即使模板引擎被用戶所控制,其也無法繞過沙盒執(zhí)行代碼或者獲取敏感信息。
但由于format帶來的字符串格式化漏洞,導(dǎo)致在Jinja2.8.1以前的沙盒可以被繞過,進(jìn)而讀取到配置文件等敏感信息。
大家可以使用pip安裝Jinja2.8:
- pip install
- https://github.com/pallets/jinja/archive/2.8.zip
并嘗試使用Jinja2的沙盒來執(zhí)行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'))
成功讀取到當(dāng)前環(huán)境所有變量__globals__,如果當(dāng)前環(huán)境導(dǎo)入了settings或其他敏感配置項(xiàng),將導(dǎo)致信息泄露漏洞:
相比之下,Jinja2.8.1修復(fù)了該漏洞,則會(huì)拋出一個(gè)SecurityError異常:
f修飾符與任意代碼執(zhí)行
在PEP 498中引入了新的字符串類型修飾符:f或F,用f修飾的字符串將可以執(zhí)行代碼。文檔在此 https://www.python.org/dev/peps/pep-0498/
用docker體驗(yàn)一下:
- 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')}'''"
可見,這種代碼執(zhí)行方法和PHP中的很類似,這是Python中很少有的幾個(gè)能夠直接將字符串轉(zhuǎn)變成的代碼的方式之一,這將導(dǎo)致很多“舶來”漏洞。
舉個(gè)栗子吧,有些開發(fā)者喜歡用eval的方法來解析json:
在有了f字符串后,即使我們不閉合雙引號,也能插入任意代碼了:
不過實(shí)際利用中并不會(huì)這么簡單,關(guān)鍵問題還在于:Python并沒有提供一個(gè)方法,將普通字符串轉(zhuǎn)換成f字符串。
但從上圖中的eval,到Python模板中的SSTI,有了這個(gè)新方法,可能都將有一些突破吧,這個(gè)留給大家分析了。
另外,PEP 498在Python3.6中才被實(shí)現(xiàn),在現(xiàn)在看來還不算普及,但我相信之后會(huì)有一些由于該特性造成的實(shí)際漏洞案例。