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

vuuo4281 4年前
   <p>在C语言里有一类特别有趣的漏洞,格式化字符串漏洞。轻则破坏内存,重则读写任意地址内容。</p>    <h2>Python中的格式化字符串</h2>    <p>Python中也有格式化字符串的方法,在Python2老版本中使用如下方法格式化字符串:</p>    <pre>  <code class="language-python">"My name is %s" % ('phithon', )  "My name is %(name)%" % {'name':'phithon'}</code></pre>    <p>后面为字符串对象增加了format方法,改进后的格式化字符串用法为:</p>    <pre>  <code class="language-python">"My name is {}".format('phithon')  "My name is {name}".format(name='phithon')</code></pre>    <p>很多人一直认为前后两者的差别,仅仅是换了一个写法而已,但实际上format方法已经包罗万象了。</p>    <p>举一些例子吧:</p>    <pre>  <code class="language-python">"{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]) # 获取数组键值</code></pre>    <p>上述用法在Python2.7和Python3均可行,所以可以说是一个通用用法。</p>    <h2>格式化字符串导致的敏感信息泄露漏洞</h2>    <p>那么,如果格式化字符串被控制,会发送什么事情?</p>    <p>我的思路是这样,首先我们暂时无法通过格式化字符串来执行代码,但我们可以利用格式化字符串中的“获取对象属性”、“获取数组数值”等方法来寻找、取得一些敏感信息。</p>    <p>以Django为例,如下的view:</p>    <pre>  <code class="language-python">def view(request, *args, **kwargs):      template = 'Hello {user}, This is your email: ' + request.GET.get('email')      return HttpResponse(template.format(user=request.user))</code></pre>    <p>原意为显示登陆用户传入的email地址:</p>    <p><img src="https://simg.open-open.com/show/c2ae315d2ed016d331263e32c76a786c.jpg"></p>    <p>但因为我们控制了格式化字符串的一部分,将会导致一些意料之外的问题。最简单的,比如:</p>    <p><img src="https://simg.open-open.com/show/5658faa1fccf8163e95644685a4f53a9.jpg"></p>    <p>输出了当前已登陆用户哈希过的密码。看一下为什么会出现这样的问题: user 是当前上下文中仅有的一个变量,也就是format函数传入的 user=request.user ,Django中 request.user 是当前用户对象,这个对象包含一个属性 password ,也就是该用户的密码。</p>    <p>所以, {user.password} 实际上就是输出了 request.user.password 。</p>    <p>如果改动一下view:</p>    <pre>  <code class="language-python">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))</code></pre>    <p>将导致一个任意用户密码泄露的漏洞:</p>    <p><img src="https://simg.open-open.com/show/ae834224f105a1fd1afca612963e3125.jpg"></p>    <h2>利用格式化字符串漏洞泄露Django配置信息</h2>    <p>上述任意密码泄露的案例可能过于理想了,我们还是用最先的那个案例:</p>    <pre>  <code class="language-python">def view(request, *args, **kwargs):      template = 'Hello {user}, This is your email: ' + request.GET.get('email')      return HttpResponse(template.format(user=request.user))</code></pre>    <p>我能够获取到的变量只有 request.user ,这种情况下怎么利用呢?</p>    <p>Django是一个庞大的框架,其数据库关系错综复杂,我们其实是可以通过属性之间的关系去一点点挖掘敏感信息。但Django仅仅是一个框架,在没有目标源码的情况下很难去挖掘信息,所以我的思路就是:去挖掘Django自带的应用中的一些路径,最终读取到Django的配置项。</p>    <p>经过翻找,我发现Django自带的应用“admin”(也就是Django自带的后台)的models.py中导入了当前网站的配置文件:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/25f1bcf2901f1769a2adcec7e1979364.jpg"></p>    <p>所以,思路就很明确了:我们只需要通过某种方式,找到Django默认应用admin的model,再通过这个model获取settings对象,进而获取数据库账号密码、Web加密密钥等信息。</p>    <p><img src="https://simg.open-open.com/show/829459a4092646b4f358582731511f2a.jpg"></p>    <h2>Jinja 2.8.1 模板沙盒绕过</h2>    <p>字符串格式化漏洞造成了一个实际的案例——Jinja模板的沙盒绕过</p>    <p>Jinja2是一个在Python web框架中使用广泛的模板引擎,可以直接被被Flask/Django等框架引用。Jinja2在防御SSTI(模板注入漏洞)时引入了沙盒机制,也就是说即使模板引擎被用户所控制,其也无法绕过沙盒执行代码或者获取敏感信息。</p>    <p>但由于format带来的字符串格式化漏洞,导致在Jinja2.8.1以前的沙盒可以被绕过,进而读取到配置文件等敏感信息。</p>    <p>大家可以使用pip安装Jinja2.8:</p>    <pre>  <code class="language-python">pip install https://github.com/pallets/jinja/archive/2.8.zip</code></pre>    <p>并尝试使用Jinja2的沙盒来执行format字符串格式化漏洞代码:</p>    <pre>  <code class="language-python">>>> 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'))</code></pre>    <p>成功读取到当前环境所有变量 __globals__ ,如果当前环境导入了settings或其他敏感配置项,将导致信息泄露漏洞:</p>    <p><img src="https://simg.open-open.com/show/98098d8263344688a1ad878278420a66.jpg"></p>    <p>相比之下,Jinja2.8.1修复了该漏洞,则会抛出一个SecurityError异常:</p>    <p><img src="https://simg.open-open.com/show/1dbdd52974272bb869d46a9ffc5003ed.jpg"></p>    <h2>f修饰符与任意代码执行</h2>    <p>在PEP 498中引入了新的字符串类型修饰符:f或F,用f修饰的字符串将可以执行代码。</p>    <p>用docker体验一下:</p>    <pre>  <code class="language-python">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')}'''"</code></pre>    <p style="text-align:center"><img src="https://simg.open-open.com/show/3fbf75208b210b4e94046f998208a3ed.jpg"></p>    <p>可见,这种代码执行方法和PHP中的 <?php "${@phpinfo()}"; ?> 很类似,这是Python中很少有的几个能够直接将字符串转变成的代码的方式之一,这将导致很多“舶来”漏洞。</p>    <p>举个栗子吧,有些开发者喜欢用eval的方法来解析json:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/bae988a0174d2effc70cb0d8f016db55.jpg"></p>    <p>在有了f字符串后,即使我们不闭合双引号,也能插入任意代码了:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/0e0bd861a1d99a4ad87f8ec83ea3dc5e.jpg"></p>    <p>不过实际利用中并不会这么简单,关键问题还在于:Python并没有提供一个方法,将普通字符串转换成f字符串。</p>    <p>但从上图中的eval,到Python模板中的SSTI,有了这个新方法,可能都将有一些突破吧,这个留给大家分析了。</p>    <p>另外,PEP 498在Python3.6中才被实现,在现在看来还不算普及,但我相信之后会有一些由于该特性造成的实际漏洞案例。</p>    <p> </p>    <p>来自:https://xianzhi.aliyun.com/forum/read/615.html</p>    <p> </p>