Python格式化字符串漏洞(Django為例)

vuuo4281 7年前發布 | 18K 次閱讀 Django Python開發

在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

 

 本文由用戶 vuuo4281 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!