C语言中有一类特别有趣的漏洞,格式字符串漏洞。轻则损坏内存,重则读写任意地址的内容。在Python中格式化字符串在Python中还有一种格式化字符串的方法。在旧版本的Python2中,格式化字符串使用如下方法:"Mynameis%s"%('phithon',)"Mynameis%(name)%"%{'name':'phithon'}增加格式化方法到string对象,改进后的formatstring用法是:"Mynameis{}".format('phithon')"Mynameis{name}".format(name='phithon')很多人总是认为两者的区别before和after只是一种不同的写法,其实格式化方法是包罗万象的。文档在这里:https://docs.python.org/3.6/library/string.html#formatstrings举一些例子:“{username}”.format(username='phithon')#Commonusage“{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)#Conversionbase"{user.username}"。format(user=request.username)#获取对象属性"{arr[2]}".format(arr=[0,1,2,3,4])#获取数组键值以上用法为Python2.7和Python3都有,所以可以说是通用用法。格式字符串引起的敏感信息泄露漏洞那么,如果控制了格式字符串,会发送什么?我的思路是这样的,首先我们暂时不能通过格式字符串来执行代码,但是我们可以通过格式字符串在字符串中使用“获取对象属性”、“获取数组值”等方法来查找和获取一些敏感的信息。以Django为例,如下视图:defview(request,*args,**kwargs):template='Hello{user},Thisisyouremail:'+request.GET.get('email')returnHttpResponse(template.format(user=request.user))的目的是显示登录用户的传入邮件地址:但由于我们控制了部分格式字符串,因此会导致一些意想不到的问题。最简单的,例如:输出当前登录用户的哈希密码。看看为什么会出现这个问题:user是当前上下文中唯一的变量,即format函数传入的user=request.user,而Django中的request.user是当前用户对象,包含一个属性password,即用户的密码。所以,{user.password}实际上输出的是request.user.password。如果更改视图:defview(request,*args,**kwargs):user=get_object_or_404(User,pk=request.GET.get('uid'))template='Thisis{user}\'semail:'+request.GET.get('email')returnHttpResponse(template.format(useruser=user))会导致任意用户密码泄露漏洞:利用格式化字符串漏洞泄露Django配置信息上述任意密码泄露案例可能太理想了,我们还是用第一种情况:defview(request,*args,**kwargs):template='Hello{user},Thisisyouremail:'+request.GET.get('email')returnHttpResponse(template.format(user=request.user))我能得到的唯一变量是request.user。在这种情况下我该如何使用它?Django是一个庞大的框架,其数据库关系错综复杂。我们其实可以通过属性之间的关系一点一点挖掘敏感信息。但是Django只是一个框架,没有目标源码很难挖掘信息,所以我的想法是:在Django自带的应用中挖掘一些路径,最后读取Django的配置项。经过查找,发现Django自带的应用“admin”(也就是Django自带的后台)的models.py导入了当前网站的配置文件:所以,思路很明确:我们只需要通过某种方法找到Django默认应用admin的model,然后通过这个model获取settings对象,进而获取数据库账号密码,Web加密密钥等信息。随便列两个,比较有意思的暂时不提:http://localhost:8000/?email={user.groups.model._meta.app_config.module.admin.settings.SECRET_KEY}http://localhost:8000/?email={user.user_permissions.model._meta.app_config.module.admin.settings.SECRET_KEY}Jinja2.8.1模板沙箱绕过字符串格式化漏洞导致真实案例——沙箱绕过Jinjatemplates(https://www.palletsprojects.com/blog/jinja-281-released/)Jinja2是Pythonweb框架中广泛使用的模板引擎,可以直接被Flask/Django等框架引用。Jinja2在防御SSTI(模板注入漏洞)时引入了沙箱机制,这意味着即使模板引擎被用户控制,也无法绕过沙箱执行代码或获取敏感信息。但是由于format带来的字符串格式化漏洞,可以绕过Jinja2.8.1之前的沙箱,进而读取配置文件等敏感信息。可以使用pip安装Jinja2.8:pipinstallhttps://github.com/pallets/jinja/archive/2.8.zip并尝试使用Jinja2沙箱执行格式化字符串格式化漏洞代码:>>>fromjinja2.sandboximportSandboxedEnvironment>>>env=SandboxedEnvironment()>>>classUser(object):...def__init__(self,name):...self.name=name...>>>t=env.from_string(...'{{"{0.__class__.__init__.__globals__}".format(user)}}')>>>t.render(user=User('joe'))成功读取当前环境的所有变量__globals__,如果当前环境导入设置或其他敏感配置项,会导致信息泄露漏洞:相比之下,Jinja2.8.1修复了该漏洞,会抛出SecurityError异常:PEP498中引入了f修饰符和任意代码执行新增字符串类型修饰符:f或F,用f修饰的字符串将可以执行代码。文档在这里https://www.python.org/dev/peps/pep-0498/使用docker体验它:dockerpullpython:3.6.0-slimdockerrun-it--rm--namepy3.6python:3.6.0-slimbashpiinstallipythonipython#还是不要用ipythonpython-c"f'''{__import__('os').system('id')}'''"可以看出这段代码的执行方式和PHP中的非常相似,这是Python中很少见的将字符串直接转换为代码的少数方法之一,这会导致许多“导入”漏洞。比如有些开发者喜欢用eval方法解析json:有了f字符串后,即使我们不把双引号闭合,也可以插入任意代码:但实际应用并不是那么简单,关键是问题仍然是:Python没有提供将普通字符串转换为f-strings的方法。但是从上图中的eval到Python模板中的SSTI,有了这个新方法,可能会有一些突破,留给大家分析。此外,PEP498仅在Python3.6中实现,现在算不上流行,但我相信未来会出现一些实际的漏洞案例。
